diff --git a/src/opds_dumper.cpp b/src/opds_dumper.cpp index af642246..91bfb7f4 100644 --- a/src/opds_dumper.cpp +++ b/src/opds_dumper.cpp @@ -73,9 +73,6 @@ IllustrationInfo getBookIllustrationInfo(const Book& book) kainjow::mustache::object getSingleBookData(const Book& book) { const auto bookDate = book.getDate() + "T00:00:00Z"; - const MustacheData bookUrl = book.getUrl().empty() - ? MustacheData(false) - : MustacheData(book.getUrl()); return kainjow::mustache::object{ {"id", book.getId()}, {"name", book.getName()}, @@ -92,7 +89,7 @@ kainjow::mustache::object getSingleBookData(const Book& book) {"media_count", to_string(book.getMediaCount())}, {"author_name", book.getCreator()}, {"publisher_name", book.getPublisher()}, - {"url", bookUrl}, + {"url", onlyAsNonEmptyMustacheValue(book.getUrl())}, {"size", to_string(book.getSize())}, {"icons", getBookIllustrationInfo(book)}, }; @@ -194,7 +191,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector& bookIds, const s {"date", gen_date_str()}, {"root", rootLocation}, {"feed_id", gen_uuid(libraryId + "/catalog/search?"+query)}, - {"filter", query.empty() ? MustacheData(false) : MustacheData(query)}, + {"filter", onlyAsNonEmptyMustacheValue(query)}, {"totalResults", to_string(m_totalResults)}, {"startIndex", to_string(m_startIndex)}, {"itemsPerPage", to_string(m_count)}, @@ -214,7 +211,7 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector& bookIds, const {"date", gen_date_str()}, {"endpoint_root", endpointRoot}, {"feed_id", gen_uuid(libraryId + endpoint + "?" + query)}, - {"filter", query.empty() ? MustacheData(false) : MustacheData(query)}, + {"filter", onlyAsNonEmptyMustacheValue(query)}, {"query", query.empty() ? "" : "?" + urlEncode(query)}, {"totalResults", to_string(m_totalResults)}, {"startIndex", to_string(m_startIndex)}, diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index eb43286b..baffc673 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -278,8 +278,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection, std::unique_ptr InternalServer::handle_request(const RequestContext& request) { try { - if (! request.is_valid_url()) - return Response::build_404(*this, request.get_full_url(), "", ""); + if (! request.is_valid_url()) { + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; + } const ETag etag = get_matching_if_none_match_etag(request); if ( etag ) @@ -387,6 +389,15 @@ SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Arch return suggestions; } +namespace +{ + +std::string noSuchBookErrorMsg(const std::string& bookName) +{ + return "No such book: " + bookName; +} + +} // unnamed namespace std::unique_ptr InternalServer::handle_suggest(const RequestContext& request) { @@ -405,8 +416,9 @@ std::unique_ptr InternalServer::handle_suggest(const RequestContext& r } if (archive == nullptr) { - const std::string error_details = "No such book: " + bookName; - return Response::build_404(*this, "", bookName, "", error_details); + return HTTP404HtmlResponse(*this, request) + + noSuchBookErrorMsg(bookName) + + TaskbarInfo(bookName); } const auto queryString = request.get_optional_param("term", std::string()); @@ -476,7 +488,8 @@ std::unique_ptr InternalServer::handle_skin(const RequestContext& requ response->set_cacheable(); return std::move(response); } catch (const ResourceNotFound& e) { - return Response::build_404(*this, request.get_full_url(), "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } } @@ -519,9 +532,8 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re data.set("pattern", encodeDiples(patternString)); data.set("root", m_root); auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8"); - response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : ""); response->set_code(MHD_HTTP_NOT_FOUND); - return std::move(response); + return withTaskbarInfo(bookName, archive.get(), std::move(response)); } std::shared_ptr searcher; @@ -591,9 +603,7 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re renderer.setSearchProtocolPrefix(m_root + "/search?"); renderer.setPageLength(pageLength); auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8"); - response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : ""); - - return std::move(response); + return withTaskbarInfo(bookName, archive.get(), std::move(response)); } catch (const std::exception& e) { std::cerr << e.what() << std::endl; return Response::build_500(*this, e.what()); @@ -617,8 +627,9 @@ std::unique_ptr InternalServer::handle_random(const RequestContext& re } if (archive == nullptr) { - const std::string error_details = "No such book: " + bookName; - return Response::build_404(*this, "", bookName, "", error_details); + return HTTP404HtmlResponse(*this, request) + + noSuchBookErrorMsg(bookName) + + TaskbarInfo(bookName); } try { @@ -626,7 +637,8 @@ std::unique_ptr InternalServer::handle_random(const RequestContext& re return build_redirect(bookName, getFinalItem(*archive, entry)); } catch(zim::EntryNotFound& e) { const std::string error_details = "Oops! Failed to pick a random article :("; - return Response::build_404(*this, "", bookName, getArchiveTitle(*archive), error_details); + auto response = Response::build_404(*this, "", error_details); + return withTaskbarInfo(bookName, archive.get(), std::move(response)); } } @@ -637,8 +649,10 @@ std::unique_ptr InternalServer::handle_captured_external(const Request source = kiwix::urlDecode(request.get_argument("source")); } catch (const std::out_of_range& e) {} - if (source.empty()) - return Response::build_404(*this, request.get_full_url(), "", ""); + if (source.empty()) { + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; + } auto data = get_default_data(); data.set("source", source); @@ -657,7 +671,8 @@ 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 Response::build_404(*this, request.get_full_url(), "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } if (url == "v2") { @@ -665,7 +680,8 @@ std::unique_ptr InternalServer::handle_catalog(const RequestContext& r } if (url != "searchdescription.xml" && url != "root.xml" && url != "search") { - return Response::build_404(*this, request.get_full_url(), "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } if (url == "searchdescription.xml") { @@ -802,7 +818,8 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true); // Make a full search on the entire library. const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern)); - return Response::build_404(*this, request.get_full_url(), bookName, "", details); + auto response = Response::build_404(*this, request.get_full_url(), details); + return withTaskbarInfo(bookName, nullptr, std::move(response)); } auto urlStr = request.get_url().substr(bookName.size()+1); @@ -819,7 +836,7 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r } auto response = ItemResponse::build(*this, request, entry.getItem()); try { - dynamic_cast(*response).set_taskbar(bookName, getArchiveTitle(*archive)); + dynamic_cast(*response).set_taskbar(bookName, archive.get()); } catch (std::bad_cast& e) {} if (m_verbose.load()) { @@ -835,7 +852,8 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true); // Make a search on this specific book only. const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern)); - return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), details); + auto response = Response::build_404(*this, request.get_full_url(), details); + return withTaskbarInfo(bookName, archive.get(), std::move(response)); } } @@ -852,12 +870,13 @@ 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 Response::build_404(*this, request.get_full_url(), bookName, "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } if (kind != "meta" && kind!= "content") { const std::string error_details = kind + " is not a valid request for raw content."; - return Response::build_404(*this, request.get_full_url(), bookName, "", error_details); + return Response::build_404(*this, request.get_full_url(), error_details); } std::shared_ptr archive; @@ -867,8 +886,9 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque } catch (const std::out_of_range& e) {} if (archive == nullptr) { - const std::string error_details = "No such book: " + bookName; - return Response::build_404(*this, request.get_full_url(), bookName, "", error_details); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg + + noSuchBookErrorMsg(bookName); } // Remove the beggining of the path: @@ -893,7 +913,7 @@ std::unique_ptr InternalServer::handle_raw(const RequestContext& reque printf("Failed to find %s\n", itemPath.c_str()); } const std::string error_details = "Cannot find " + kind + " entry " + itemPath; - return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), error_details); + return Response::build_404(*this, request.get_full_url(), error_details); } } diff --git a/src/server/internalServer_catalog_v2.cpp b/src/server/internalServer_catalog_v2.cpp index 3a578cb0..a5c9f49c 100644 --- a/src/server/internalServer_catalog_v2.cpp +++ b/src/server/internalServer_catalog_v2.cpp @@ -43,7 +43,8 @@ std::unique_ptr InternalServer::handle_catalog_v2(const RequestContext try { url = request.get_url_part(2); } catch (const std::out_of_range&) { - return Response::build_404(*this, request.get_full_url(), "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } if (url == "root.xml") { @@ -69,7 +70,8 @@ std::unique_ptr InternalServer::handle_catalog_v2(const RequestContext } else if (url == "illustration") { return handle_catalog_v2_illustration(request); } else { - return Response::build_404(*this, request.get_full_url(), "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } } @@ -110,7 +112,8 @@ std::unique_ptr InternalServer::handle_catalog_v2_complete_entry(const try { mp_library->getBookById(entryId); } catch (const std::out_of_range&) { - return Response::build_404(*this, request.get_full_url(), "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } OPDSDumper opdsDumper(mp_library); @@ -158,7 +161,8 @@ 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 Response::build_404(*this, request.get_full_url(), "", ""); + return HTTP404HtmlResponse(*this, request) + + urlNotFoundMsg; } } diff --git a/src/server/response.cpp b/src/server/response.cpp index 398c5ee8..50a66c34 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -25,6 +25,7 @@ #include "tools/regexTools.h" #include "tools/stringTools.h" #include "tools/otherTools.h" +#include "tools/archiveTools.h" #include "string.h" #include @@ -83,19 +84,71 @@ std::unique_ptr Response::build_304(const InternalServer& server, cons return response; } -std::unique_ptr Response::build_404(const InternalServer& server, const std::string& url, const std::string& bookName, const std::string& bookTitle, const std::string& details) +kainjow::mustache::data make404ResponseData(const std::string& url, const std::string& details) { - MustacheData results; + kainjow::mustache::list pList; if ( !url.empty() ) { - results.set("url", url); + kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{url}}" was not found on this server.)"); + const auto urlNotFoundMsg = msgTmpl.render({"url", url}); + pList.push_back({"p", urlNotFoundMsg}); } - results.set("details", details); + pList.push_back({"p", details}); + return {"details", pList}; +} - auto response = ContentResponse::build(server, RESOURCE::templates::_404_html, results, "text/html"); +std::unique_ptr Response::build_404(const InternalServer& server, const std::string& url, const std::string& details) +{ + return build_404(server, make404ResponseData(url, details)); +} + +std::unique_ptr Response::build_404(const InternalServer& server, const kainjow::mustache::data& data) +{ + auto response = ContentResponse::build(server, RESOURCE::templates::_404_html, data, "text/html"); response->set_code(MHD_HTTP_NOT_FOUND); - response->set_taskbar(bookName, bookTitle); - return std::move(response); + return response; +} + +extern const UrlNotFoundMsg urlNotFoundMsg; + +std::unique_ptr ContentResponseBlueprint::generateResponseObject() const +{ + auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType); + r->set_code(m_httpStatusCode); + return m_taskbarInfo + ? withTaskbarInfo(m_taskbarInfo->bookName, m_taskbarInfo->archive, std::move(r)) + : std::move(r); +} + +HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server, + const RequestContext& request) + : ContentResponseBlueprint(&server, + &request, + MHD_HTTP_NOT_FOUND, + "text/html", + RESOURCE::templates::_404_html) +{ + kainjow::mustache::list emptyList; + this->m_data = kainjow::mustache::object{{"details", emptyList}}; +} + +HTTP404HtmlResponse& HTTP404HtmlResponse::operator+(UrlNotFoundMsg /*unused*/) +{ + const std::string requestUrl = m_request.get_full_url(); + kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{url}}" was not found on this server.)"); + return *this + msgTmpl.render({"url", requestUrl}); +} + +HTTP404HtmlResponse& HTTP404HtmlResponse::operator+(const std::string& msg) +{ + m_data["details"].push_back({"p", msg}); + return *this; +} + +ContentResponseBlueprint& ContentResponseBlueprint::operator+(const TaskbarInfo& taskbarInfo) +{ + this->m_taskbarInfo.reset(new TaskbarInfo(taskbarInfo)); + return *this; } std::unique_ptr Response::build_416(const InternalServer& server, size_t resourceLength) @@ -332,10 +385,10 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect return ret; } -void ContentResponse::set_taskbar(const std::string& bookName, const std::string& bookTitle) +void ContentResponse::set_taskbar(const std::string& bookName, const zim::Archive* archive) { m_bookName = bookName; - m_bookTitle = bookTitle; + m_bookTitle = archive ? getArchiveTitle(*archive) : ""; } @@ -383,6 +436,15 @@ std::unique_ptr ContentResponse::build( return ContentResponse::build(server, content, mimetype, isHomePage); } +std::unique_ptr withTaskbarInfo( + const std::string& bookName, + const zim::Archive* archive, + std::unique_ptr r) +{ + r->set_taskbar(bookName, archive); + return r; +} + ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) : Response(verbose), m_item(item), diff --git a/src/server/response.h b/src/server/response.h index 1a74aaf0..b10524bc 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -33,12 +33,16 @@ extern "C" { #include "microhttpd_wrapper.h" } +namespace zim { +class Archive; +} // namespace zim + namespace kiwix { class InternalServer; class RequestContext; -class EntryResponse; +class ContentResponse; class Response { public: @@ -47,7 +51,8 @@ class Response { static std::unique_ptr build(const InternalServer& server); static std::unique_ptr build_304(const InternalServer& server, const ETag& etag); - static std::unique_ptr build_404(const InternalServer& server, const std::string& url, const std::string& bookName, const std::string& bookTitle, const std::string& details=""); + static std::unique_ptr build_404(const InternalServer& server, const kainjow::mustache::data& data); + static std::unique_ptr build_404(const InternalServer& server, const std::string& url, const std::string& details=""); static std::unique_ptr build_416(const InternalServer& server, size_t resourceLength); static std::unique_ptr build_500(const InternalServer& server, const std::string& msg); static std::unique_ptr build_redirect(const InternalServer& server, const std::string& redirectUrl); @@ -100,7 +105,7 @@ class ContentResponse : public Response { const std::string& mimetype, bool isHomePage = false); - void set_taskbar(const std::string& bookName, const std::string& bookTitle); + void set_taskbar(const std::string& bookName, const zim::Archive* archive); private: MHD_Response* create_mhd_response(const RequestContext& request); @@ -124,6 +129,84 @@ class ContentResponse : public Response { std::string m_bookTitle; }; +struct TaskbarInfo +{ + const std::string bookName; + const zim::Archive* const archive; + + TaskbarInfo(const std::string& bookName, const zim::Archive* a = nullptr) + : bookName(bookName) + , archive(a) + {} +}; + +std::unique_ptr withTaskbarInfo(const std::string& bookName, + const zim::Archive* archive, + std::unique_ptr r); + +class ContentResponseBlueprint +{ +public: // functions + ContentResponseBlueprint(const InternalServer* server, + const RequestContext* request, + int httpStatusCode, + const std::string& mimeType, + const std::string& templateStr) + : m_server(*server) + , m_request(*request) + , m_httpStatusCode(httpStatusCode) + , m_mimeType(mimeType) + , m_template(templateStr) + {} + + virtual ~ContentResponseBlueprint() = default; + + ContentResponseBlueprint& operator+(kainjow::mustache::data&& data) + { + this->m_data = std::move(data); + return *this; + } + + operator std::unique_ptr() const + { + return generateResponseObject(); + } + + operator std::unique_ptr() const + { + return operator std::unique_ptr(); + } + + + ContentResponseBlueprint& operator+(const TaskbarInfo& taskbarInfo); + +protected: // functions + virtual std::unique_ptr generateResponseObject() const; + +public: //data + const InternalServer& m_server; + const RequestContext& m_request; + const int m_httpStatusCode; + const std::string m_mimeType; + const std::string m_template; + kainjow::mustache::data m_data; + std::unique_ptr m_taskbarInfo; +}; + +class UrlNotFoundMsg {}; + +extern const UrlNotFoundMsg urlNotFoundMsg; + +struct HTTP404HtmlResponse : ContentResponseBlueprint +{ + HTTP404HtmlResponse(const InternalServer& server, + const RequestContext& request); + + using ContentResponseBlueprint::operator+; + HTTP404HtmlResponse& operator+(UrlNotFoundMsg /*unused*/); + HTTP404HtmlResponse& operator+(const std::string& errorDetails); +}; + class ItemResponse : public Response { public: ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange); diff --git a/src/tools/otherTools.cpp b/src/tools/otherTools.cpp index 1afcc7d7..ab5bf587 100644 --- a/src/tools/otherTools.cpp +++ b/src/tools/otherTools.cpp @@ -370,6 +370,13 @@ std::string kiwix::gen_uuid(const std::string& s) return kiwix::to_string(zim::Uuid::generate(s)); } +kainjow::mustache::data kiwix::onlyAsNonEmptyMustacheValue(const std::string& s) +{ + return s.empty() + ? kainjow::mustache::data(false) + : kainjow::mustache::data(s); +} + std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data) { kainjow::mustache::mustache tmpl(template_str); diff --git a/src/tools/otherTools.h b/src/tools/otherTools.h index 219332cd..b1297798 100644 --- a/src/tools/otherTools.h +++ b/src/tools/otherTools.h @@ -48,6 +48,10 @@ namespace kiwix std::string gen_date_str(); std::string gen_uuid(const std::string& s); + // if s is empty then returns kainjow::mustache::data(false) + // otherwise kainjow::mustache::data(value) + kainjow::mustache::data onlyAsNonEmptyMustacheValue(const std::string& s); + std::string render_template(const std::string& template_str, kainjow::mustache::data data); } diff --git a/static/templates/404.html b/static/templates/404.html index 2f6cf2ac..795e8c9e 100644 --- a/static/templates/404.html +++ b/static/templates/404.html @@ -6,15 +6,10 @@

Not Found

- {{#url}} +{{#details}}

- The requested URL "{{url}}" was not found on this server. + {{{p}}}

- {{/url}} - {{#details}} -

- {{{details}}} -

- {{/details}} +{{/details}} diff --git a/test/server.cpp b/test/server.cpp index 4eb22e06..cf57826e 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -530,7 +530,6 @@ TEST_F(ServerTest, 404WithBodyTesting) { /* url */ "/ROOT/random?content=non-existent-book", expected_body==R"(

Not Found

- //EOLWHITESPACEMARKER

No such book: non-existent-book

@@ -539,7 +538,6 @@ TEST_F(ServerTest, 404WithBodyTesting) { /* url */ "/ROOT/suggest?content=no-such-book&term=whatever", expected_body==R"(

Not Found

- //EOLWHITESPACEMARKER

No such book: no-such-book

@@ -551,9 +549,6 @@ TEST_F(ServerTest, 404WithBodyTesting)

The requested URL "/ROOT/catalog/" was not found on this server.

-

- //EOLWHITESPACEMARKER -

)" }, { /* url */ "/ROOT/catalog/invalid_endpoint", @@ -562,9 +557,6 @@ TEST_F(ServerTest, 404WithBodyTesting)

The requested URL "/ROOT/catalog/invalid_endpoint" was not found on this server.

-

- //EOLWHITESPACEMARKER -

)" }, { /* url */ "/ROOT/invalid-book/whatever", @@ -638,8 +630,6 @@ TEST_F(ServerTest, 404WithBodyTesting) )" }, { /* url */ "/ROOT/raw/zimfile/meta/invalid-metadata", - book_name=="zimfile" && - book_title=="Ray Charles" && expected_body==R"(

Not Found

@@ -651,8 +641,6 @@ TEST_F(ServerTest, 404WithBodyTesting) )" }, { /* url */ "/ROOT/raw/zimfile/content/invalid-article", - book_name=="zimfile" && - book_title=="Ray Charles" && expected_body==R"(

Not Found