diff --git a/include/search_renderer.h b/include/search_renderer.h index 22dccef1f..1e1fa9596 100644 --- a/include/search_renderer.h +++ b/include/search_renderer.h @@ -102,11 +102,19 @@ class SearchRenderer this->pageLength = pageLength; } + std::string renderTemplate(const std::string& tmpl_str); + /** * Generate the html page with the resutls of the search. */ std::string getHtml(); + /** + * Generate the xml page with the resutls of the search. + */ + std::string getXml(); + + protected: std::string beautifyInteger(const unsigned int number); zim::SearchResultSet m_srs; diff --git a/src/search_renderer.cpp b/src/search_renderer.cpp index f5bf15447..f80a0f9e1 100644 --- a/src/search_renderer.cpp +++ b/src/search_renderer.cpp @@ -87,6 +87,16 @@ void SearchRenderer::setSearchProtocolPrefix(const std::string& prefix) this->searchProtocolPrefix = prefix; } +std::string extractValueFromQuery(const std::string& query, const std::string& key) { + const std::string p = key + "="; + const size_t i = query.find(p); + if (i == std::string::npos) { + return ""; + } + std::string r = query.substr(i + p.size()); + return r.substr(0, r.find("&")); +} + kainjow::mustache::data buildQueryData ( const std::string& searchProtocolPrefix, @@ -99,6 +109,10 @@ kainjow::mustache::data buildQueryData ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern, true); ss << "&" << bookQuery; query.set("unpaginatedQuery", ss.str()); + auto lang = extractValueFromQuery(bookQuery, "books.filter.lang"); + if(!lang.empty()) { + query.set("lang", lang); + } return query; } @@ -162,7 +176,7 @@ kainjow::mustache::data buildPagination( return pagination; } -std::string SearchRenderer::getHtml() +std::string SearchRenderer::renderTemplate(const std::string& tmpl_str) { // Build the results list kainjow::mustache::data items{kainjow::mustache::data::type::list}; @@ -185,8 +199,8 @@ std::string SearchRenderer::getHtml() results.set("items", items); results.set("count", kiwix::beautifyInteger(estimatedResultCount)); results.set("hasResults", estimatedResultCount != 0); - results.set("start", kiwix::beautifyInteger(resultStart+1)); - results.set("end", kiwix::beautifyInteger(min(resultStart+pageLength, estimatedResultCount))); + results.set("start", kiwix::beautifyInteger(resultStart)); + results.set("end", kiwix::beautifyInteger(min(resultStart+pageLength-1, estimatedResultCount))); // pagination auto pagination = buildPagination( @@ -201,14 +215,15 @@ std::string SearchRenderer::getHtml() searchBookQuery ); - std::string template_str = RESOURCE::templates::search_result_html; - kainjow::mustache::mustache tmpl(template_str); kainjow::mustache::data allData; + allData.set("protocolPrefix", protocolPrefix); allData.set("results", results); allData.set("pagination", pagination); allData.set("query", query); + kainjow::mustache::mustache tmpl(tmpl_str); + std::stringstream ss; tmpl.render(allData, [&ss](const std::string& str) { ss << str; }); if (!tmpl.is_valid()) { @@ -217,4 +232,15 @@ std::string SearchRenderer::getHtml() return ss.str(); } +std::string SearchRenderer::getHtml() +{ + return renderTemplate(RESOURCE::templates::search_result_html); +} + +std::string SearchRenderer::getXml() +{ + return renderTemplate(RESOURCE::templates::search_result_xml); +} + + } diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index 772f6a4da..c78cd448e 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -499,7 +499,7 @@ std::unique_ptr InternalServer::handle_request(const RequestContext& r { try { if (! request.is_valid_url()) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } @@ -519,6 +519,14 @@ std::unique_ptr InternalServer::handle_request(const RequestContext& r if (request.get_url() == "/search") return handle_search(request); + if (request.get_url() == "/search/searchdescription.xml") { + return ContentResponse::build( + *this, + RESOURCE::ft_opensearchdescription_xml, + get_default_data(), + "application/opensearchdescription+xml"); + } + if (request.get_url() == "/suggest") return handle_suggest(request); @@ -531,11 +539,11 @@ std::unique_ptr InternalServer::handle_request(const RequestContext& r return handle_content(request); } catch (std::exception& e) { fprintf(stderr, "===== Unhandled error : %s\n", e.what()); - return HTTP500HtmlResponse(*this, request) + return HTTP500Response(*this, request) + e.what(); } catch (...) { fprintf(stderr, "===== Unhandled unknown error\n"); - return HTTP500HtmlResponse(*this, request) + return HTTP500Response(*this, request) + "Unknown error"; } } @@ -628,7 +636,7 @@ std::unique_ptr InternalServer::handle_suggest(const RequestContext& r } if (archive == nullptr) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + noSuchBookErrorMsg(bookName) + TaskbarInfo(bookName); } @@ -701,7 +709,7 @@ std::unique_ptr InternalServer::handle_skin(const RequestContext& requ response->set_cacheable(); return std::move(response); } catch (const ResourceNotFound& e) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } } @@ -732,10 +740,10 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re // Searcher->search will throw a runtime error if there is no valid xapian database to do the search. // (in case of zim file not containing a index) const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css); - HTTPErrorHtmlResponse response(*this, request, MHD_HTTP_NOT_FOUND, - "fulltext-search-unavailable", - "404-page-heading", - cssUrl); + HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND, + "fulltext-search-unavailable", + "404-page-heading", + cssUrl); response += nonParameterizedMessage("no-search-results"); if(bookIds.size() == 1) { auto bookId = *bookIds.begin(); @@ -745,10 +753,11 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re return response; } - auto start = 0; + auto start = 1; try { start = request.get_argument("start"); } catch (const std::exception&) {} + start = max(1, start); auto pageLength = 25; try { @@ -762,13 +771,18 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re } /* Get the results */ - SearchRenderer renderer(search->getResults(start, pageLength), mp_nameMapper, mp_library, start, + SearchRenderer renderer(search->getResults(start-1, pageLength), mp_nameMapper, mp_library, start, search->getEstimatedMatches()); renderer.setSearchPattern(searchInfo.pattern); renderer.setSearchBookQuery(searchInfo.bookFilterQuery); renderer.setProtocolPrefix(m_root + "/"); renderer.setSearchProtocolPrefix(m_root + "/search"); renderer.setPageLength(pageLength); + if (request.get_requested_format() == "xml") { + return ContentResponse::build(*this, renderer.getXml(), "application/rss+xml; charset=utf-8", + /*isHomePage =*/false, + /*raw =*/true); + } auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8"); if(bookIds.size() == 1) { auto bookId = *bookIds.begin(); @@ -777,7 +791,7 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re } return std::move(response); } catch (const Error& e) { - return HTTP400HtmlResponse(*this, request) + return HTTP400Response(*this, request) + invalidUrlMsg + e.message(); } @@ -800,7 +814,7 @@ std::unique_ptr InternalServer::handle_random(const RequestContext& re } if (archive == nullptr) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + noSuchBookErrorMsg(bookName) + TaskbarInfo(bookName); } @@ -809,7 +823,7 @@ std::unique_ptr InternalServer::handle_random(const RequestContext& re auto entry = archive->getRandomEntry(); return build_redirect(bookName, getFinalItem(*archive, entry)); } catch(zim::EntryNotFound& e) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + nonParameterizedMessage("random-article-failure") + TaskbarInfo(bookName, archive.get()); } @@ -823,7 +837,7 @@ std::unique_ptr InternalServer::handle_captured_external(const Request } catch (const std::out_of_range& e) {} if (source.empty()) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } @@ -844,7 +858,7 @@ std::unique_ptr InternalServer::handle_catalog(const RequestContext& r host = request.get_header("Host"); url = request.get_url_part(1); } catch (const std::out_of_range&) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } @@ -853,7 +867,7 @@ std::unique_ptr InternalServer::handle_catalog(const RequestContext& r } if (url != "searchdescription.xml" && url != "root.xml" && url != "search") { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } @@ -950,7 +964,7 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r if (archive == nullptr) { const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true); - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern)) + TaskbarInfo(bookName); @@ -984,7 +998,7 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r printf("Failed to find %s\n", urlStr.c_str()); std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true); - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern)) + TaskbarInfo(bookName, archive.get()); @@ -1004,12 +1018,12 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque bookName = request.get_url_part(1); kind = request.get_url_part(2); } catch (const std::out_of_range& e) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } if (kind != "meta" && kind!= "content") { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg + invalidRawAccessMsg(kind); } @@ -1021,7 +1035,7 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque } catch (const std::out_of_range& e) {} if (archive == nullptr) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg + noSuchBookErrorMsg(bookName); } @@ -1047,7 +1061,7 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque if (m_verbose.load()) { printf("Failed to find %s\n", itemPath.c_str()); } - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg + rawEntryNotFoundMsg(kind, itemPath); } diff --git a/src/server/internalServer_catalog_v2.cpp b/src/server/internalServer_catalog_v2.cpp index a5c9f49c8..79504e5de 100644 --- a/src/server/internalServer_catalog_v2.cpp +++ b/src/server/internalServer_catalog_v2.cpp @@ -43,7 +43,7 @@ std::unique_ptr InternalServer::handle_catalog_v2(const RequestContext try { url = request.get_url_part(2); } catch (const std::out_of_range&) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } @@ -70,7 +70,7 @@ std::unique_ptr InternalServer::handle_catalog_v2(const RequestContext } else if (url == "illustration") { return handle_catalog_v2_illustration(request); } else { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } } @@ -112,7 +112,7 @@ std::unique_ptr InternalServer::handle_catalog_v2_complete_entry(const try { mp_library->getBookById(entryId); } catch (const std::out_of_range&) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } @@ -161,7 +161,7 @@ std::unique_ptr InternalServer::handle_catalog_v2_illustration(const R auto illustration = book.getIllustration(size); return ContentResponse::build(*this, illustration->getData(), illustration->mimeType); } catch(...) { - return HTTP404HtmlResponse(*this, request) + return HTTP404Response(*this, request) + urlNotFoundMsg; } } diff --git a/src/server/request_context.cpp b/src/server/request_context.cpp index 53a5cde21..0b9a1a539 100644 --- a/src/server/request_context.cpp +++ b/src/server/request_context.cpp @@ -202,4 +202,9 @@ std::string RequestContext::get_user_language() const return "en"; } +std::string RequestContext::get_requested_format() const +{ + return get_optional_param("format", "html"); +} + } diff --git a/src/server/request_context.h b/src/server/request_context.h index f0b79b58e..f63f89810 100644 --- a/src/server/request_context.h +++ b/src/server/request_context.h @@ -118,6 +118,7 @@ class RequestContext { bool can_compress() const { return acceptEncodingGzip; } std::string get_user_language() const; + std::string get_requested_format() const; private: // data std::string full_url; diff --git a/src/server/response.cpp b/src/server/response.cpp index 5543499fd..37f12fca4 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -146,17 +146,17 @@ std::unique_ptr ContentResponseBlueprint::generateResponseObjec return r; } -HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server, - const RequestContext& request, - int httpStatusCode, - const std::string& pageTitleMsgId, - const std::string& headingMsgId, - const std::string& cssUrl) +HTTPErrorResponse::HTTPErrorResponse(const InternalServer& server, + const RequestContext& request, + int httpStatusCode, + const std::string& pageTitleMsgId, + const std::string& headingMsgId, + const std::string& cssUrl) : ContentResponseBlueprint(&server, &request, httpStatusCode, - "text/html; charset=utf-8", - RESOURCE::templates::error_html) + 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{ @@ -167,51 +167,51 @@ HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server, }; } -HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server, - const RequestContext& request) - : HTTPErrorHtmlResponse(server, - request, - MHD_HTTP_NOT_FOUND, - "404-page-title", - "404-page-heading") +HTTP404Response::HTTP404Response(const InternalServer& server, + const RequestContext& request) + : HTTPErrorResponse(server, + request, + MHD_HTTP_NOT_FOUND, + "404-page-title", + "404-page-heading") { } -HTTPErrorHtmlResponse& HTTP404HtmlResponse::operator+(UrlNotFoundMsg /*unused*/) +HTTPErrorResponse& HTTP404Response::operator+(UrlNotFoundMsg /*unused*/) { const std::string requestUrl = m_request.get_full_url(); return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}}); } -HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const std::string& msg) +HTTPErrorResponse& HTTPErrorResponse::operator+(const std::string& msg) { m_data["details"].push_back({"p", msg}); return *this; } -HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const ParameterizedMessage& details) +HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details) { return *this + details.getText(m_request.get_user_language()); } -HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+=(const ParameterizedMessage& details) +HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& details) { // operator+() is already a state-modifying operator (akin to operator+=) return *this + details; } -HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server, - const RequestContext& request) - : HTTPErrorHtmlResponse(server, - request, - MHD_HTTP_BAD_REQUEST, - "400-page-title", - "400-page-heading") +HTTP400Response::HTTP400Response(const InternalServer& server, + const RequestContext& request) + : HTTPErrorResponse(server, + request, + MHD_HTTP_BAD_REQUEST, + "400-page-title", + "400-page-heading") { } -HTTPErrorHtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/) +HTTPErrorResponse& HTTP400Response::operator+(InvalidUrlMsg /*unused*/) { std::string requestUrl = m_request.get_full_url(); const auto query = m_request.get_query(); @@ -222,19 +222,19 @@ HTTPErrorHtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/) return *this + msgTmpl.render({"url", requestUrl}); } -HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server, - const RequestContext& request) - : HTTPErrorHtmlResponse(server, - request, - MHD_HTTP_INTERNAL_SERVER_ERROR, - "500-page-title", - "500-page-heading") +HTTP500Response::HTTP500Response(const InternalServer& server, + const RequestContext& request) + : HTTPErrorResponse(server, + request, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "500-page-title", + "500-page-heading") { // operator+() is a state-modifying operator (akin to operator+=) *this + "An internal server error occured. We are sorry about that :/"; } -std::unique_ptr HTTP500HtmlResponse::generateResponseObject() const +std::unique_ptr HTTP500Response::generateResponseObject() const { // We want a 500 response to be a minimalistic one (so that the server doesn't // have to provide additional resources required for its proper rendering) diff --git a/src/server/response.h b/src/server/response.h index 5c14e7786..afdbd336d 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -181,52 +181,52 @@ public: //data std::unique_ptr m_taskbarInfo; }; -struct HTTPErrorHtmlResponse : ContentResponseBlueprint +struct HTTPErrorResponse : ContentResponseBlueprint { - HTTPErrorHtmlResponse(const InternalServer& server, - const RequestContext& request, - int httpStatusCode, - const std::string& pageTitleMsgId, - const std::string& headingMsgId, - const std::string& cssUrl = ""); + HTTPErrorResponse(const InternalServer& server, + const RequestContext& request, + int httpStatusCode, + const std::string& pageTitleMsgId, + const std::string& headingMsgId, + const std::string& cssUrl = ""); using ContentResponseBlueprint::operator+; using ContentResponseBlueprint::operator+=; - HTTPErrorHtmlResponse& operator+(const std::string& msg); - HTTPErrorHtmlResponse& operator+(const ParameterizedMessage& errorDetails); - HTTPErrorHtmlResponse& operator+=(const ParameterizedMessage& errorDetails); + HTTPErrorResponse& operator+(const std::string& msg); + HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails); + HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails); }; class UrlNotFoundMsg {}; extern const UrlNotFoundMsg urlNotFoundMsg; -struct HTTP404HtmlResponse : HTTPErrorHtmlResponse +struct HTTP404Response : HTTPErrorResponse { - HTTP404HtmlResponse(const InternalServer& server, - const RequestContext& request); + HTTP404Response(const InternalServer& server, + const RequestContext& request); - using HTTPErrorHtmlResponse::operator+; - HTTPErrorHtmlResponse& operator+(UrlNotFoundMsg /*unused*/); + using HTTPErrorResponse::operator+; + HTTPErrorResponse& operator+(UrlNotFoundMsg /*unused*/); }; class InvalidUrlMsg {}; extern const InvalidUrlMsg invalidUrlMsg; -struct HTTP400HtmlResponse : HTTPErrorHtmlResponse +struct HTTP400Response : HTTPErrorResponse { - HTTP400HtmlResponse(const InternalServer& server, - const RequestContext& request); + HTTP400Response(const InternalServer& server, + const RequestContext& request); - using HTTPErrorHtmlResponse::operator+; - HTTPErrorHtmlResponse& operator+(InvalidUrlMsg /*unused*/); + using HTTPErrorResponse::operator+; + HTTPErrorResponse& operator+(InvalidUrlMsg /*unused*/); }; -struct HTTP500HtmlResponse : HTTPErrorHtmlResponse +struct HTTP500Response : HTTPErrorResponse { - HTTP500HtmlResponse(const InternalServer& server, - const RequestContext& request); + HTTP500Response(const InternalServer& server, + const RequestContext& request); private: // overrides // generateResponseObject() is overriden in order to produce a minimal diff --git a/static/ft_opensearchdescription.xml b/static/ft_opensearchdescription.xml new file mode 100644 index 000000000..9b86e5a72 --- /dev/null +++ b/static/ft_opensearchdescription.xml @@ -0,0 +1,10 @@ + + + Fulltext articles search + Search for articles in the Library. + + diff --git a/static/resources_list.txt b/static/resources_list.txt index bc1eb1af3..c3b9c0d56 100644 --- a/static/resources_list.txt +++ b/static/resources_list.txt @@ -34,7 +34,9 @@ skin/fonts/Roboto.ttf skin/block_external.js skin/search_results.css templates/search_result.html +templates/search_result.xml templates/error.html +templates/error.xml templates/index.html templates/suggestion.json templates/head_taskbar.html @@ -49,4 +51,5 @@ templates/catalog_v2_categories.xml templates/catalog_v2_languages.xml templates/url_of_search_results_css opensearchdescription.xml +ft_opensearchdescription.xml catalog_v2_searchdescription.xml diff --git a/static/templates/error.xml b/static/templates/error.xml new file mode 100644 index 000000000..9d6eea7dc --- /dev/null +++ b/static/templates/error.xml @@ -0,0 +1,5 @@ + +{{PAGE_TITLE}} +{{#details}} +{{{p}}} +{{/details}} diff --git a/static/templates/search_result.xml b/static/templates/search_result.xml new file mode 100644 index 000000000..d418a0a0d --- /dev/null +++ b/static/templates/search_result.xml @@ -0,0 +1,37 @@ + + + + Search: {{query.pattern}} + {{query.unpaginatedQuery}}&format=xml&start={{results.start}}&pageLength={{pagination.itemsPerPage}} + Search result for {{query.pattern}} + {{results.count}} + {{results.start}} + {{pagination.itemsPerPage}} + + diff --git a/test/meson.build b/test/meson.build index 4ce7d286b..58d4d605d 100644 --- a/test/meson.build +++ b/test/meson.build @@ -15,7 +15,11 @@ tests = [ ] if build_machine.system() != 'windows' - tests += ['server'] + tests += [ + 'server', + 'server_html_search', + 'server_xml_search' + ] endif diff --git a/test/server.cpp b/test/server.cpp index 94db3c944..5aeed662e 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -3,26 +3,9 @@ #include "./httplib.h" #include "gtest/gtest.h" -#include "../include/manager.h" -#include "../include/server.h" -#include "../include/name_mapper.h" -#include "../include/tools.h" +#define SERVER_PORT 8001 +#include "server_testing_tools.h" -using TestContextImpl = std::vector >; -struct TestContext : TestContextImpl { - TestContext(const std::initializer_list& il) - : TestContextImpl(il) - {} -}; - -std::ostream& operator<<(std::ostream& out, const TestContext& ctx) -{ - out << "Test context:\n"; - for ( const auto& kv : ctx ) - out << "\t" << kv.first << ": " << kv.second << "\n"; - out << std::endl; - return out; -} bool is_valid_etag(const std::string& etag) { @@ -38,148 +21,6 @@ T1 concat(T1 a, const T2& b) return a; } -typedef httplib::Headers Headers; - -Headers invariantHeaders(Headers headers) -{ - headers.erase("Date"); - return headers; -} - -std::string replace(std::string s, std::string pattern, std::string replacement) -{ - return std::regex_replace(s, std::regex(pattern), replacement); -} - -// Output generated via mustache templates sometimes contains end-of-line -// whitespace. This complicates representing the expected output of a unit-test -// as C++ raw strings in editors that are configured to delete EOL whitespace. -// A workaround is to put special markers (//EOLWHITESPACEMARKER) at the end -// of such lines in the expected output string and remove them at runtime. -// This is exactly what this function is for. -std::string removeEOLWhitespaceMarkers(const std::string& s) -{ - const std::regex pattern("//EOLWHITESPACEMARKER"); - return std::regex_replace(s, pattern, ""); -} - -class ZimFileServer -{ -public: // types - typedef std::shared_ptr Response; - typedef std::vector FilePathCollection; - -public: // functions - ZimFileServer(int serverPort, std::string libraryFilePath); - ZimFileServer(int serverPort, - bool withTaskbar, - const FilePathCollection& zimpaths, - std::string indexTemplateString = ""); - ~ZimFileServer(); - - Response GET(const char* path, const Headers& headers = Headers()) - { - return client->Get(path, headers); - } - - Response HEAD(const char* path, const Headers& headers = Headers()) - { - return client->Head(path, headers); - } - -private: - void run(int serverPort, std::string indexTemplateString = ""); - -private: // data - kiwix::Library library; - kiwix::Manager manager; - std::unique_ptr nameMapper; - std::unique_ptr server; - std::unique_ptr client; - const bool withTaskbar = true; -}; - -ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath) -: manager(&this->library) -{ - if ( kiwix::isRelativePath(libraryFilePath) ) - libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath); - manager.readFile(libraryFilePath, true, true); - run(serverPort); -} - -ZimFileServer::ZimFileServer(int serverPort, - bool _withTaskbar, - const FilePathCollection& zimpaths, - std::string indexTemplateString) -: manager(&this->library) -, withTaskbar(_withTaskbar) -{ - for ( const auto& zimpath : zimpaths ) { - if (!manager.addBookFromPath(zimpath, zimpath, "", false)) - throw std::runtime_error("Unable to add the ZIM file '" + zimpath + "'"); - } - run(serverPort, indexTemplateString); -} - -void ZimFileServer::run(int serverPort, std::string indexTemplateString) -{ - const std::string address = "127.0.0.1"; - nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false)); - server.reset(new kiwix::Server(&library, nameMapper.get())); - server->setRoot("ROOT"); - server->setAddress(address); - server->setPort(serverPort); - server->setNbThreads(2); - server->setVerbose(false); - server->setTaskbar(withTaskbar, withTaskbar); - server->setMultiZimSearchLimit(3); - if (!indexTemplateString.empty()) { - server->setIndexTemplateString(indexTemplateString); - } - - if ( !server->start() ) - throw std::runtime_error("ZimFileServer failed to start"); - - client.reset(new httplib::Client(address, serverPort)); -} - -ZimFileServer::~ZimFileServer() -{ - server->stop(); -} - -class ServerTest : public ::testing::Test -{ -protected: - std::unique_ptr zfs1_; - - const int PORT = 8001; - const ZimFileServer::FilePathCollection ZIMFILES { - "./test/zimfile.zim", - "./test/example.zim", - "./test/poor.zim", - "./test/corner_cases.zim" - }; - -protected: - void SetUp() override { - zfs1_.reset(new ZimFileServer(PORT, /*withTaskbar=*/true, ZIMFILES)); - } - - void TearDown() override { - zfs1_.reset(); - } -}; - -class TaskbarlessServerTest : public ServerTest -{ -protected: - void SetUp() override { - zfs1_.reset(new ZimFileServer(PORT, /*withTaskbar=*/false, ZIMFILES)); - } -}; - const bool WITH_ETAG = true; const bool NO_ETAG = false; @@ -469,7 +310,7 @@ enum ExpectedResponseDataType // Operator overloading is used as a means of defining a mini-DSL for // defining test data in a concise way (see usage in -// TEST_F(ServerTest, 404WithBodyTesting)) +// TEST_F(ServerTest, Http404HtmlError)) ExpectedResponseData operator==(ExpectedResponseDataType t, std::string s) { switch (t) @@ -704,7 +545,7 @@ std::string TestContentIn400HtmlResponse::pageTitle() const { } // namespace TestingOfHtmlResponses -TEST_F(ServerTest, 404WithBodyTesting) +TEST_F(ServerTest, Http404HtmlError) { using namespace TestingOfHtmlResponses; const std::vector testData{ @@ -894,7 +735,7 @@ TEST_F(ServerTest, 404WithBodyTesting) } } -TEST_F(ServerTest, 400WithBodyTesting) +TEST_F(ServerTest, Http400HtmlError) { using namespace TestingOfHtmlResponses; const std::vector testData{ @@ -970,6 +811,85 @@ TEST_F(ServerTest, 400WithBodyTesting) } } +TEST_F(ServerTest, HttpXmlError) +{ + struct TestData + { + std::string url; + int expectedStatusCode; + std::string expectedXml; + + std::string fullExpectedXml() const + { + return R"()" + expectedXml; + } + + TestContext ctx() const + { + return TestContext{ {"url", url} }; + } + }; + + const std::vector testData{ + { /* url */ "/ROOT/search?format=xml", + /* HTTP status code */ 400, + /* expected response XML */ R"( +Invalid request +The requested URL "/ROOT/search?format=xml" is not a valid request. +Too many books requested (4) where limit is 3 +)" }, + { /* url */ "/ROOT/search?format=xml&content=zimfile", + /* HTTP status code */ 400, + /* expected response XML */ R"( +Invalid request +The requested URL "/ROOT/search?content=zimfile&format=xml" is not a valid request. +No query provided. +)" }, + { /* url */ "/ROOT/search?format=xml&content=non-existing-book&pattern=asdfqwerty", + /* HTTP status code */ 400, + /* expected response XML */ R"( +Invalid request +The requested URL "/ROOT/search?content=non-existing-book&format=xml&pattern=asdfqwerty" is not a valid request. +No such book: non-existing-book +)" }, + { /* url */ "/ROOT/search?format=xml&content=non-existing-book&pattern=a\"