Merge pull request #731 from kiwix/opensearch

Render xml result - opensearch
This commit is contained in:
Veloman Yunkan 2022-06-04 00:51:56 +04:00 committed by GitHub
commit 56167dc23e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2790 additions and 1547 deletions

View File

@ -102,11 +102,19 @@ class SearchRenderer
this->pageLength = pageLength; this->pageLength = pageLength;
} }
std::string renderTemplate(const std::string& tmpl_str);
/** /**
* Generate the html page with the resutls of the search. * Generate the html page with the resutls of the search.
*/ */
std::string getHtml(); std::string getHtml();
/**
* Generate the xml page with the resutls of the search.
*/
std::string getXml();
protected: protected:
std::string beautifyInteger(const unsigned int number); std::string beautifyInteger(const unsigned int number);
zim::SearchResultSet m_srs; zim::SearchResultSet m_srs;

View File

@ -87,6 +87,16 @@ void SearchRenderer::setSearchProtocolPrefix(const std::string& prefix)
this->searchProtocolPrefix = 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 kainjow::mustache::data buildQueryData
( (
const std::string& searchProtocolPrefix, const std::string& searchProtocolPrefix,
@ -99,6 +109,10 @@ kainjow::mustache::data buildQueryData
ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern, true); ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern, true);
ss << "&" << bookQuery; ss << "&" << bookQuery;
query.set("unpaginatedQuery", ss.str()); query.set("unpaginatedQuery", ss.str());
auto lang = extractValueFromQuery(bookQuery, "books.filter.lang");
if(!lang.empty()) {
query.set("lang", lang);
}
return query; return query;
} }
@ -162,7 +176,7 @@ kainjow::mustache::data buildPagination(
return pagination; return pagination;
} }
std::string SearchRenderer::getHtml() std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
{ {
// Build the results list // Build the results list
kainjow::mustache::data items{kainjow::mustache::data::type::list}; kainjow::mustache::data items{kainjow::mustache::data::type::list};
@ -185,8 +199,8 @@ std::string SearchRenderer::getHtml()
results.set("items", items); results.set("items", items);
results.set("count", kiwix::beautifyInteger(estimatedResultCount)); results.set("count", kiwix::beautifyInteger(estimatedResultCount));
results.set("hasResults", estimatedResultCount != 0); results.set("hasResults", estimatedResultCount != 0);
results.set("start", kiwix::beautifyInteger(resultStart+1)); results.set("start", kiwix::beautifyInteger(resultStart));
results.set("end", kiwix::beautifyInteger(min(resultStart+pageLength, estimatedResultCount))); results.set("end", kiwix::beautifyInteger(min(resultStart+pageLength-1, estimatedResultCount)));
// pagination // pagination
auto pagination = buildPagination( auto pagination = buildPagination(
@ -201,14 +215,15 @@ std::string SearchRenderer::getHtml()
searchBookQuery searchBookQuery
); );
std::string template_str = RESOURCE::templates::search_result_html;
kainjow::mustache::mustache tmpl(template_str);
kainjow::mustache::data allData; kainjow::mustache::data allData;
allData.set("protocolPrefix", protocolPrefix);
allData.set("results", results); allData.set("results", results);
allData.set("pagination", pagination); allData.set("pagination", pagination);
allData.set("query", query); allData.set("query", query);
kainjow::mustache::mustache tmpl(tmpl_str);
std::stringstream ss; std::stringstream ss;
tmpl.render(allData, [&ss](const std::string& str) { ss << str; }); tmpl.render(allData, [&ss](const std::string& str) { ss << str; });
if (!tmpl.is_valid()) { if (!tmpl.is_valid()) {
@ -217,4 +232,15 @@ std::string SearchRenderer::getHtml()
return ss.str(); 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);
}
} }

View File

@ -499,7 +499,7 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
{ {
try { try {
if (! request.is_valid_url()) { if (! request.is_valid_url()) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
@ -519,6 +519,14 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
if (request.get_url() == "/search") if (request.get_url() == "/search")
return handle_search(request); 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") if (request.get_url() == "/suggest")
return handle_suggest(request); return handle_suggest(request);
@ -531,11 +539,11 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
return handle_content(request); return handle_content(request);
} catch (std::exception& e) { } catch (std::exception& e) {
fprintf(stderr, "===== Unhandled error : %s\n", e.what()); fprintf(stderr, "===== Unhandled error : %s\n", e.what());
return HTTP500HtmlResponse(*this, request) return HTTP500Response(*this, request)
+ e.what(); + e.what();
} catch (...) { } catch (...) {
fprintf(stderr, "===== Unhandled unknown error\n"); fprintf(stderr, "===== Unhandled unknown error\n");
return HTTP500HtmlResponse(*this, request) return HTTP500Response(*this, request)
+ "Unknown error"; + "Unknown error";
} }
} }
@ -628,7 +636,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
} }
if (archive == nullptr) { if (archive == nullptr) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ noSuchBookErrorMsg(bookName) + noSuchBookErrorMsg(bookName)
+ TaskbarInfo(bookName); + TaskbarInfo(bookName);
} }
@ -701,7 +709,7 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
response->set_cacheable(); response->set_cacheable();
return std::move(response); return std::move(response);
} catch (const ResourceNotFound& e) { } catch (const ResourceNotFound& e) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
} }
@ -732,7 +740,7 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
// Searcher->search will throw a runtime error if there is no valid xapian database to do the search. // 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) // (in case of zim file not containing a index)
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css); const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css);
HTTPErrorHtmlResponse response(*this, request, MHD_HTTP_NOT_FOUND, HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND,
"fulltext-search-unavailable", "fulltext-search-unavailable",
"404-page-heading", "404-page-heading",
cssUrl); cssUrl);
@ -745,10 +753,11 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
return response; return response;
} }
auto start = 0; auto start = 1;
try { try {
start = request.get_argument<unsigned int>("start"); start = request.get_argument<unsigned int>("start");
} catch (const std::exception&) {} } catch (const std::exception&) {}
start = max(1, start);
auto pageLength = 25; auto pageLength = 25;
try { try {
@ -762,13 +771,18 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
} }
/* Get the results */ /* 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()); search->getEstimatedMatches());
renderer.setSearchPattern(searchInfo.pattern); renderer.setSearchPattern(searchInfo.pattern);
renderer.setSearchBookQuery(searchInfo.bookFilterQuery); renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
renderer.setProtocolPrefix(m_root + "/"); renderer.setProtocolPrefix(m_root + "/");
renderer.setSearchProtocolPrefix(m_root + "/search"); renderer.setSearchProtocolPrefix(m_root + "/search");
renderer.setPageLength(pageLength); 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"); auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
if(bookIds.size() == 1) { if(bookIds.size() == 1) {
auto bookId = *bookIds.begin(); auto bookId = *bookIds.begin();
@ -777,7 +791,7 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
} }
return std::move(response); return std::move(response);
} catch (const Error& e) { } catch (const Error& e) {
return HTTP400HtmlResponse(*this, request) return HTTP400Response(*this, request)
+ invalidUrlMsg + invalidUrlMsg
+ e.message(); + e.message();
} }
@ -800,7 +814,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
} }
if (archive == nullptr) { if (archive == nullptr) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ noSuchBookErrorMsg(bookName) + noSuchBookErrorMsg(bookName)
+ TaskbarInfo(bookName); + TaskbarInfo(bookName);
} }
@ -809,7 +823,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
auto entry = archive->getRandomEntry(); auto entry = archive->getRandomEntry();
return build_redirect(bookName, getFinalItem(*archive, entry)); return build_redirect(bookName, getFinalItem(*archive, entry));
} catch(zim::EntryNotFound& e) { } catch(zim::EntryNotFound& e) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ nonParameterizedMessage("random-article-failure") + nonParameterizedMessage("random-article-failure")
+ TaskbarInfo(bookName, archive.get()); + TaskbarInfo(bookName, archive.get());
} }
@ -823,7 +837,7 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
} catch (const std::out_of_range& e) {} } catch (const std::out_of_range& e) {}
if (source.empty()) { if (source.empty()) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
@ -844,7 +858,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
host = request.get_header("Host"); host = request.get_header("Host");
url = request.get_url_part(1); url = request.get_url_part(1);
} catch (const std::out_of_range&) { } catch (const std::out_of_range&) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
@ -853,7 +867,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
} }
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") { if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
@ -950,7 +964,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
if (archive == nullptr) { if (archive == nullptr) {
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true); const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true);
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg + urlNotFoundMsg
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern)) + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName); + TaskbarInfo(bookName);
@ -984,7 +998,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
printf("Failed to find %s\n", urlStr.c_str()); printf("Failed to find %s\n", urlStr.c_str());
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true); std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true);
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg + urlNotFoundMsg
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern)) + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName, archive.get()); + TaskbarInfo(bookName, archive.get());
@ -1004,12 +1018,12 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
bookName = request.get_url_part(1); bookName = request.get_url_part(1);
kind = request.get_url_part(2); kind = request.get_url_part(2);
} catch (const std::out_of_range& e) { } catch (const std::out_of_range& e) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
if (kind != "meta" && kind!= "content") { if (kind != "meta" && kind!= "content") {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg + urlNotFoundMsg
+ invalidRawAccessMsg(kind); + invalidRawAccessMsg(kind);
} }
@ -1021,7 +1035,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
} catch (const std::out_of_range& e) {} } catch (const std::out_of_range& e) {}
if (archive == nullptr) { if (archive == nullptr) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg + urlNotFoundMsg
+ noSuchBookErrorMsg(bookName); + noSuchBookErrorMsg(bookName);
} }
@ -1047,7 +1061,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
if (m_verbose.load()) { if (m_verbose.load()) {
printf("Failed to find %s\n", itemPath.c_str()); printf("Failed to find %s\n", itemPath.c_str());
} }
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg + urlNotFoundMsg
+ rawEntryNotFoundMsg(kind, itemPath); + rawEntryNotFoundMsg(kind, itemPath);
} }

View File

@ -43,7 +43,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
try { try {
url = request.get_url_part(2); url = request.get_url_part(2);
} catch (const std::out_of_range&) { } catch (const std::out_of_range&) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
@ -70,7 +70,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
} else if (url == "illustration") { } else if (url == "illustration") {
return handle_catalog_v2_illustration(request); return handle_catalog_v2_illustration(request);
} else { } else {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
} }
@ -112,7 +112,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
try { try {
mp_library->getBookById(entryId); mp_library->getBookById(entryId);
} catch (const std::out_of_range&) { } catch (const std::out_of_range&) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
@ -161,7 +161,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
auto illustration = book.getIllustration(size); auto illustration = book.getIllustration(size);
return ContentResponse::build(*this, illustration->getData(), illustration->mimeType); return ContentResponse::build(*this, illustration->getData(), illustration->mimeType);
} catch(...) { } catch(...) {
return HTTP404HtmlResponse(*this, request) return HTTP404Response(*this, request)
+ urlNotFoundMsg; + urlNotFoundMsg;
} }
} }

View File

@ -202,4 +202,9 @@ std::string RequestContext::get_user_language() const
return "en"; return "en";
} }
std::string RequestContext::get_requested_format() const
{
return get_optional_param<std::string>("format", "html");
}
} }

View File

@ -118,6 +118,7 @@ class RequestContext {
bool can_compress() const { return acceptEncodingGzip; } bool can_compress() const { return acceptEncodingGzip; }
std::string get_user_language() const; std::string get_user_language() const;
std::string get_requested_format() const;
private: // data private: // data
std::string full_url; std::string full_url;

View File

@ -146,7 +146,7 @@ std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObjec
return r; return r;
} }
HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server, HTTPErrorResponse::HTTPErrorResponse(const InternalServer& server,
const RequestContext& request, const RequestContext& request,
int httpStatusCode, int httpStatusCode,
const std::string& pageTitleMsgId, const std::string& pageTitleMsgId,
@ -155,8 +155,8 @@ HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
: ContentResponseBlueprint(&server, : ContentResponseBlueprint(&server,
&request, &request,
httpStatusCode, httpStatusCode,
"text/html; charset=utf-8", request.get_requested_format() == "html" ? "text/html; charset=utf-8" : "application/xml; charset=utf-8",
RESOURCE::templates::error_html) request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml)
{ {
kainjow::mustache::list emptyList; kainjow::mustache::list emptyList;
this->m_data = kainjow::mustache::object{ this->m_data = kainjow::mustache::object{
@ -167,9 +167,9 @@ HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
}; };
} }
HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server, HTTP404Response::HTTP404Response(const InternalServer& server,
const RequestContext& request) const RequestContext& request)
: HTTPErrorHtmlResponse(server, : HTTPErrorResponse(server,
request, request,
MHD_HTTP_NOT_FOUND, MHD_HTTP_NOT_FOUND,
"404-page-title", "404-page-title",
@ -177,33 +177,33 @@ HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server,
{ {
} }
HTTPErrorHtmlResponse& HTTP404HtmlResponse::operator+(UrlNotFoundMsg /*unused*/) HTTPErrorResponse& HTTP404Response::operator+(UrlNotFoundMsg /*unused*/)
{ {
const std::string requestUrl = m_request.get_full_url(); const std::string requestUrl = m_request.get_full_url();
return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}}); 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}); m_data["details"].push_back({"p", msg});
return *this; return *this;
} }
HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const ParameterizedMessage& details) HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details)
{ {
return *this + details.getText(m_request.get_user_language()); 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+=) // operator+() is already a state-modifying operator (akin to operator+=)
return *this + details; return *this + details;
} }
HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server, HTTP400Response::HTTP400Response(const InternalServer& server,
const RequestContext& request) const RequestContext& request)
: HTTPErrorHtmlResponse(server, : HTTPErrorResponse(server,
request, request,
MHD_HTTP_BAD_REQUEST, MHD_HTTP_BAD_REQUEST,
"400-page-title", "400-page-title",
@ -211,7 +211,7 @@ HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server,
{ {
} }
HTTPErrorHtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/) HTTPErrorResponse& HTTP400Response::operator+(InvalidUrlMsg /*unused*/)
{ {
std::string requestUrl = m_request.get_full_url(); std::string requestUrl = m_request.get_full_url();
const auto query = m_request.get_query(); const auto query = m_request.get_query();
@ -222,9 +222,9 @@ HTTPErrorHtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/)
return *this + msgTmpl.render({"url", requestUrl}); return *this + msgTmpl.render({"url", requestUrl});
} }
HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server, HTTP500Response::HTTP500Response(const InternalServer& server,
const RequestContext& request) const RequestContext& request)
: HTTPErrorHtmlResponse(server, : HTTPErrorResponse(server,
request, request,
MHD_HTTP_INTERNAL_SERVER_ERROR, MHD_HTTP_INTERNAL_SERVER_ERROR,
"500-page-title", "500-page-title",
@ -234,7 +234,7 @@ HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server,
*this + "An internal server error occured. We are sorry about that :/"; *this + "An internal server error occured. We are sorry about that :/";
} }
std::unique_ptr<ContentResponse> HTTP500HtmlResponse::generateResponseObject() const std::unique_ptr<ContentResponse> HTTP500Response::generateResponseObject() const
{ {
// We want a 500 response to be a minimalistic one (so that the server doesn't // 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) // have to provide additional resources required for its proper rendering)

View File

@ -181,9 +181,9 @@ public: //data
std::unique_ptr<TaskbarInfo> m_taskbarInfo; std::unique_ptr<TaskbarInfo> m_taskbarInfo;
}; };
struct HTTPErrorHtmlResponse : ContentResponseBlueprint struct HTTPErrorResponse : ContentResponseBlueprint
{ {
HTTPErrorHtmlResponse(const InternalServer& server, HTTPErrorResponse(const InternalServer& server,
const RequestContext& request, const RequestContext& request,
int httpStatusCode, int httpStatusCode,
const std::string& pageTitleMsgId, const std::string& pageTitleMsgId,
@ -192,40 +192,40 @@ struct HTTPErrorHtmlResponse : ContentResponseBlueprint
using ContentResponseBlueprint::operator+; using ContentResponseBlueprint::operator+;
using ContentResponseBlueprint::operator+=; using ContentResponseBlueprint::operator+=;
HTTPErrorHtmlResponse& operator+(const std::string& msg); HTTPErrorResponse& operator+(const std::string& msg);
HTTPErrorHtmlResponse& operator+(const ParameterizedMessage& errorDetails); HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
HTTPErrorHtmlResponse& operator+=(const ParameterizedMessage& errorDetails); HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
}; };
class UrlNotFoundMsg {}; class UrlNotFoundMsg {};
extern const UrlNotFoundMsg urlNotFoundMsg; extern const UrlNotFoundMsg urlNotFoundMsg;
struct HTTP404HtmlResponse : HTTPErrorHtmlResponse struct HTTP404Response : HTTPErrorResponse
{ {
HTTP404HtmlResponse(const InternalServer& server, HTTP404Response(const InternalServer& server,
const RequestContext& request); const RequestContext& request);
using HTTPErrorHtmlResponse::operator+; using HTTPErrorResponse::operator+;
HTTPErrorHtmlResponse& operator+(UrlNotFoundMsg /*unused*/); HTTPErrorResponse& operator+(UrlNotFoundMsg /*unused*/);
}; };
class InvalidUrlMsg {}; class InvalidUrlMsg {};
extern const InvalidUrlMsg invalidUrlMsg; extern const InvalidUrlMsg invalidUrlMsg;
struct HTTP400HtmlResponse : HTTPErrorHtmlResponse struct HTTP400Response : HTTPErrorResponse
{ {
HTTP400HtmlResponse(const InternalServer& server, HTTP400Response(const InternalServer& server,
const RequestContext& request); const RequestContext& request);
using HTTPErrorHtmlResponse::operator+; using HTTPErrorResponse::operator+;
HTTPErrorHtmlResponse& operator+(InvalidUrlMsg /*unused*/); HTTPErrorResponse& operator+(InvalidUrlMsg /*unused*/);
}; };
struct HTTP500HtmlResponse : HTTPErrorHtmlResponse struct HTTP500Response : HTTPErrorResponse
{ {
HTTP500HtmlResponse(const InternalServer& server, HTTP500Response(const InternalServer& server,
const RequestContext& request); const RequestContext& request);
private: // overrides private: // overrides

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Fulltext articles search</ShortName>
<Description>Search for articles in the Library.</Description>
<Url type="application/atom+xml;profile=opds-catalog"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:k="http://kiwix.org/opensearchextension/1.0"
indexOffset="0"
template="{{root}}/search?format=xml&amp;pattern={searchTerms}&amp;books.filter.lang={language?}&amp;books.name={k:name?}&amp;pageLength={count?}&amp;start={startIndex?}"/>
</OpenSearchDescription>

View File

@ -34,7 +34,9 @@ skin/fonts/Roboto.ttf
skin/block_external.js skin/block_external.js
skin/search_results.css skin/search_results.css
templates/search_result.html templates/search_result.html
templates/search_result.xml
templates/error.html templates/error.html
templates/error.xml
templates/index.html templates/index.html
templates/suggestion.json templates/suggestion.json
templates/head_taskbar.html templates/head_taskbar.html
@ -49,4 +51,5 @@ templates/catalog_v2_categories.xml
templates/catalog_v2_languages.xml templates/catalog_v2_languages.xml
templates/url_of_search_results_css templates/url_of_search_results_css
opensearchdescription.xml opensearchdescription.xml
ft_opensearchdescription.xml
catalog_v2_searchdescription.xml catalog_v2_searchdescription.xml

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8">
<error>{{PAGE_TITLE}}</error>
{{#details}}
<detail>{{{p}}}</detail>
{{/details}}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Search: {{query.pattern}}</title>
<link>{{query.unpaginatedQuery}}&amp;format=xml&amp;start={{results.start}}&amp;pageLength={{pagination.itemsPerPage}}</link>
<description>Search result for {{query.pattern}}</description>
<opensearch:totalResults>{{results.count}}</opensearch:totalResults>
<opensearch:startIndex>{{results.start}}</opensearch:startIndex>
<opensearch:itemsPerPage>{{pagination.itemsPerPage}}</opensearch:itemsPerPage>
<atom:link rel="search" type="application/opensearchdescription+xml" href="{{protocolPrefix}}search/searchdescription.xml"/>
<opensearch:Query role="request"
searchTerms="{{query.pattern}}"{{#query.lang}}
language="{{query.lang}}"{{/query.lang}}
startIndex="{{results.start}}"
count="{{pagination.itemsPerPage}}"
/>
{{#results.items}}
<item>
<title>{{title}}</title>
<link>{{absolutePath}}</link>
{{#snippet}}
<description>{{>snippet}}...</description>
{{/snippet}}
{{#bookTitle}}
<book>
<title>{{bookTitle}}</title>
</book>
{{/bookTitle}}
{{#wordCount}}
<wordCount>{{wordCount}}</wordCount>
{{/wordCount}}
</item>
{{/results.items}}
</channel>
</rss>

View File

@ -15,7 +15,11 @@ tests = [
] ]
if build_machine.system() != 'windows' if build_machine.system() != 'windows'
tests += ['server'] tests += [
'server',
'server_html_search',
'server_xml_search'
]
endif endif

File diff suppressed because it is too large Load Diff

1308
test/server_html_search.cpp Normal file

File diff suppressed because it is too large Load Diff

164
test/server_testing_tools.h Normal file
View File

@ -0,0 +1,164 @@
#include "../include/manager.h"
#include "../include/server.h"
#include "../include/name_mapper.h"
#include "../include/tools.h"
// 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, "");
}
std::string replace(std::string s, std::string pattern, std::string replacement)
{
return std::regex_replace(s, std::regex(pattern), replacement);
}
using TestContextImpl = std::vector<std::pair<std::string, std::string> >;
struct TestContext : TestContextImpl {
TestContext(const std::initializer_list<value_type>& 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;
}
typedef httplib::Headers Headers;
Headers invariantHeaders(Headers headers)
{
headers.erase("Date");
return headers;
}
class ZimFileServer
{
public: // types
typedef std::shared_ptr<httplib::Response> Response;
typedef std::vector<std::string> 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<kiwix::HumanReadableNameMapper> nameMapper;
std::unique_ptr<kiwix::Server> server;
std::unique_ptr<httplib::Client> 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<ZimFileServer> zfs1_;
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(SERVER_PORT, /*withTaskbar=*/true, ZIMFILES));
}
void TearDown() override {
zfs1_.reset();
}
};
class TaskbarlessServerTest : public ServerTest
{
protected:
void SetUp() override {
zfs1_.reset(new ZimFileServer(SERVER_PORT, /*withTaskbar=*/false, ZIMFILES));
}
};

1029
test/server_xml_search.cpp Normal file

File diff suppressed because it is too large Load Diff