mirror of https://github.com/kiwix/libkiwix.git
Merge pull request #731 from kiwix/opensearch
Render xml result - opensearch
This commit is contained in:
commit
56167dc23e
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -499,7 +499,7 @@ std::unique_ptr<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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,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.
|
||||
// (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,
|
||||
HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND,
|
||||
"fulltext-search-unavailable",
|
||||
"404-page-heading",
|
||||
cssUrl);
|
||||
|
@ -745,10 +753,11 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
|||
return response;
|
||||
}
|
||||
|
||||
auto start = 0;
|
||||
auto start = 1;
|
||||
try {
|
||||
start = request.get_argument<unsigned int>("start");
|
||||
} catch (const std::exception&) {}
|
||||
start = max(1, start);
|
||||
|
||||
auto pageLength = 25;
|
||||
try {
|
||||
|
@ -762,13 +771,18 @@ std::unique_ptr<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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);
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ std::unique_ptr<Response> 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<Response> 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<Response> 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<Response> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -202,4 +202,9 @@ std::string RequestContext::get_user_language() const
|
|||
return "en";
|
||||
}
|
||||
|
||||
std::string RequestContext::get_requested_format() const
|
||||
{
|
||||
return get_optional_param<std::string>("format", "html");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -146,7 +146,7 @@ std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObjec
|
|||
return r;
|
||||
}
|
||||
|
||||
HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
|
||||
HTTPErrorResponse::HTTPErrorResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
int httpStatusCode,
|
||||
const std::string& pageTitleMsgId,
|
||||
|
@ -155,8 +155,8 @@ HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
|
|||
: 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,9 +167,9 @@ HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
|
|||
};
|
||||
}
|
||||
|
||||
HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server,
|
||||
HTTP404Response::HTTP404Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorHtmlResponse(server,
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
MHD_HTTP_NOT_FOUND,
|
||||
"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();
|
||||
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,
|
||||
HTTP400Response::HTTP400Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorHtmlResponse(server,
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
MHD_HTTP_BAD_REQUEST,
|
||||
"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();
|
||||
const auto query = m_request.get_query();
|
||||
|
@ -222,9 +222,9 @@ HTTPErrorHtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/)
|
|||
return *this + msgTmpl.render({"url", requestUrl});
|
||||
}
|
||||
|
||||
HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server,
|
||||
HTTP500Response::HTTP500Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorHtmlResponse(server,
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"500-page-title",
|
||||
|
@ -234,7 +234,7 @@ HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server,
|
|||
*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
|
||||
// have to provide additional resources required for its proper rendering)
|
||||
|
|
|
@ -181,9 +181,9 @@ public: //data
|
|||
std::unique_ptr<TaskbarInfo> m_taskbarInfo;
|
||||
};
|
||||
|
||||
struct HTTPErrorHtmlResponse : ContentResponseBlueprint
|
||||
struct HTTPErrorResponse : ContentResponseBlueprint
|
||||
{
|
||||
HTTPErrorHtmlResponse(const InternalServer& server,
|
||||
HTTPErrorResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
int httpStatusCode,
|
||||
const std::string& pageTitleMsgId,
|
||||
|
@ -192,40 +192,40 @@ struct HTTPErrorHtmlResponse : ContentResponseBlueprint
|
|||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
HTTP500Response(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
private: // overrides
|
||||
|
|
|
@ -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&pattern={searchTerms}&books.filter.lang={language?}&books.name={k:name?}&pageLength={count?}&start={startIndex?}"/>
|
||||
</OpenSearchDescription>
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8">
|
||||
<error>{{PAGE_TITLE}}</error>
|
||||
{{#details}}
|
||||
<detail>{{{p}}}</detail>
|
||||
{{/details}}
|
|
@ -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}}&format=xml&start={{results.start}}&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>
|
|
@ -15,7 +15,11 @@ tests = [
|
|||
]
|
||||
|
||||
if build_machine.system() != 'windows'
|
||||
tests += ['server']
|
||||
tests += [
|
||||
'server',
|
||||
'server_html_search',
|
||||
'server_xml_search'
|
||||
]
|
||||
endif
|
||||
|
||||
|
||||
|
|
1541
test/server.cpp
1541
test/server.cpp
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue