mirror of
https://github.com/kiwix/libkiwix.git
synced 2026-01-19 19:58:14 -05:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e22e073d43 | ||
|
|
6dcf4ee034 | ||
|
|
61ccbc65fb | ||
|
|
85a9d35488 | ||
|
|
a17258fcc9 | ||
|
|
ae1bf39023 | ||
|
|
dbcbdff275 | ||
|
|
c1823b8ee4 | ||
|
|
3f41ce8337 | ||
|
|
2a20e87341 | ||
|
|
2028bf3a98 | ||
|
|
545d409150 | ||
|
|
89dc9afc28 | ||
|
|
647118dd5e | ||
|
|
d8a60db739 | ||
|
|
f4059f3faf | ||
|
|
800cc5b68a | ||
|
|
b1f03385e4 | ||
|
|
feb30d08aa | ||
|
|
95d4dd63ac | ||
|
|
311f783ea9 | ||
|
|
f2a1c0f106 | ||
|
|
2cc4befb12 | ||
|
|
3641dbf14d | ||
|
|
1962262f94 | ||
|
|
7407f30790 | ||
|
|
d740ffe465 | ||
|
|
e7293346be | ||
|
|
b1643e422e | ||
|
|
574c1ad690 | ||
|
|
59364a737a | ||
|
|
49f24d18df | ||
|
|
ec2e10b40e | ||
|
|
2da8ea1650 | ||
|
|
0eb8f09f79 | ||
|
|
0ecbdbcf63 | ||
|
|
9bc09a815c | ||
|
|
48d377ca44 | ||
|
|
d5ae92e4e2 | ||
|
|
1a5e2eda0f | ||
|
|
89785a259a | ||
|
|
668063205c | ||
|
|
df98c58d07 | ||
|
|
ff8da65c68 | ||
|
|
ae60ba806b | ||
|
|
8cfcf2ea86 | ||
|
|
26c16bb1b2 | ||
|
|
ca965d448f | ||
|
|
6d16d7386d | ||
|
|
40e9a19c48 | ||
|
|
d487c78ea4 | ||
|
|
96cbd2bf26 |
@@ -1,3 +1,11 @@
|
||||
libkiwix 10.1.1
|
||||
===============
|
||||
|
||||
* Correctly detect the number of article for older zims (<=6) (@mgautier #743)
|
||||
* [server] Fix fulltext search (@mgautierfr #724)
|
||||
* [server][internal] New way to build Error message (@veloman-yunkan #732 #738 #744)
|
||||
* Fix CI (@mgautierfr #736)
|
||||
|
||||
libkiwix 10.1.0
|
||||
===============
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ task writePom {
|
||||
project {
|
||||
groupId 'org.kiwix.kiwixlib'
|
||||
artifactId 'kiwixlib'
|
||||
version '10.1.0' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
|
||||
version '10.1.1' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
|
||||
packaging 'aar'
|
||||
name 'kiwixlib'
|
||||
url 'https://github.com/kiwix/libkiwix'
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
breathe
|
||||
exhale
|
||||
sphinx<4
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('libkiwix', 'cpp',
|
||||
version : '10.1.0', # Also change this in android-kiwix-lib-publisher/kiwixLibAndroid/build.gradle
|
||||
version : '10.1.1', # Also change this in android-kiwix-lib-publisher/kiwixLibAndroid/build.gradle
|
||||
license : 'GPLv3+',
|
||||
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ void Book::update(const zim::Archive& archive) {
|
||||
m_flavour = getMetaFlavour(archive);
|
||||
m_tags = getMetaTags(archive);
|
||||
m_category = getCategoryFromTags();
|
||||
m_articleCount = archive.getArticleCount();
|
||||
m_articleCount = getArchiveArticleCount(archive);
|
||||
m_mediaCount = getArchiveMediaCount(archive);
|
||||
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
|
||||
|
||||
|
||||
@@ -73,9 +73,6 @@ IllustrationInfo getBookIllustrationInfo(const Book& book)
|
||||
kainjow::mustache::object getSingleBookData(const Book& book)
|
||||
{
|
||||
const auto bookDate = book.getDate() + "T00:00:00Z";
|
||||
const MustacheData bookUrl = book.getUrl().empty()
|
||||
? MustacheData(false)
|
||||
: MustacheData(book.getUrl());
|
||||
return kainjow::mustache::object{
|
||||
{"id", book.getId()},
|
||||
{"name", book.getName()},
|
||||
@@ -92,7 +89,7 @@ kainjow::mustache::object getSingleBookData(const Book& book)
|
||||
{"media_count", to_string(book.getMediaCount())},
|
||||
{"author_name", book.getCreator()},
|
||||
{"publisher_name", book.getPublisher()},
|
||||
{"url", bookUrl},
|
||||
{"url", onlyAsNonEmptyMustacheValue(book.getUrl())},
|
||||
{"size", to_string(book.getSize())},
|
||||
{"icons", getBookIllustrationInfo(book)},
|
||||
};
|
||||
@@ -194,7 +191,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const s
|
||||
{"date", gen_date_str()},
|
||||
{"root", rootLocation},
|
||||
{"feed_id", gen_uuid(libraryId + "/catalog/search?"+query)},
|
||||
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
|
||||
{"filter", onlyAsNonEmptyMustacheValue(query)},
|
||||
{"totalResults", to_string(m_totalResults)},
|
||||
{"startIndex", to_string(m_startIndex)},
|
||||
{"itemsPerPage", to_string(m_count)},
|
||||
@@ -214,7 +211,7 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const
|
||||
{"date", gen_date_str()},
|
||||
{"endpoint_root", endpointRoot},
|
||||
{"feed_id", gen_uuid(libraryId + endpoint + "?" + query)},
|
||||
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
|
||||
{"filter", onlyAsNonEmptyMustacheValue(query)},
|
||||
{"query", query.empty() ? "" : "?" + urlEncode(query)},
|
||||
{"totalResults", to_string(m_totalResults)},
|
||||
{"startIndex", to_string(m_startIndex)},
|
||||
|
||||
@@ -109,6 +109,54 @@ unsigned int getCacheLength(const char* name, unsigned int defaultVal) {
|
||||
}
|
||||
} // unnamed namespace
|
||||
|
||||
SearchInfo::SearchInfo(const std::string& pattern)
|
||||
: pattern(pattern),
|
||||
geoQuery()
|
||||
{}
|
||||
|
||||
SearchInfo::SearchInfo(const std::string& pattern, GeoQuery geoQuery)
|
||||
: pattern(pattern),
|
||||
geoQuery(geoQuery)
|
||||
{}
|
||||
|
||||
SearchInfo::SearchInfo(const RequestContext& request)
|
||||
: pattern(request.get_optional_param<std::string>("pattern", "")),
|
||||
geoQuery(),
|
||||
bookName(request.get_optional_param<std::string>("content", ""))
|
||||
{
|
||||
/* Retrive geo search */
|
||||
try {
|
||||
auto latitude = request.get_argument<float>("latitude");
|
||||
auto longitude = request.get_argument<float>("longitude");
|
||||
auto distance = request.get_argument<float>("distance");
|
||||
geoQuery = GeoQuery(latitude, longitude, distance);
|
||||
} catch(const std::out_of_range&) {}
|
||||
catch(const std::invalid_argument&) {}
|
||||
|
||||
if (!geoQuery && pattern.empty()) {
|
||||
throw std::invalid_argument("No query provided.");
|
||||
}
|
||||
}
|
||||
|
||||
zim::Query SearchInfo::getZimQuery(bool verbose) const {
|
||||
zim::Query query;
|
||||
if (verbose) {
|
||||
std::cout << "Performing query '" << pattern<< "'";
|
||||
}
|
||||
query.setQuery(pattern);
|
||||
if (geoQuery) {
|
||||
if (verbose) {
|
||||
std::cout << " with geo query '" << geoQuery.distance << "&(" << geoQuery.latitude << ";" << geoQuery.longitude << ")'";
|
||||
}
|
||||
query.setGeorange(geoQuery.latitude, geoQuery.longitude, geoQuery.distance);
|
||||
}
|
||||
if (verbose) {
|
||||
std::cout << std::endl;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
static IdNameMapper defaultNameMapper;
|
||||
|
||||
static MHD_Result staticHandlerCallback(void* cls,
|
||||
@@ -278,8 +326,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& request)
|
||||
{
|
||||
try {
|
||||
if (! request.is_valid_url())
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
if (! request.is_valid_url()) {
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
const ETag etag = get_matching_if_none_match_etag(request);
|
||||
if ( etag )
|
||||
@@ -309,10 +359,12 @@ 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 Response::build_500(*this, e.what());
|
||||
return HTTP500HtmlResponse(*this, request)
|
||||
+ e.what();
|
||||
} catch (...) {
|
||||
fprintf(stderr, "===== Unhandled unknown error\n");
|
||||
return Response::build_500(*this, "Unknown error");
|
||||
return HTTP500HtmlResponse(*this, request)
|
||||
+ "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +439,20 @@ SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Arch
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string noSuchBookErrorMsg(const std::string& bookName)
|
||||
{
|
||||
return "No such book: " + bookName;
|
||||
}
|
||||
|
||||
std::string noSearchResultsMsg()
|
||||
{
|
||||
return "The fulltext search engine is not available for this content.";
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& request)
|
||||
{
|
||||
@@ -405,8 +471,9 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
}
|
||||
|
||||
if (archive == nullptr) {
|
||||
const std::string error_details = "No such book: " + bookName;
|
||||
return Response::build_404(*this, "", bookName, "", error_details);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ noSuchBookErrorMsg(bookName)
|
||||
+ TaskbarInfo(bookName);
|
||||
}
|
||||
|
||||
const auto queryString = request.get_optional_param("term", std::string());
|
||||
@@ -476,7 +543,8 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
|
||||
response->set_cacheable();
|
||||
return std::move(response);
|
||||
} catch (const ResourceNotFound& e) {
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,117 +554,92 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
printf("** running handle_search\n");
|
||||
}
|
||||
|
||||
std::string patternString;
|
||||
try {
|
||||
patternString = request.get_argument("pattern");
|
||||
} catch (const std::out_of_range&) {}
|
||||
auto searchInfo = SearchInfo(request);
|
||||
|
||||
/* Retrive geo search */
|
||||
bool has_geo_query = false;
|
||||
float latitude = 0;
|
||||
float longitude = 0;
|
||||
float distance = 0;
|
||||
try {
|
||||
latitude = request.get_argument<float>("latitude");
|
||||
longitude = request.get_argument<float>("longitude");
|
||||
distance = request.get_argument<float>("distance");
|
||||
has_geo_query = true;
|
||||
} catch(const std::out_of_range&) {}
|
||||
catch(const std::invalid_argument&) {}
|
||||
|
||||
std::string bookName, bookId;
|
||||
std::shared_ptr<zim::Archive> archive;
|
||||
try {
|
||||
bookName = request.get_argument("content");
|
||||
bookId = mp_nameMapper->getIdForName(bookName);
|
||||
archive = mp_library->getArchiveById(bookId);
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
/* Make the search */
|
||||
if ( (!archive && !bookName.empty())
|
||||
|| (patternString.empty() && ! has_geo_query) ) {
|
||||
auto data = get_default_data();
|
||||
data.set("pattern", encodeDiples(patternString));
|
||||
data.set("root", m_root);
|
||||
auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8");
|
||||
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
|
||||
response->set_code(MHD_HTTP_NOT_FOUND);
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
std::shared_ptr<zim::Searcher> searcher;
|
||||
if (archive) {
|
||||
searcher = searcherCache.getOrPut(bookId, [=](){ return std::make_shared<zim::Searcher>(*archive);});
|
||||
} else {
|
||||
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
|
||||
auto currentArchive = mp_library->getArchiveById(bookId);
|
||||
if (currentArchive) {
|
||||
if (! searcher) {
|
||||
searcher = std::make_shared<zim::Searcher>(*currentArchive);
|
||||
} else {
|
||||
searcher->addArchive(*currentArchive);
|
||||
}
|
||||
std::string bookId;
|
||||
std::shared_ptr<zim::Archive> archive;
|
||||
if (!searchInfo.bookName.empty()) {
|
||||
try {
|
||||
bookId = mp_nameMapper->getIdForName(searchInfo.bookName);
|
||||
archive = mp_library->getArchiveById(bookId);
|
||||
} catch (const std::out_of_range&) {
|
||||
throw std::invalid_argument("The requested book doesn't exist.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto start = 0;
|
||||
try {
|
||||
start = request.get_argument<unsigned int>("start");
|
||||
} catch (const std::exception&) {}
|
||||
|
||||
auto pageLength = 25;
|
||||
try {
|
||||
pageLength = request.get_argument<unsigned int>("pageLength");
|
||||
} catch (const std::exception&) {}
|
||||
if (pageLength > MAX_SEARCH_LEN) {
|
||||
pageLength = MAX_SEARCH_LEN;
|
||||
}
|
||||
if (pageLength == 0) {
|
||||
pageLength = 25;
|
||||
}
|
||||
|
||||
/* Get the results */
|
||||
std::string queryString;
|
||||
try {
|
||||
zim::Query query;
|
||||
if (patternString.empty()) {
|
||||
// Execute geo-search
|
||||
if (m_verbose.load()) {
|
||||
cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl;
|
||||
}
|
||||
|
||||
query.setQuery("");
|
||||
queryString = "GEO:" + to_string(latitude) + to_string(longitude) + to_string(distance);
|
||||
query.setGeorange(latitude, longitude, distance);
|
||||
} else {
|
||||
// Execute Ft search
|
||||
if (m_verbose.load()) {
|
||||
cout << "Performing query `" << patternString << "'" << endl;
|
||||
}
|
||||
|
||||
queryString = "FT:" + removeAccents(patternString);
|
||||
query.setQuery(queryString);
|
||||
}
|
||||
queryString = bookId + queryString;
|
||||
|
||||
/* Make the search */
|
||||
// Try to get a search from the searchInfo, else build it
|
||||
std::shared_ptr<zim::Search> search;
|
||||
search = searchCache.getOrPut(queryString, [=](){ return make_shared<zim::Search>(searcher->search(query));});
|
||||
try {
|
||||
search = searchCache.getOrPut(searchInfo,
|
||||
[=](){
|
||||
std::shared_ptr<zim::Searcher> searcher;
|
||||
if (archive) {
|
||||
searcher = searcherCache.getOrPut(bookId, [=](){ return std::make_shared<zim::Searcher>(*archive);});
|
||||
} else {
|
||||
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
|
||||
auto currentArchive = mp_library->getArchiveById(bookId);
|
||||
if (currentArchive) {
|
||||
if (! searcher) {
|
||||
searcher = std::make_shared<zim::Searcher>(*currentArchive);
|
||||
} else {
|
||||
searcher->addArchive(*currentArchive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return make_shared<zim::Search>(searcher->search(searchInfo.getZimQuery(m_verbose.load())));
|
||||
}
|
||||
);
|
||||
} catch(std::runtime_error& e) {
|
||||
// Searcher->search will throw a runtime error if there is no valid xapian database to do the search.
|
||||
// (in case of zim file not containing a index)
|
||||
return HTTPErrorHtmlResponse(*this, request, MHD_HTTP_NOT_FOUND,
|
||||
"Fulltext search unavailable",
|
||||
"Not Found",
|
||||
m_root + "/skin/search_results.css")
|
||||
+ noSearchResultsMsg()
|
||||
+ TaskbarInfo(searchInfo.bookName, archive.get());
|
||||
}
|
||||
|
||||
|
||||
auto start = 0;
|
||||
try {
|
||||
start = request.get_argument<unsigned int>("start");
|
||||
} catch (const std::exception&) {}
|
||||
|
||||
auto pageLength = 25;
|
||||
try {
|
||||
pageLength = request.get_argument<unsigned int>("pageLength");
|
||||
} catch (const std::exception&) {}
|
||||
if (pageLength > MAX_SEARCH_LEN) {
|
||||
pageLength = MAX_SEARCH_LEN;
|
||||
}
|
||||
if (pageLength == 0) {
|
||||
pageLength = 25;
|
||||
}
|
||||
|
||||
/* Get the results */
|
||||
SearchRenderer renderer(search->getResults(start, pageLength), mp_nameMapper, mp_library, start,
|
||||
search->getEstimatedMatches());
|
||||
renderer.setSearchPattern(patternString);
|
||||
renderer.setSearchContent(bookName);
|
||||
renderer.setSearchPattern(searchInfo.pattern);
|
||||
renderer.setSearchContent(searchInfo.bookName);
|
||||
renderer.setProtocolPrefix(m_root + "/");
|
||||
renderer.setSearchProtocolPrefix(m_root + "/search?");
|
||||
renderer.setPageLength(pageLength);
|
||||
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
|
||||
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
|
||||
|
||||
response->set_taskbar(searchInfo.bookName, archive.get());
|
||||
return std::move(response);
|
||||
} catch (const std::invalid_argument& e) {
|
||||
return HTTP400HtmlResponse(*this, request)
|
||||
+ invalidUrlMsg
|
||||
+ std::string(e.what());
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << e.what() << std::endl;
|
||||
return Response::build_500(*this, e.what());
|
||||
return HTTP500HtmlResponse(*this, request)
|
||||
+ e.what();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,8 +660,9 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
}
|
||||
|
||||
if (archive == nullptr) {
|
||||
const std::string error_details = "No such book: " + bookName;
|
||||
return Response::build_404(*this, "", bookName, "", error_details);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ noSuchBookErrorMsg(bookName)
|
||||
+ TaskbarInfo(bookName);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -626,7 +670,9 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
const std::string error_details = "Oops! Failed to pick a random article :(";
|
||||
return Response::build_404(*this, "", bookName, getArchiveTitle(*archive), error_details);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ error_details
|
||||
+ TaskbarInfo(bookName, archive.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,8 +683,10 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
|
||||
source = kiwix::urlDecode(request.get_argument("source"));
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (source.empty())
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
if (source.empty()) {
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
auto data = get_default_data();
|
||||
data.set("source", source);
|
||||
@@ -657,7 +705,8 @@ 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 Response::build_404(*this, request.get_full_url(), "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (url == "v2") {
|
||||
@@ -665,7 +714,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
}
|
||||
|
||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (url == "searchdescription.xml") {
|
||||
@@ -799,10 +849,11 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (archive == nullptr) {
|
||||
std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true); // Make a full search on the entire library.
|
||||
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
|
||||
|
||||
return Response::build_404(*this, request.get_full_url(), bookName, "", details);
|
||||
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern))
|
||||
+ TaskbarInfo(bookName);
|
||||
}
|
||||
|
||||
auto urlStr = request.get_url().substr(bookName.size()+1);
|
||||
@@ -819,7 +870,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
}
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
try {
|
||||
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, getArchiveTitle(*archive));
|
||||
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, archive.get());
|
||||
} catch (std::bad_cast& e) {}
|
||||
|
||||
if (m_verbose.load()) {
|
||||
@@ -832,10 +883,11 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
if (m_verbose.load())
|
||||
printf("Failed to find %s\n", urlStr.c_str());
|
||||
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true); // Make a search on this specific book only.
|
||||
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
|
||||
|
||||
return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), details);
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern))
|
||||
+ TaskbarInfo(bookName, archive.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,12 +904,15 @@ 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 Response::build_404(*this, request.get_full_url(), bookName, "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (kind != "meta" && kind!= "content") {
|
||||
const std::string error_details = kind + " is not a valid request for raw content.";
|
||||
return Response::build_404(*this, request.get_full_url(), bookName, "", error_details);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ error_details;
|
||||
}
|
||||
|
||||
std::shared_ptr<zim::Archive> archive;
|
||||
@@ -867,8 +922,9 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (archive == nullptr) {
|
||||
const std::string error_details = "No such book: " + bookName;
|
||||
return Response::build_404(*this, request.get_full_url(), bookName, "", error_details);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
// Remove the beggining of the path:
|
||||
@@ -893,7 +949,9 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
printf("Failed to find %s\n", itemPath.c_str());
|
||||
}
|
||||
const std::string error_details = "Cannot find " + kind + " entry " + itemPath;
|
||||
return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), error_details);
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ error_details;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,9 +43,53 @@ extern "C" {
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
struct GeoQuery {
|
||||
GeoQuery()
|
||||
: GeoQuery(0, 0, -1)
|
||||
{}
|
||||
|
||||
GeoQuery(float latitude, float longitude, float distance)
|
||||
: latitude(latitude), longitude(longitude), distance(distance)
|
||||
{}
|
||||
float latitude;
|
||||
float longitude;
|
||||
float distance;
|
||||
|
||||
explicit operator bool() const {
|
||||
return distance >= 0;
|
||||
}
|
||||
|
||||
friend bool operator<(const GeoQuery& l, const GeoQuery& r)
|
||||
{
|
||||
return std::tie(l.latitude, l.longitude, l.distance)
|
||||
< std::tie(r.latitude, r.longitude, r.distance); // keep the same order
|
||||
}
|
||||
};
|
||||
|
||||
class SearchInfo {
|
||||
public:
|
||||
SearchInfo(const std::string& pattern);
|
||||
SearchInfo(const std::string& pattern, GeoQuery geoQuery);
|
||||
SearchInfo(const RequestContext& request);
|
||||
|
||||
zim::Query getZimQuery(bool verbose) const;
|
||||
|
||||
friend bool operator<(const SearchInfo& l, const SearchInfo& r)
|
||||
{
|
||||
return std::tie(l.bookName, l.pattern, l.geoQuery)
|
||||
< std::tie(r.bookName, r.pattern, r.geoQuery); // keep the same order
|
||||
}
|
||||
|
||||
public: //data
|
||||
std::string pattern;
|
||||
GeoQuery geoQuery;
|
||||
std::string bookName;
|
||||
};
|
||||
|
||||
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
typedef ConcurrentCache<string, std::shared_ptr<zim::Searcher>> SearcherCache;
|
||||
typedef ConcurrentCache<string, std::shared_ptr<zim::Search>> SearchCache;
|
||||
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;
|
||||
typedef ConcurrentCache<string, std::shared_ptr<zim::SuggestionSearcher>> SuggestionSearcherCache;
|
||||
|
||||
class Entry;
|
||||
@@ -77,7 +121,7 @@ class InternalServer {
|
||||
bool start();
|
||||
void stop();
|
||||
std::string getAddress() { return m_addr; }
|
||||
int getPort() { return m_port; }
|
||||
int getPort() { return m_port; }
|
||||
|
||||
private: // functions
|
||||
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
||||
@@ -133,8 +177,6 @@ class InternalServer {
|
||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage, bool raw);
|
||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw);
|
||||
friend std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg);
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
|
||||
try {
|
||||
url = request.get_url_part(2);
|
||||
} catch (const std::out_of_range&) {
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
if (url == "root.xml") {
|
||||
@@ -69,7 +70,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
|
||||
} else if (url == "illustration") {
|
||||
return handle_catalog_v2_illustration(request);
|
||||
} else {
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +112,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
try {
|
||||
mp_library->getBookById(entryId);
|
||||
} catch (const std::out_of_range&) {
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
OPDSDumper opdsDumper(mp_library);
|
||||
@@ -158,7 +161,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
|
||||
auto illustration = book.getIllustration(size);
|
||||
return ContentResponse::build(*this, illustration->getData(), illustration->mimeType);
|
||||
} catch(...) {
|
||||
return Response::build_404(*this, request.get_full_url(), "", "");
|
||||
return HTTP404HtmlResponse(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "tools/regexTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "tools/otherTools.h"
|
||||
#include "tools/archiveTools.h"
|
||||
|
||||
#include "string.h"
|
||||
#include <mustache.hpp>
|
||||
@@ -83,19 +84,112 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
|
||||
return response;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_404(const InternalServer& server, const std::string& url, const std::string& bookName, const std::string& bookTitle, const std::string& details)
|
||||
const UrlNotFoundMsg urlNotFoundMsg;
|
||||
const InvalidUrlMsg invalidUrlMsg;
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObject() const
|
||||
{
|
||||
MustacheData results;
|
||||
if ( !url.empty() ) {
|
||||
results.set("url", url);
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
if ( m_taskbarInfo ) {
|
||||
r->set_taskbar(m_taskbarInfo->bookName, m_taskbarInfo->archive);
|
||||
}
|
||||
results.set("details", details);
|
||||
return r;
|
||||
}
|
||||
|
||||
auto response = ContentResponse::build(server, RESOURCE::templates::_404_html, results, "text/html");
|
||||
response->set_code(MHD_HTTP_NOT_FOUND);
|
||||
response->set_taskbar(bookName, bookTitle);
|
||||
HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
int httpStatusCode,
|
||||
const std::string& pageTitleMsg,
|
||||
const std::string& headingMsg,
|
||||
const std::string& cssUrl)
|
||||
: ContentResponseBlueprint(&server,
|
||||
&request,
|
||||
httpStatusCode,
|
||||
"text/html; charset=utf-8",
|
||||
RESOURCE::templates::error_html)
|
||||
{
|
||||
kainjow::mustache::list emptyList;
|
||||
this->m_data = kainjow::mustache::object{
|
||||
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) },
|
||||
{"PAGE_TITLE", pageTitleMsg},
|
||||
{"PAGE_HEADING", headingMsg},
|
||||
{"details", emptyList}
|
||||
};
|
||||
}
|
||||
|
||||
return std::move(response);
|
||||
HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorHtmlResponse(server,
|
||||
request,
|
||||
MHD_HTTP_NOT_FOUND,
|
||||
"Content not found",
|
||||
"Not Found")
|
||||
{
|
||||
}
|
||||
|
||||
HTTPErrorHtmlResponse& HTTP404HtmlResponse::operator+(UrlNotFoundMsg /*unused*/)
|
||||
{
|
||||
const std::string requestUrl = m_request.get_full_url();
|
||||
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{url}}" was not found on this server.)");
|
||||
return *this + msgTmpl.render({"url", requestUrl});
|
||||
}
|
||||
|
||||
HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const std::string& msg)
|
||||
{
|
||||
m_data["details"].push_back({"p", msg});
|
||||
return *this;
|
||||
}
|
||||
|
||||
HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorHtmlResponse(server,
|
||||
request,
|
||||
MHD_HTTP_BAD_REQUEST,
|
||||
"Invalid request",
|
||||
"Invalid request")
|
||||
{
|
||||
}
|
||||
|
||||
HTTPErrorHtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/)
|
||||
{
|
||||
std::string requestUrl = m_request.get_full_url();
|
||||
const auto query = m_request.get_query();
|
||||
if (!query.empty()) {
|
||||
requestUrl += "?" + encodeDiples(query);
|
||||
}
|
||||
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{{url}}}" is not a valid request.)");
|
||||
return *this + msgTmpl.render({"url", requestUrl});
|
||||
}
|
||||
|
||||
HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorHtmlResponse(server,
|
||||
request,
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"Internal Server Error",
|
||||
"Internal Server Error")
|
||||
{
|
||||
// operator+() is a state-modifying operator (akin to operator+=)
|
||||
*this + "An internal server error occured. We are sorry about that :/";
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> HTTP500HtmlResponse::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)
|
||||
// ";raw=true" in the MIME-type below disables response decoration
|
||||
// (see ContentResponse::contentDecorationAllowed())
|
||||
const std::string mimeType = "text/html;charset=utf-8;raw=true";
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
return r;
|
||||
}
|
||||
|
||||
ContentResponseBlueprint& ContentResponseBlueprint::operator+(const TaskbarInfo& taskbarInfo)
|
||||
{
|
||||
this->m_taskbarInfo.reset(new TaskbarInfo(taskbarInfo));
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
|
||||
@@ -111,26 +205,6 @@ std::unique_ptr<Response> Response::build_416(const InternalServer& server, size
|
||||
return response;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg)
|
||||
{
|
||||
MustacheData data;
|
||||
data.set("error", msg);
|
||||
auto content = render_template(RESOURCE::templates::_500_html, data);
|
||||
std::unique_ptr<Response> response (
|
||||
new ContentResponse(
|
||||
server.m_root, //root
|
||||
true, //verbose
|
||||
true, //raw
|
||||
false, //withTaskbar
|
||||
false, //withLibraryButton
|
||||
false, //blockExternalLinks
|
||||
content, //content
|
||||
"text/html" //mimetype
|
||||
));
|
||||
response->set_code(MHD_HTTP_INTERNAL_SERVER_ERROR);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
|
||||
{
|
||||
@@ -332,10 +406,10 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ContentResponse::set_taskbar(const std::string& bookName, const std::string& bookTitle)
|
||||
void ContentResponse::set_taskbar(const std::string& bookName, const zim::Archive* archive)
|
||||
{
|
||||
m_bookName = bookName;
|
||||
m_bookTitle = bookTitle;
|
||||
m_bookTitle = archive ? getArchiveTitle(*archive) : "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,13 +33,15 @@ extern "C" {
|
||||
#include "microhttpd_wrapper.h"
|
||||
}
|
||||
|
||||
namespace zim {
|
||||
class Archive;
|
||||
} // namespace zim
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
class InternalServer;
|
||||
class RequestContext;
|
||||
|
||||
class EntryResponse;
|
||||
|
||||
class Response {
|
||||
public:
|
||||
Response(bool verbose);
|
||||
@@ -47,9 +49,7 @@ class Response {
|
||||
|
||||
static std::unique_ptr<Response> build(const InternalServer& server);
|
||||
static std::unique_ptr<Response> build_304(const InternalServer& server, const ETag& etag);
|
||||
static std::unique_ptr<Response> build_404(const InternalServer& server, const std::string& url, const std::string& bookName, const std::string& bookTitle, const std::string& details="");
|
||||
static std::unique_ptr<Response> build_416(const InternalServer& server, size_t resourceLength);
|
||||
static std::unique_ptr<Response> build_500(const InternalServer& server, const std::string& msg);
|
||||
static std::unique_ptr<Response> build_redirect(const InternalServer& server, const std::string& redirectUrl);
|
||||
|
||||
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
|
||||
@@ -100,7 +100,7 @@ class ContentResponse : public Response {
|
||||
const std::string& mimetype,
|
||||
bool isHomePage = false);
|
||||
|
||||
void set_taskbar(const std::string& bookName, const std::string& bookTitle);
|
||||
void set_taskbar(const std::string& bookName, const zim::Archive* archive);
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
@@ -124,6 +124,110 @@ class ContentResponse : public Response {
|
||||
std::string m_bookTitle;
|
||||
};
|
||||
|
||||
struct TaskbarInfo
|
||||
{
|
||||
const std::string bookName;
|
||||
const zim::Archive* const archive;
|
||||
|
||||
TaskbarInfo(const std::string& bookName, const zim::Archive* a = nullptr)
|
||||
: bookName(bookName)
|
||||
, archive(a)
|
||||
{}
|
||||
};
|
||||
|
||||
class ContentResponseBlueprint
|
||||
{
|
||||
public: // functions
|
||||
ContentResponseBlueprint(const InternalServer* server,
|
||||
const RequestContext* request,
|
||||
int httpStatusCode,
|
||||
const std::string& mimeType,
|
||||
const std::string& templateStr)
|
||||
: m_server(*server)
|
||||
, m_request(*request)
|
||||
, m_httpStatusCode(httpStatusCode)
|
||||
, m_mimeType(mimeType)
|
||||
, m_template(templateStr)
|
||||
{}
|
||||
|
||||
virtual ~ContentResponseBlueprint() = default;
|
||||
|
||||
operator std::unique_ptr<ContentResponse>() const
|
||||
{
|
||||
return generateResponseObject();
|
||||
}
|
||||
|
||||
operator std::unique_ptr<Response>() const
|
||||
{
|
||||
return operator std::unique_ptr<ContentResponse>();
|
||||
}
|
||||
|
||||
|
||||
ContentResponseBlueprint& operator+(const TaskbarInfo& taskbarInfo);
|
||||
|
||||
protected: // functions
|
||||
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
|
||||
|
||||
public: //data
|
||||
const InternalServer& m_server;
|
||||
const RequestContext& m_request;
|
||||
const int m_httpStatusCode;
|
||||
const std::string m_mimeType;
|
||||
const std::string m_template;
|
||||
kainjow::mustache::data m_data;
|
||||
std::unique_ptr<TaskbarInfo> m_taskbarInfo;
|
||||
};
|
||||
|
||||
struct HTTPErrorHtmlResponse : ContentResponseBlueprint
|
||||
{
|
||||
HTTPErrorHtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
int httpStatusCode,
|
||||
const std::string& pageTitleMsg,
|
||||
const std::string& headingMsg,
|
||||
const std::string& cssUrl = "");
|
||||
|
||||
using ContentResponseBlueprint::operator+;
|
||||
HTTPErrorHtmlResponse& operator+(const std::string& msg);
|
||||
};
|
||||
|
||||
class UrlNotFoundMsg {};
|
||||
|
||||
extern const UrlNotFoundMsg urlNotFoundMsg;
|
||||
|
||||
struct HTTP404HtmlResponse : HTTPErrorHtmlResponse
|
||||
{
|
||||
HTTP404HtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
using HTTPErrorHtmlResponse::operator+;
|
||||
HTTPErrorHtmlResponse& operator+(UrlNotFoundMsg /*unused*/);
|
||||
};
|
||||
|
||||
class InvalidUrlMsg {};
|
||||
|
||||
extern const InvalidUrlMsg invalidUrlMsg;
|
||||
|
||||
struct HTTP400HtmlResponse : HTTPErrorHtmlResponse
|
||||
{
|
||||
HTTP400HtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
using HTTPErrorHtmlResponse::operator+;
|
||||
HTTPErrorHtmlResponse& operator+(InvalidUrlMsg /*unused*/);
|
||||
};
|
||||
|
||||
struct HTTP500HtmlResponse : HTTPErrorHtmlResponse
|
||||
{
|
||||
HTTP500HtmlResponse(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
private: // overrides
|
||||
// generateResponseObject() is overriden in order to produce a minimal
|
||||
// response without any need for additional resources from the server
|
||||
std::unique_ptr<ContentResponse> generateResponseObject() const override;
|
||||
};
|
||||
|
||||
class ItemResponse : public Response {
|
||||
public:
|
||||
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||
|
||||
@@ -125,6 +125,30 @@ unsigned int getArchiveMediaCount(const zim::Archive& archive) {
|
||||
return counter;
|
||||
}
|
||||
|
||||
unsigned int getArchiveArticleCount(const zim::Archive& archive) {
|
||||
// [HACK]
|
||||
// getArticleCount() returns different things depending of the "version" of the zim.
|
||||
// On old zim (<=6), it returns the number of entry in `A` namespace
|
||||
// On recent zim (>=7), it returns:
|
||||
// - the number of entry in `C` namespace (==getEntryCount) if no frontArticleIndex is present
|
||||
// - the number of front article if a frontArticleIndex is present
|
||||
// The use case >=7 without frontArticleIndex is pretty rare so we don't care
|
||||
// We can detect if we are reading a zim <= 6 by checking if we have a newNamespaceScheme.
|
||||
if (archive.hasNewNamespaceScheme()) {
|
||||
//The articleCount is "good"
|
||||
return archive.getArticleCount();
|
||||
} else {
|
||||
// We have to parse the `M/Counter` metadata
|
||||
unsigned int counter = 0;
|
||||
for(const auto& pair:parseArchiveCounter(archive)) {
|
||||
if (startsWith(pair.first, "text/html")) {
|
||||
counter += pair.second;
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int getArchiveFileSize(const zim::Archive& archive) {
|
||||
return archive.getFilesize() / 1024;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace kiwix
|
||||
std::string& content, std::string& mimeType);
|
||||
|
||||
unsigned int getArchiveMediaCount(const zim::Archive& archive);
|
||||
unsigned int getArchiveArticleCount(const zim::Archive& archive);
|
||||
unsigned int getArchiveFileSize(const zim::Archive& archive);
|
||||
|
||||
zim::Item getFinalItem(const zim::Archive& archive, const zim::Entry& entry);
|
||||
|
||||
@@ -370,6 +370,13 @@ std::string kiwix::gen_uuid(const std::string& s)
|
||||
return kiwix::to_string(zim::Uuid::generate(s));
|
||||
}
|
||||
|
||||
kainjow::mustache::data kiwix::onlyAsNonEmptyMustacheValue(const std::string& s)
|
||||
{
|
||||
return s.empty()
|
||||
? kainjow::mustache::data(false)
|
||||
: kainjow::mustache::data(s);
|
||||
}
|
||||
|
||||
std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data)
|
||||
{
|
||||
kainjow::mustache::mustache tmpl(template_str);
|
||||
|
||||
@@ -48,6 +48,10 @@ namespace kiwix
|
||||
std::string gen_date_str();
|
||||
std::string gen_uuid(const std::string& s);
|
||||
|
||||
// if s is empty then returns kainjow::mustache::data(false)
|
||||
// otherwise kainjow::mustache::data(value)
|
||||
kainjow::mustache::data onlyAsNonEmptyMustacheValue(const std::string& s);
|
||||
|
||||
std::string render_template(const std::string& template_str, kainjow::mustache::data data);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ skin/fonts/Roboto.ttf
|
||||
skin/block_external.js
|
||||
skin/search_results.css
|
||||
templates/search_result.html
|
||||
templates/no_search_result.html
|
||||
templates/404.html
|
||||
templates/500.html
|
||||
templates/error.html
|
||||
templates/index.html
|
||||
templates/suggestion.json
|
||||
templates/head_taskbar.html
|
||||
|
||||
@@ -18,7 +18,7 @@ a:hover {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.header {
|
||||
h1 {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>Content not found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Not Found</h1>
|
||||
{{#url}}
|
||||
<p>
|
||||
The requested URL "{{url}}" was not found on this server.
|
||||
</p>
|
||||
{{/url}}
|
||||
{{#details}}
|
||||
<p>
|
||||
{{{details}}}
|
||||
</p>
|
||||
{{/details}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>Internal Server Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>
|
||||
An internal server error occured. We are sorry about that :/
|
||||
</p>
|
||||
<p>
|
||||
{{ error }}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
18
static/templates/error.html
Normal file
18
static/templates/error.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
{{#CSS_URL}}
|
||||
<link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />
|
||||
{{/CSS_URL}}
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{PAGE_HEADING}}</h1>
|
||||
{{#details}}
|
||||
<p>
|
||||
{{{p}}}
|
||||
</p>
|
||||
{{/details}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>Fulltext search unavailable</title>
|
||||
<link type="text/css" href="{{root}}/skin/search_results.css" rel="Stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">Not found</div>
|
||||
<p>
|
||||
There is no article with the title <b> "{{pattern}}"</b>
|
||||
and the fulltext search engine is not available for this content.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -107,11 +107,11 @@
|
||||
</b> of <b>
|
||||
{{count}}
|
||||
</b> for <b>
|
||||
{{searchPattern}}
|
||||
"{{{searchPattern}}}"
|
||||
</b>
|
||||
{{/hasResults}}
|
||||
{{^hasResults}}
|
||||
No results were found for <b>{{searchPattern}}</b>
|
||||
No results were found for <b>"{{{searchPattern}}}"</b>
|
||||
{{/hasResults}}
|
||||
</div>
|
||||
|
||||
|
||||
BIN
test/data/poor.zim
Normal file
BIN
test/data/poor.zim
Normal file
Binary file not shown.
@@ -29,6 +29,7 @@ if gtest_dep.found() and not meson.is_cross_build()
|
||||
'zimfile.zim',
|
||||
'zimfile&other.zim',
|
||||
'corner_cases.zim',
|
||||
'poor.zim',
|
||||
'library.xml'
|
||||
]
|
||||
foreach file : data_files
|
||||
|
||||
182
test/server.cpp
182
test/server.cpp
@@ -142,6 +142,7 @@ protected:
|
||||
const int PORT = 8001;
|
||||
const ZimFileServer::FilePathCollection ZIMFILES {
|
||||
"./test/zimfile.zim",
|
||||
"./test/poor.zim",
|
||||
"./test/corner_cases.zim"
|
||||
};
|
||||
|
||||
@@ -287,6 +288,22 @@ TEST_F(ServerTest, UncompressibleContentIsNotCompressed)
|
||||
}
|
||||
}
|
||||
|
||||
const char* urls400[] = {
|
||||
"/ROOT/search",
|
||||
"/ROOT/search?content=zimfile",
|
||||
"/ROOT/search?content=non-existing-book&pattern=asdfqwerty",
|
||||
"/ROOT/search?content=non-existing-book&pattern=asd<qwerty",
|
||||
"/ROOT/search?pattern"
|
||||
};
|
||||
|
||||
|
||||
TEST_F(ServerTest, 400)
|
||||
{
|
||||
for (const char* url: urls400 ) {
|
||||
EXPECT_EQ(400, zfs1_->GET(url)->status) << "url: " << url;
|
||||
}
|
||||
}
|
||||
|
||||
const char* urls404[] = {
|
||||
"/",
|
||||
"/zimfile",
|
||||
@@ -302,8 +319,6 @@ const char* urls404[] = {
|
||||
"/ROOT/meta?content=non-existent-book&name=title",
|
||||
"/ROOT/random",
|
||||
"/ROOT/random?content=non-existent-book",
|
||||
"/ROOT/search",
|
||||
"/ROOT/search?content=non-existing-book&pattern=asdfqwerty",
|
||||
"/ROOT/suggest",
|
||||
"/ROOT/suggest?content=non-existent-book&term=abcd",
|
||||
"/ROOT/catch/external",
|
||||
@@ -319,8 +334,9 @@ const char* urls404[] = {
|
||||
|
||||
TEST_F(ServerTest, 404)
|
||||
{
|
||||
for ( const char* url : urls404 )
|
||||
for ( const char* url : urls404 ) {
|
||||
EXPECT_EQ(404, zfs1_->GET(url)->status) << "url: " << url;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestingOfHtmlResponses
|
||||
@@ -388,13 +404,14 @@ public:
|
||||
: ExpectedResponseData(erd)
|
||||
, url(url)
|
||||
{}
|
||||
virtual ~TestContentIn404HtmlResponse() = default;
|
||||
|
||||
const std::string url;
|
||||
|
||||
std::string expectedResponse() const;
|
||||
|
||||
private:
|
||||
std::string pageTitle() const;
|
||||
virtual std::string pageTitle() const;
|
||||
std::string pageCssLink() const;
|
||||
std::string hiddenBookNameInput() const;
|
||||
std::string searchPatternInput() const;
|
||||
@@ -413,7 +430,8 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||
R"FRAG(</title>
|
||||
)FRAG",
|
||||
|
||||
R"FRAG( <link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/jquery-ui/jquery-ui.min.css" rel="Stylesheet" />
|
||||
R"FRAG(
|
||||
<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/jquery-ui/jquery-ui.min.css" rel="Stylesheet" />
|
||||
<link type="text/css" href="/ROOT/skin/jquery-ui/jquery-ui.theme.min.css" rel="Stylesheet" />
|
||||
<link type="text/css" href="/ROOT/skin/taskbar.css" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="/ROOT/skin/jquery-ui/external/jquery/jquery.js" defer></script>
|
||||
@@ -480,8 +498,7 @@ std::string TestContentIn404HtmlResponse::pageCssLink() const
|
||||
|
||||
return R"( <link type="text/css" href=")"
|
||||
+ expectedCssUrl
|
||||
+ R"(" rel="Stylesheet" />
|
||||
)";
|
||||
+ R"(" rel="Stylesheet" />)";
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::hiddenBookNameInput() const
|
||||
@@ -521,6 +538,25 @@ std::string TestContentIn404HtmlResponse::taskbarLinks() const
|
||||
+ R"("><button>🎲</button></a>)";
|
||||
}
|
||||
|
||||
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
|
||||
{
|
||||
public:
|
||||
TestContentIn400HtmlResponse(const std::string& url,
|
||||
const ExpectedResponseData& erd)
|
||||
: TestContentIn404HtmlResponse(url, erd)
|
||||
{}
|
||||
|
||||
private:
|
||||
std::string pageTitle() const;
|
||||
};
|
||||
|
||||
std::string TestContentIn400HtmlResponse::pageTitle() const {
|
||||
return expectedPageTitle.empty()
|
||||
? "Invalid request"
|
||||
: expectedPageTitle;
|
||||
}
|
||||
|
||||
|
||||
} // namespace TestingOfHtmlResponses
|
||||
|
||||
TEST_F(ServerTest, 404WithBodyTesting)
|
||||
@@ -530,7 +566,6 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
{ /* url */ "/ROOT/random?content=non-existent-book",
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
//EOLWHITESPACEMARKER
|
||||
<p>
|
||||
No such book: non-existent-book
|
||||
</p>
|
||||
@@ -539,7 +574,6 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
{ /* url */ "/ROOT/suggest?content=no-such-book&term=whatever",
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
//EOLWHITESPACEMARKER
|
||||
<p>
|
||||
No such book: no-such-book
|
||||
</p>
|
||||
@@ -551,9 +585,6 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
<p>
|
||||
The requested URL "/ROOT/catalog/" was not found on this server.
|
||||
</p>
|
||||
<p>
|
||||
//EOLWHITESPACEMARKER
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/catalog/invalid_endpoint",
|
||||
@@ -562,9 +593,6 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
<p>
|
||||
The requested URL "/ROOT/catalog/invalid_endpoint" was not found on this server.
|
||||
</p>
|
||||
<p>
|
||||
//EOLWHITESPACEMARKER
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/invalid-book/whatever",
|
||||
@@ -638,8 +666,6 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/raw/zimfile/meta/invalid-metadata",
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -651,8 +677,6 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/raw/zimfile/content/invalid-article",
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -663,27 +687,15 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/search?content=zimfile",
|
||||
{ /* url */ "/ROOT/search?content=poor&pattern=whatever",
|
||||
expected_page_title=="Fulltext search unavailable" &&
|
||||
expected_css_url=="/ROOT/skin/search_results.css" &&
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
book_name=="poor" &&
|
||||
book_title=="poor" &&
|
||||
expected_body==R"(
|
||||
<div class="header">Not found</div>
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
There is no article with the title <b> ""</b>
|
||||
and the fulltext search engine is not available for this content.
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/search?content=non-existent-book&pattern=asdfqwerty",
|
||||
expected_page_title=="Fulltext search unavailable" &&
|
||||
expected_css_url=="/ROOT/skin/search_results.css" &&
|
||||
expected_body==R"(
|
||||
<div class="header">Not found</div>
|
||||
<p>
|
||||
There is no article with the title <b> "asdfqwerty"</b>
|
||||
and the fulltext search engine is not available for this content.
|
||||
The fulltext search engine is not available for this content.
|
||||
</p>
|
||||
)" },
|
||||
};
|
||||
@@ -696,6 +708,98 @@ TEST_F(ServerTest, 404WithBodyTesting)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, 400WithBodyTesting)
|
||||
{
|
||||
using namespace TestingOfHtmlResponses;
|
||||
const std::vector<TestContentIn400HtmlResponse> testData{
|
||||
{ /* url */ "/ROOT/search",
|
||||
expected_body== R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
The requested URL "/ROOT/search" is not a valid request.
|
||||
</p>
|
||||
<p>
|
||||
No query provided.
|
||||
</p>
|
||||
)" },
|
||||
{ /* url */ "/ROOT/search?content=zimfile",
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
The requested URL "/ROOT/search?content=zimfile" is not a valid request.
|
||||
</p>
|
||||
<p>
|
||||
No query provided.
|
||||
</p>
|
||||
)" },
|
||||
{ /* url */ "/ROOT/search?content=non-existing-book&pattern=asdfqwerty",
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
The requested URL "/ROOT/search?content=non-existing-book&pattern=asdfqwerty" is not a valid request.
|
||||
</p>
|
||||
<p>
|
||||
The requested book doesn't exist.
|
||||
</p>
|
||||
)" },
|
||||
{ /* url */ "/ROOT/search?content=non-existing-book&pattern=a\"<script foo>",
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
The requested URL "/ROOT/search?content=non-existing-book&pattern=a"<script foo>" is not a valid request.
|
||||
</p>
|
||||
<p>
|
||||
The requested book doesn't exist.
|
||||
</p>
|
||||
)" },
|
||||
// There is a flaw in our way to handle query string, we cannot differenciate
|
||||
// between `pattern` and `pattern=`
|
||||
{ /* url */ "/ROOT/search?pattern",
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
The requested URL "/ROOT/search?pattern=" is not a valid request.
|
||||
</p>
|
||||
<p>
|
||||
No query provided.
|
||||
</p>
|
||||
)" },
|
||||
};
|
||||
|
||||
for ( const auto& t : testData ) {
|
||||
const TestContext ctx{ {"url", t.url} };
|
||||
const auto r = zfs1_->GET(t.url.c_str());
|
||||
EXPECT_EQ(r->status, 400) << ctx;
|
||||
EXPECT_EQ(r->body, t.expectedResponse()) << ctx;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, 500)
|
||||
{
|
||||
const std::string expectedBody = R"(<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>Internal Server Error</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>
|
||||
An internal server error occured. We are sorry about that :/
|
||||
</p>
|
||||
<p>
|
||||
Entry redirect_loop.html is a redirect entry.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
|
||||
const auto r = zfs1_->GET("/ROOT/poor/A/redirect_loop.html");
|
||||
EXPECT_EQ(r->status, 500);
|
||||
EXPECT_EQ(r->body, expectedBody);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle)
|
||||
{
|
||||
auto g = zfs1_->GET("/ROOT/random?content=zimfile");
|
||||
@@ -740,14 +844,16 @@ TEST_F(ServerTest, RawEntry)
|
||||
|
||||
TEST_F(ServerTest, HeadMethodIsSupported)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() )
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
EXPECT_EQ(200, zfs1_->HEAD(res.url)->status) << res;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, TheResponseToHeadRequestHasNoBody)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() )
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
EXPECT_TRUE(zfs1_->HEAD(res.url)->body.empty()) << res;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, HeadersAreTheSameInResponsesToHeadAndGetRequests)
|
||||
|
||||
Reference in New Issue
Block a user