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;
|
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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +740,10 @@ 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);
|
||||||
response += nonParameterizedMessage("no-search-results");
|
response += nonParameterizedMessage("no-search-results");
|
||||||
if(bookIds.size() == 1) {
|
if(bookIds.size() == 1) {
|
||||||
auto bookId = *bookIds.begin();
|
auto bookId = *bookIds.begin();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -146,17 +146,17 @@ 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,
|
||||||
const std::string& headingMsgId,
|
const std::string& headingMsgId,
|
||||||
const std::string& cssUrl)
|
const std::string& cssUrl)
|
||||||
: 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,51 +167,51 @@ 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",
|
||||||
"404-page-heading")
|
"404-page-heading")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
||||||
"400-page-heading")
|
"400-page-heading")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
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,19 +222,19 @@ 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",
|
||||||
"500-page-heading")
|
"500-page-heading")
|
||||||
{
|
{
|
||||||
// operator+() is a state-modifying operator (akin to operator+=)
|
// operator+() is a state-modifying operator (akin to operator+=)
|
||||||
*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)
|
||||||
|
|
|
@ -181,52 +181,52 @@ 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,
|
||||||
const std::string& headingMsgId,
|
const std::string& headingMsgId,
|
||||||
const std::string& cssUrl = "");
|
const std::string& cssUrl = "");
|
||||||
|
|
||||||
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
|
||||||
// generateResponseObject() is overriden in order to produce a minimal
|
// generateResponseObject() is overriden in order to produce a minimal
|
||||||
|
|
|
@ -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/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
|
||||||
|
|
|
@ -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'
|
if build_machine.system() != 'windows'
|
||||||
tests += ['server']
|
tests += [
|
||||||
|
'server',
|
||||||
|
'server_html_search',
|
||||||
|
'server_xml_search'
|
||||||
|
]
|
||||||
endif
|
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