diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index baffc6738..0c45580ad 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -109,6 +109,54 @@ unsigned int getCacheLength(const char* name, unsigned int defaultVal) { } } // unnamed namespace +SearchInfo::SearchInfo(const std::string& pattern) + : pattern(pattern), + geoQuery() +{} + +SearchInfo::SearchInfo(const std::string& pattern, GeoQuery geoQuery) + : pattern(pattern), + geoQuery(geoQuery) +{} + +SearchInfo::SearchInfo(const RequestContext& request) + : pattern(request.get_optional_param("pattern", "")), + geoQuery(), + bookName(request.get_optional_param("content", "")) +{ + /* Retrive geo search */ + try { + auto latitude = request.get_argument("latitude"); + auto longitude = request.get_argument("longitude"); + auto distance = request.get_argument("distance"); + geoQuery = GeoQuery(latitude, longitude, distance); + } catch(const std::out_of_range&) {} + catch(const std::invalid_argument&) {} + + if (!geoQuery && pattern.empty()) { + throw std::invalid_argument("No query provided."); + } +} + +zim::Query SearchInfo::getZimQuery(bool verbose) const { + zim::Query query; + if (verbose) { + std::cout << "Performing query '" << pattern<< "'"; + } + query.setQuery(pattern); + if (geoQuery) { + if (verbose) { + std::cout << " with geo query '" << geoQuery.distance << "&(" << geoQuery.latitude << ";" << geoQuery.longitude << ")'"; + } + query.setGeorange(geoQuery.latitude, geoQuery.longitude, geoQuery.distance); + } + if (verbose) { + std::cout << std::endl; + } + return query; +} + + static IdNameMapper defaultNameMapper; static MHD_Result staticHandlerCallback(void* cls, @@ -499,111 +547,86 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re printf("** running handle_search\n"); } - std::string patternString; - try { - patternString = request.get_argument("pattern"); - } catch (const std::out_of_range&) {} - - /* Retrive geo search */ - bool has_geo_query = false; - float latitude = 0; - float longitude = 0; - float distance = 0; - try { - latitude = request.get_argument("latitude"); - longitude = request.get_argument("longitude"); - distance = request.get_argument("distance"); - has_geo_query = true; - } catch(const std::out_of_range&) {} - catch(const std::invalid_argument&) {} - - std::string bookName, bookId; - std::shared_ptr archive; try { - bookName = request.get_argument("content"); - bookId = mp_nameMapper->getIdForName(bookName); - archive = mp_library->getArchiveById(bookId); - } catch (const std::out_of_range&) {} - - /* Make the search */ - if ( (!archive && !bookName.empty()) - || (patternString.empty() && ! has_geo_query) ) { - auto data = get_default_data(); - 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_code(MHD_HTTP_NOT_FOUND); - return withTaskbarInfo(bookName, archive.get(), std::move(response)); - } - - std::shared_ptr searcher; - if (archive) { - searcher = searcherCache.getOrPut(bookId, [=](){ return std::make_shared(*archive);}); - } else { - for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) { - auto currentArchive = mp_library->getArchiveById(bookId); - if (currentArchive) { - if (! searcher) { - searcher = std::make_shared(*currentArchive); - } else { - searcher->addArchive(*currentArchive); - } + auto searchInfo = SearchInfo(request); + + std::string bookId; + std::shared_ptr archive; + if (!searchInfo.bookName.empty()) { + try { + bookId = mp_nameMapper->getIdForName(searchInfo.bookName); + archive = mp_library->getArchiveById(bookId); + } catch (const std::out_of_range&) { + throw std::invalid_argument("The requested book doesn't exist."); } } - } - auto start = 0; - try { - start = request.get_argument("start"); - } catch (const std::exception&) {} - auto pageLength = 25; - try { - pageLength = request.get_argument("pageLength"); - } catch (const std::exception&) {} - if (pageLength > MAX_SEARCH_LEN) { - pageLength = MAX_SEARCH_LEN; - } - if (pageLength == 0) { - pageLength = 25; - } + /* Make the search */ + // Try to get a search from the searchInfo, else build it + std::shared_ptr search; + try { + search = searchCache.getOrPut(searchInfo, + [=](){ + std::shared_ptr searcher; + if (archive) { + searcher = searcherCache.getOrPut(bookId, [=](){ return std::make_shared(*archive);}); + } else { + for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) { + auto currentArchive = mp_library->getArchiveById(bookId); + if (currentArchive) { + if (! searcher) { + searcher = std::make_shared(*currentArchive); + } else { + searcher->addArchive(*currentArchive); + } + } + } + } + return make_shared(searcher->search(searchInfo.getZimQuery(m_verbose.load()))); + } + ); + } catch(std::runtime_error& e) { + // 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) + auto data = get_default_data(); + data.set("pattern", encodeDiples(searchInfo.pattern)); + auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8"); + response->set_code(MHD_HTTP_NOT_FOUND); + return withTaskbarInfo(searchInfo.bookName, archive.get(), std::move(response)); + } - /* Get the results */ - std::string queryString; - try { - zim::Query query; - if (patternString.empty()) { - // Execute geo-search - if (m_verbose.load()) { - cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl; - } - query.setQuery(""); - queryString = "GEO:" + to_string(latitude) + to_string(longitude) + to_string(distance); - query.setGeorange(latitude, longitude, distance); - } else { - // Execute Ft search - if (m_verbose.load()) { - cout << "Performing query `" << patternString << "'" << endl; - } + auto start = 0; + try { + start = request.get_argument("start"); + } catch (const std::exception&) {} - queryString = "FT:" + removeAccents(patternString); - query.setQuery(queryString); + auto pageLength = 25; + try { + pageLength = request.get_argument("pageLength"); + } catch (const std::exception&) {} + if (pageLength > MAX_SEARCH_LEN) { + pageLength = MAX_SEARCH_LEN; + } + if (pageLength == 0) { + pageLength = 25; } - queryString = bookId + queryString; - - std::shared_ptr search; - search = searchCache.getOrPut(queryString, [=](){ return make_shared(searcher->search(query));}); + /* Get the results */ SearchRenderer renderer(search->getResults(start, pageLength), mp_nameMapper, mp_library, start, search->getEstimatedMatches()); - renderer.setSearchPattern(patternString); - renderer.setSearchContent(bookName); + renderer.setSearchPattern(searchInfo.pattern); + renderer.setSearchContent(searchInfo.bookName); renderer.setProtocolPrefix(m_root + "/"); renderer.setSearchProtocolPrefix(m_root + "/search?"); renderer.setPageLength(pageLength); auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8"); - return withTaskbarInfo(bookName, archive.get(), std::move(response)); + return withTaskbarInfo(searchInfo.bookName, archive.get(), std::move(response)); + } catch (const std::invalid_argument& e) { + return HTTP400HtmlResponse(*this, request) + + invalidUrlMsg + + std::string(e.what()); } catch (const std::exception& e) { std::cerr << e.what() << std::endl; return Response::build_500(*this, e.what()); diff --git a/src/server/internalServer.h b/src/server/internalServer.h index 6ccde9f8f..eb8dcc5cf 100644 --- a/src/server/internalServer.h +++ b/src/server/internalServer.h @@ -43,9 +43,53 @@ extern "C" { namespace kiwix { +struct GeoQuery { + GeoQuery() + : GeoQuery(0, 0, -1) + {} + + GeoQuery(float latitude, float longitude, float distance) + : latitude(latitude), longitude(longitude), distance(distance) + {} + float latitude; + float longitude; + float distance; + + explicit operator bool() const { + return distance >= 0; + } + + friend bool operator<(const GeoQuery& l, const GeoQuery& r) + { + return std::tie(l.latitude, l.longitude, l.distance) + < std::tie(r.latitude, r.longitude, r.distance); // keep the same order + } +}; + +class SearchInfo { + public: + SearchInfo(const std::string& pattern); + SearchInfo(const std::string& pattern, GeoQuery geoQuery); + SearchInfo(const RequestContext& request); + + zim::Query getZimQuery(bool verbose) const; + + friend bool operator<(const SearchInfo& l, const SearchInfo& r) + { + return std::tie(l.bookName, l.pattern, l.geoQuery) + < std::tie(r.bookName, r.pattern, r.geoQuery); // keep the same order + } + + public: //data + std::string pattern; + GeoQuery geoQuery; + std::string bookName; +}; + + typedef kainjow::mustache::data MustacheData; typedef ConcurrentCache> SearcherCache; -typedef ConcurrentCache> SearchCache; +typedef ConcurrentCache> SearchCache; typedef ConcurrentCache> SuggestionSearcherCache; class Entry; @@ -77,7 +121,7 @@ class InternalServer { bool start(); void stop(); std::string getAddress() { return m_addr; } - int getPort() { return m_port; } + int getPort() { return m_port; } private: // functions std::unique_ptr handle_request(const RequestContext& request); diff --git a/src/server/response.cpp b/src/server/response.cpp index 50a66c344..4b82659c8 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -110,6 +110,7 @@ std::unique_ptr Response::build_404(const InternalServer& serve } extern const UrlNotFoundMsg urlNotFoundMsg; +extern const InvalidUrlMsg invalidUrlMsg; std::unique_ptr ContentResponseBlueprint::generateResponseObject() const { @@ -145,6 +146,36 @@ HTTP404HtmlResponse& HTTP404HtmlResponse::operator+(const std::string& msg) return *this; } +HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server, + const RequestContext& request) + : ContentResponseBlueprint(&server, + &request, + MHD_HTTP_BAD_REQUEST, + "text/html", + RESOURCE::templates::_400_html) +{ + kainjow::mustache::list emptyList; + this->m_data = kainjow::mustache::object{{"details", emptyList}}; +} + +HTTP400HtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/) +{ + std::string requestUrl = m_request.get_full_url(); + const auto query = m_request.get_query(); + if (!query.empty()) { + requestUrl += "?" + encodeDiples(query); + } + kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{{url}}}" is not a valid request.)"); + return *this + msgTmpl.render({"url", requestUrl}); +} + +HTTP400HtmlResponse& HTTP400HtmlResponse::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)); diff --git a/src/server/response.h b/src/server/response.h index b10524bc8..d46898923 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -207,6 +207,20 @@ struct HTTP404HtmlResponse : ContentResponseBlueprint HTTP404HtmlResponse& operator+(const std::string& errorDetails); }; +class InvalidUrlMsg {}; + +extern const InvalidUrlMsg invalidUrlMsg; + +struct HTTP400HtmlResponse : ContentResponseBlueprint +{ + HTTP400HtmlResponse(const InternalServer& server, + const RequestContext& request); + + using ContentResponseBlueprint::operator+; + HTTP400HtmlResponse& operator+(InvalidUrlMsg /*unused*/); + HTTP400HtmlResponse& 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/static/resources_list.txt b/static/resources_list.txt index 5587111c0..ab583d7a6 100644 --- a/static/resources_list.txt +++ b/static/resources_list.txt @@ -35,6 +35,7 @@ skin/block_external.js skin/search_results.css templates/search_result.html templates/no_search_result.html +templates/400.html templates/404.html templates/500.html templates/index.html diff --git a/static/templates/400.html b/static/templates/400.html new file mode 100644 index 000000000..5f4ecf42f --- /dev/null +++ b/static/templates/400.html @@ -0,0 +1,15 @@ + + + + + Invalid request + + +

Invalid request

+{{#details}} +

+ {{{p}}} +

+{{/details}} + + diff --git a/static/templates/search_result.html b/static/templates/search_result.html index b52c9c3d3..c0ec3b02e 100644 --- a/static/templates/search_result.html +++ b/static/templates/search_result.html @@ -107,11 +107,11 @@ of {{count}} for - {{searchPattern}} + "{{{searchPattern}}}" {{/hasResults}} {{^hasResults}} - No results were found for {{searchPattern}} + No results were found for "{{{searchPattern}}}" {{/hasResults}} diff --git a/test/server.cpp b/test/server.cpp index cf57826e6..59ed29bb4 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -287,6 +287,22 @@ TEST_F(ServerTest, UncompressibleContentIsNotCompressed) } } +const char* urls400[] = { + "/ROOT/search", + "/ROOT/search?content=zimfile", + "/ROOT/search?content=non-existing-book&pattern=asdfqwerty", + "/ROOT/search?content=non-existing-book&pattern=asdGET(url)->status) << "url: " << url; + } +} + const char* urls404[] = { "/", "/zimfile", @@ -302,8 +318,6 @@ const char* urls404[] = { "/ROOT/meta?content=non-existent-book&name=title", "/ROOT/random", "/ROOT/random?content=non-existent-book", - "/ROOT/search", - "/ROOT/search?content=non-existing-book&pattern=asdfqwerty", "/ROOT/suggest", "/ROOT/suggest?content=non-existent-book&term=abcd", "/ROOT/catch/external", @@ -319,8 +333,9 @@ const char* urls404[] = { TEST_F(ServerTest, 404) { - for ( const char* url : urls404 ) + for ( const char* url : urls404 ) { EXPECT_EQ(404, zfs1_->GET(url)->status) << "url: " << url; + } } namespace TestingOfHtmlResponses @@ -388,13 +403,14 @@ class TestContentIn404HtmlResponse : public ExpectedResponseData : ExpectedResponseData(erd) , url(url) {} + virtual ~TestContentIn404HtmlResponse() = default; const std::string url; std::string expectedResponse() const; private: - std::string pageTitle() const; + virtual std::string pageTitle() const; std::string pageCssLink() const; std::string hiddenBookNameInput() const; std::string searchPatternInput() const; @@ -521,6 +537,25 @@ std::string TestContentIn404HtmlResponse::taskbarLinks() const + R"(">)"; } +class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse +{ +public: + TestContentIn400HtmlResponse(const std::string& url, + const ExpectedResponseData& erd) + : TestContentIn404HtmlResponse(url, erd) + {} + +private: + std::string pageTitle() const; +}; + +std::string TestContentIn400HtmlResponse::pageTitle() const { + return expectedPageTitle.empty() + ? "Invalid request" + : expectedPageTitle; +} + + } // namespace TestingOfHtmlResponses TEST_F(ServerTest, 404WithBodyTesting) @@ -650,28 +685,70 @@ TEST_F(ServerTest, 404WithBodyTesting) Cannot find content entry invalid-article

)" }, + }; + + for ( const auto& t : testData ) { + const TestContext ctx{ {"url", t.url} }; + const auto r = zfs1_->GET(t.url.c_str()); + EXPECT_EQ(r->status, 404) << ctx; + EXPECT_EQ(r->body, t.expectedResponse()) << ctx; + } +} +TEST_F(ServerTest, 400WithBodyTesting) +{ + using namespace TestingOfHtmlResponses; + const std::vector testData{ + { /* url */ "/ROOT/search", + expected_body== R"( +

Invalid request

+

+ The requested URL "/ROOT/search" is not a valid request. +

+

+ No query provided. +

+)" }, { /* url */ "/ROOT/search?content=zimfile", - expected_page_title=="Fulltext search unavailable" && - expected_css_url=="/ROOT/skin/search_results.css" && - book_name=="zimfile" && - book_title=="Ray Charles" && expected_body==R"( -
Not found
+

Invalid request

+

+ The requested URL "/ROOT/search?content=zimfile" is not a valid request. +

- There is no article with the title "" - and the fulltext search engine is not available for this content. + No query provided.

)" }, - - { /* url */ "/ROOT/search?content=non-existent-book&pattern=asdfqwerty", - expected_page_title=="Fulltext search unavailable" && - expected_css_url=="/ROOT/skin/search_results.css" && + { /* url */ "/ROOT/search?content=non-existing-book&pattern=asdfqwerty", + expected_body==R"( +

Invalid request

+

+ The requested URL "/ROOT/search?content=non-existing-book&pattern=asdfqwerty" is not a valid request. +

+

+ The requested book doesn't exist. +

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