mirror of https://github.com/kiwix/libkiwix.git
Merge pull request #716 from kiwix/iframe_based_content_viewer
Iframe-based content viewer
This commit is contained in:
commit
3a75facfdc
|
@ -553,9 +553,12 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||||
if (url == "/" )
|
if (url == "/" )
|
||||||
return build_homepage(request);
|
return build_homepage(request);
|
||||||
|
|
||||||
if (isEndpointUrl(url, "skin"))
|
if (isEndpointUrl(url, "viewer") || isEndpointUrl(url, "skin"))
|
||||||
return handle_skin(request);
|
return handle_skin(request);
|
||||||
|
|
||||||
|
if (url == "/viewer_settings.js")
|
||||||
|
return handle_viewer_settings(request);
|
||||||
|
|
||||||
if (isEndpointUrl(url, "content"))
|
if (isEndpointUrl(url, "content"))
|
||||||
return handle_content(request);
|
return handle_content(request);
|
||||||
|
|
||||||
|
@ -623,7 +626,7 @@ InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
|
||||||
|
|
||||||
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
|
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
|
||||||
{
|
{
|
||||||
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8", true);
|
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -653,8 +656,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||||
|
|
||||||
if (archive == nullptr) {
|
if (archive == nullptr) {
|
||||||
return HTTP404Response(*this, request)
|
return HTTP404Response(*this, request)
|
||||||
+ noSuchBookErrorMsg(bookName)
|
+ noSuchBookErrorMsg(bookName);
|
||||||
+ TaskbarInfo(bookName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto queryString = request.get_optional_param("term", std::string());
|
const auto queryString = request.get_optional_param("term", std::string());
|
||||||
|
@ -714,13 +716,30 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||||
return std::move(response);
|
return std::move(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_viewer_settings\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const kainjow::mustache::object data{
|
||||||
|
{"enable_toolbar", m_withTaskbar ? "true" : "false" },
|
||||||
|
{"enable_link_blocking", m_blockExternalLinks ? "true" : "false" },
|
||||||
|
{"enable_library_button", m_withLibraryButton ? "true" : "false" }
|
||||||
|
};
|
||||||
|
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& request)
|
std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& request)
|
||||||
{
|
{
|
||||||
if (m_verbose.load()) {
|
if (m_verbose.load()) {
|
||||||
printf("** running handle_skin\n");
|
printf("** running handle_skin\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto resourceName = request.get_url().substr(1);
|
const bool isRequestForViewer = request.get_url() == "/viewer";
|
||||||
|
auto resourceName = isRequestForViewer
|
||||||
|
? "viewer.html"
|
||||||
|
: request.get_url().substr(1);
|
||||||
try {
|
try {
|
||||||
auto response = ContentResponse::build(
|
auto response = ContentResponse::build(
|
||||||
*this,
|
*this,
|
||||||
|
@ -777,11 +796,15 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||||
"404-page-heading",
|
"404-page-heading",
|
||||||
cssUrl);
|
cssUrl);
|
||||||
response += nonParameterizedMessage("no-search-results");
|
response += nonParameterizedMessage("no-search-results");
|
||||||
|
// XXX: Now this has to be handled by the iframe-based viewer which
|
||||||
|
// XXX: has to resolve if the book selection resulted in a single book.
|
||||||
|
/*
|
||||||
if(bookIds.size() == 1) {
|
if(bookIds.size() == 1) {
|
||||||
auto bookId = *bookIds.begin();
|
auto bookId = *bookIds.begin();
|
||||||
auto bookName = mp_nameMapper->getNameForId(bookId);
|
auto bookName = mp_nameMapper->getNameForId(bookId);
|
||||||
response += TaskbarInfo(bookName, mp_library->getArchiveById(bookId).get());
|
response += TaskbarInfo(bookName, mp_library->getArchiveById(bookId).get());
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -811,16 +834,18 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||||
renderer.setSearchProtocolPrefix(m_root + "/search");
|
renderer.setSearchProtocolPrefix(m_root + "/search");
|
||||||
renderer.setPageLength(pageLength);
|
renderer.setPageLength(pageLength);
|
||||||
if (request.get_requested_format() == "xml") {
|
if (request.get_requested_format() == "xml") {
|
||||||
return ContentResponse::build(*this, renderer.getXml(), "application/rss+xml; charset=utf-8",
|
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");
|
||||||
|
// XXX: Now this has to be handled by the iframe-based viewer which
|
||||||
|
// XXX: has to resolve if the book selection resulted in a single book.
|
||||||
|
/*
|
||||||
if(bookIds.size() == 1) {
|
if(bookIds.size() == 1) {
|
||||||
auto bookId = *bookIds.begin();
|
auto bookId = *bookIds.begin();
|
||||||
auto bookName = mp_nameMapper->getNameForId(bookId);
|
auto bookName = mp_nameMapper->getNameForId(bookId);
|
||||||
response->set_taskbar(bookName, mp_library->getArchiveById(bookId).get());
|
response->set_taskbar(bookName, mp_library->getArchiveById(bookId).get());
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return std::move(response);
|
return std::move(response);
|
||||||
} catch (const Error& e) {
|
} catch (const Error& e) {
|
||||||
return HTTP400Response(*this, request)
|
return HTTP400Response(*this, request)
|
||||||
|
@ -852,8 +877,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||||
|
|
||||||
if (archive == nullptr) {
|
if (archive == nullptr) {
|
||||||
return HTTP404Response(*this, request)
|
return HTTP404Response(*this, request)
|
||||||
+ noSuchBookErrorMsg(bookName)
|
+ noSuchBookErrorMsg(bookName);
|
||||||
+ TaskbarInfo(bookName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -861,8 +885,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||||
} catch(zim::EntryNotFound& e) {
|
} catch(zim::EntryNotFound& e) {
|
||||||
return HTTP404Response(*this, request)
|
return HTTP404Response(*this, request)
|
||||||
+ nonParameterizedMessage("random-article-failure")
|
+ nonParameterizedMessage("random-article-failure");
|
||||||
+ TaskbarInfo(bookName, archive.get());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1010,8 +1033,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||||
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 HTTP404Response(*this, request)
|
return HTTP404Response(*this, request)
|
||||||
+ urlNotFoundMsg
|
+ urlNotFoundMsg
|
||||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
|
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||||
+ TaskbarInfo(bookName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto urlStr = url.substr(prefixLength + bookName.size());
|
auto urlStr = url.substr(prefixLength + bookName.size());
|
||||||
|
@ -1027,9 +1049,6 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||||
}
|
}
|
||||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||||
try {
|
|
||||||
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, archive.get());
|
|
||||||
} catch (std::bad_cast& e) {}
|
|
||||||
|
|
||||||
if (m_verbose.load()) {
|
if (m_verbose.load()) {
|
||||||
printf("Found %s\n", entry.getPath().c_str());
|
printf("Found %s\n", entry.getPath().c_str());
|
||||||
|
@ -1044,8 +1063,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||||
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 HTTP404Response(*this, request)
|
return HTTP404Response(*this, request)
|
||||||
+ urlNotFoundMsg
|
+ urlNotFoundMsg
|
||||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
|
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||||
+ TaskbarInfo(bookName, archive.get());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1093,13 +1111,13 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||||
try {
|
try {
|
||||||
if (kind == "meta") {
|
if (kind == "meta") {
|
||||||
auto item = archive->getMetadataItem(itemPath);
|
auto item = archive->getMetadataItem(itemPath);
|
||||||
return ItemResponse::build(*this, request, item, /*raw=*/true);
|
return ItemResponse::build(*this, request, item);
|
||||||
} else {
|
} else {
|
||||||
auto entry = archive->getEntryByPath(itemPath);
|
auto entry = archive->getEntryByPath(itemPath);
|
||||||
if (entry.isRedirect()) {
|
if (entry.isRedirect()) {
|
||||||
return build_redirect(bookName, entry.getItem(true));
|
return build_redirect(bookName, entry.getItem(true));
|
||||||
}
|
}
|
||||||
return ItemResponse::build(*this, request, entry.getItem(), /*raw=*/true);
|
return ItemResponse::build(*this, request, entry.getItem());
|
||||||
}
|
}
|
||||||
} catch (zim::EntryNotFound& e ) {
|
} catch (zim::EntryNotFound& e ) {
|
||||||
if (m_verbose.load()) {
|
if (m_verbose.load()) {
|
||||||
|
@ -1136,9 +1154,7 @@ std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(con
|
||||||
|
|
||||||
return ContentResponse::build(*this,
|
return ContentResponse::build(*this,
|
||||||
resourceData,
|
resourceData,
|
||||||
crd.mimeType,
|
crd.mimeType);
|
||||||
/*isHomePage=*/false,
|
|
||||||
/*raw=*/true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,6 +126,7 @@ class InternalServer {
|
||||||
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
||||||
std::unique_ptr<Response> build_redirect(const std::string& bookName, const zim::Item& item) const;
|
std::unique_ptr<Response> build_redirect(const std::string& bookName, const zim::Item& item) const;
|
||||||
std::unique_ptr<Response> build_homepage(const RequestContext& request);
|
std::unique_ptr<Response> build_homepage(const RequestContext& request);
|
||||||
|
std::unique_ptr<Response> handle_viewer_settings(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_skin(const RequestContext& request);
|
std::unique_ptr<Response> handle_skin(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
|
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_catalog_v2(const RequestContext& request);
|
std::unique_ptr<Response> handle_catalog_v2(const RequestContext& request);
|
||||||
|
@ -183,8 +184,8 @@ class InternalServer {
|
||||||
std::unique_ptr<CustomizedResources> m_customizedResources;
|
std::unique_ptr<CustomizedResources> m_customizedResources;
|
||||||
|
|
||||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
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<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype);
|
||||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw);
|
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
|
||||||
auto book = mp_library->getBookByIdThreadSafe(bookId);
|
auto book = mp_library->getBookByIdThreadSafe(bookId);
|
||||||
auto size = request.get_argument<unsigned int>("size");
|
auto size = request.get_argument<unsigned int>("size");
|
||||||
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 HTTP404Response(*this, request)
|
return HTTP404Response(*this, request)
|
||||||
+ urlNotFoundMsg;
|
+ urlNotFoundMsg;
|
||||||
|
|
|
@ -140,9 +140,6 @@ std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObjec
|
||||||
{
|
{
|
||||||
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
|
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
|
||||||
r->set_code(m_httpStatusCode);
|
r->set_code(m_httpStatusCode);
|
||||||
if ( m_taskbarInfo ) {
|
|
||||||
r->set_taskbar(m_taskbarInfo->bookName, m_taskbarInfo->archive);
|
|
||||||
}
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,29 +233,12 @@ HTTP500Response::HTTP500Response(const InternalServer& server,
|
||||||
|
|
||||||
std::unique_ptr<ContentResponse> HTTP500Response::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
|
const std::string mimeType = "text/html;charset=utf-8";
|
||||||
// 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);
|
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
|
||||||
r->set_code(m_httpStatusCode);
|
r->set_code(m_httpStatusCode);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentResponseBlueprint& ContentResponseBlueprint::operator+(const TaskbarInfo& taskbarInfo)
|
|
||||||
{
|
|
||||||
this->m_taskbarInfo.reset(new TaskbarInfo(taskbarInfo));
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentResponseBlueprint& ContentResponseBlueprint::operator+=(const TaskbarInfo& taskbarInfo)
|
|
||||||
{
|
|
||||||
// operator+() is already a state-modifying operator (akin to operator+=)
|
|
||||||
return *this + taskbarInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
|
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
|
||||||
{
|
{
|
||||||
auto response = Response::build(server);
|
auto response = Response::build(server);
|
||||||
|
@ -337,52 +317,6 @@ void print_response_info(int retCode, MHD_Response* response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ContentResponse::introduce_taskbar(const std::string& lang)
|
|
||||||
{
|
|
||||||
i18n::GetTranslatedString t(lang);
|
|
||||||
kainjow::mustache::object data{
|
|
||||||
{"root", m_root},
|
|
||||||
{"content", m_bookName},
|
|
||||||
{"hascontent", (!m_bookName.empty() && !m_bookTitle.empty())},
|
|
||||||
{"title", m_bookTitle},
|
|
||||||
{"withlibrarybutton", m_withLibraryButton},
|
|
||||||
{"LIBRARY_BUTTON_TEXT", t("library-button-text")},
|
|
||||||
{"HOME_BUTTON_TEXT", t("home-button-text", {{"BOOK_TITLE", m_bookTitle}}) },
|
|
||||||
{"RANDOM_PAGE_BUTTON_TEXT", t("random-page-button-text") },
|
|
||||||
{"SEARCHBOX_TOOLTIP", t("searchbox-tooltip", {{"BOOK_TITLE", m_bookTitle}}) },
|
|
||||||
};
|
|
||||||
auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data);
|
|
||||||
m_content = prependToFirstOccurence(
|
|
||||||
m_content,
|
|
||||||
"</head[ \\t]*>",
|
|
||||||
head_content);
|
|
||||||
|
|
||||||
auto taskbar_part = render_template(RESOURCE::templates::taskbar_part_html, data);
|
|
||||||
m_content = appendToFirstOccurence(
|
|
||||||
m_content,
|
|
||||||
"<body[^>]*>",
|
|
||||||
taskbar_part);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void ContentResponse::inject_externallinks_blocker()
|
|
||||||
{
|
|
||||||
kainjow::mustache::data data;
|
|
||||||
data.set("root", m_root);
|
|
||||||
auto script_tag = render_template(RESOURCE::templates::external_blocker_part_html, data);
|
|
||||||
m_content = prependToFirstOccurence(
|
|
||||||
m_content,
|
|
||||||
"</head[ \\t]*>",
|
|
||||||
script_tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ContentResponse::inject_root_link(){
|
|
||||||
m_content = prependToFirstOccurence(
|
|
||||||
m_content,
|
|
||||||
"</head[ \\t]*>",
|
|
||||||
"<link type=\"root\" href=\"" + m_root + "\">");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
ContentResponse::can_compress(const RequestContext& request) const
|
ContentResponse::can_compress(const RequestContext& request) const
|
||||||
{
|
{
|
||||||
|
@ -391,16 +325,6 @@ ContentResponse::can_compress(const RequestContext& request) const
|
||||||
&& (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_COMPRESS);
|
&& (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_COMPRESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
|
||||||
ContentResponse::contentDecorationAllowed() const
|
|
||||||
{
|
|
||||||
if (m_raw) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (startsWith(m_mimeType, "text/html")
|
|
||||||
&& m_mimeType.find(";raw=true") == std::string::npos);
|
|
||||||
}
|
|
||||||
|
|
||||||
MHD_Response*
|
MHD_Response*
|
||||||
Response::create_mhd_response(const RequestContext& request)
|
Response::create_mhd_response(const RequestContext& request)
|
||||||
{
|
{
|
||||||
|
@ -411,17 +335,6 @@ Response::create_mhd_response(const RequestContext& request)
|
||||||
MHD_Response*
|
MHD_Response*
|
||||||
ContentResponse::create_mhd_response(const RequestContext& request)
|
ContentResponse::create_mhd_response(const RequestContext& request)
|
||||||
{
|
{
|
||||||
if (contentDecorationAllowed()) {
|
|
||||||
inject_root_link();
|
|
||||||
|
|
||||||
if (m_withTaskbar) {
|
|
||||||
introduce_taskbar(request.get_user_language());
|
|
||||||
}
|
|
||||||
if (m_blockExternalLinks) {
|
|
||||||
inject_externallinks_blocker();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool isCompressed = can_compress(request) && compress(m_content);
|
const bool isCompressed = can_compress(request) && compress(m_content);
|
||||||
|
|
||||||
MHD_Response* response = MHD_create_response_from_buffer(
|
MHD_Response* response = MHD_create_response_from_buffer(
|
||||||
|
@ -461,24 +374,11 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ContentResponse::set_taskbar(const std::string& bookName, const zim::Archive* archive)
|
ContentResponse::ContentResponse(const std::string& root, bool verbose, const std::string& content, const std::string& mimetype) :
|
||||||
{
|
|
||||||
m_bookName = bookName;
|
|
||||||
m_bookTitle = archive ? getArchiveTitle(*archive) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ContentResponse::ContentResponse(const std::string& root, bool verbose, bool raw, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype) :
|
|
||||||
Response(verbose),
|
Response(verbose),
|
||||||
m_root(root),
|
m_root(root),
|
||||||
m_content(content),
|
m_content(content),
|
||||||
m_mimeType(mimetype),
|
m_mimeType(mimetype)
|
||||||
m_raw(raw),
|
|
||||||
m_withTaskbar(withTaskbar),
|
|
||||||
m_withLibraryButton(withLibraryButton),
|
|
||||||
m_blockExternalLinks(blockExternalLinks),
|
|
||||||
m_bookName(""),
|
|
||||||
m_bookTitle("")
|
|
||||||
{
|
{
|
||||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||||
}
|
}
|
||||||
|
@ -486,17 +386,11 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, bool raw
|
||||||
std::unique_ptr<ContentResponse> ContentResponse::build(
|
std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||||
const InternalServer& server,
|
const InternalServer& server,
|
||||||
const std::string& content,
|
const std::string& content,
|
||||||
const std::string& mimetype,
|
const std::string& mimetype)
|
||||||
bool isHomePage,
|
|
||||||
bool raw)
|
|
||||||
{
|
{
|
||||||
return std::unique_ptr<ContentResponse>(new ContentResponse(
|
return std::unique_ptr<ContentResponse>(new ContentResponse(
|
||||||
server.m_root,
|
server.m_root,
|
||||||
server.m_verbose.load(),
|
server.m_verbose.load(),
|
||||||
raw,
|
|
||||||
server.m_withTaskbar && !isHomePage,
|
|
||||||
server.m_withLibraryButton,
|
|
||||||
server.m_blockExternalLinks,
|
|
||||||
content,
|
content,
|
||||||
mimetype));
|
mimetype));
|
||||||
}
|
}
|
||||||
|
@ -505,11 +399,10 @@ std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||||
const InternalServer& server,
|
const InternalServer& server,
|
||||||
const std::string& template_str,
|
const std::string& template_str,
|
||||||
kainjow::mustache::data data,
|
kainjow::mustache::data data,
|
||||||
const std::string& mimetype,
|
const std::string& mimetype)
|
||||||
bool isHomePage)
|
|
||||||
{
|
{
|
||||||
auto content = render_template(template_str, data);
|
auto content = render_template(template_str, data);
|
||||||
return ContentResponse::build(server, content, mimetype, isHomePage);
|
return ContentResponse::build(server, content, mimetype);
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
||||||
|
@ -522,14 +415,14 @@ ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::strin
|
||||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw)
|
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item)
|
||||||
{
|
{
|
||||||
const std::string mimetype = get_mime_type(item);
|
const std::string mimetype = get_mime_type(item);
|
||||||
auto byteRange = request.get_range().resolve(item.getSize());
|
auto byteRange = request.get_range().resolve(item.getSize());
|
||||||
const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
|
const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
|
||||||
if (noRange && is_compressible_mime_type(mimetype)) {
|
if (noRange && is_compressible_mime_type(mimetype)) {
|
||||||
// Return a contentResponse
|
// Return a contentResponse
|
||||||
auto response = ContentResponse::build(server, item.getData(), mimetype, /*isHomePage=*/false, raw);
|
auto response = ContentResponse::build(server, item.getData(), mimetype);
|
||||||
response->set_cacheable();
|
response->set_cacheable();
|
||||||
response->m_byteRange = byteRange;
|
response->m_byteRange = byteRange;
|
||||||
return std::move(response);
|
return std::move(response);
|
||||||
|
|
|
@ -83,60 +83,32 @@ class ContentResponse : public Response {
|
||||||
ContentResponse(
|
ContentResponse(
|
||||||
const std::string& root,
|
const std::string& root,
|
||||||
bool verbose,
|
bool verbose,
|
||||||
bool raw,
|
|
||||||
bool withTaskbar,
|
|
||||||
bool withLibraryButton,
|
|
||||||
bool blockExternalLinks,
|
|
||||||
const std::string& content,
|
const std::string& content,
|
||||||
const std::string& mimetype);
|
const std::string& mimetype);
|
||||||
|
|
||||||
static std::unique_ptr<ContentResponse> build(
|
static std::unique_ptr<ContentResponse> build(
|
||||||
const InternalServer& server,
|
const InternalServer& server,
|
||||||
const std::string& content,
|
const std::string& content,
|
||||||
const std::string& mimetype,
|
const std::string& mimetype);
|
||||||
bool isHomePage = false,
|
|
||||||
bool raw = false);
|
|
||||||
static std::unique_ptr<ContentResponse> build(
|
static std::unique_ptr<ContentResponse> build(
|
||||||
const InternalServer& server,
|
const InternalServer& server,
|
||||||
const std::string& template_str,
|
const std::string& template_str,
|
||||||
kainjow::mustache::data data,
|
kainjow::mustache::data data,
|
||||||
const std::string& mimetype,
|
const std::string& mimetype);
|
||||||
bool isHomePage = false);
|
|
||||||
|
|
||||||
void set_taskbar(const std::string& bookName, const zim::Archive* archive);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||||
|
|
||||||
void introduce_taskbar(const std::string& lang);
|
|
||||||
void inject_externallinks_blocker();
|
|
||||||
void inject_root_link();
|
|
||||||
bool can_compress(const RequestContext& request) const;
|
bool can_compress(const RequestContext& request) const;
|
||||||
bool contentDecorationAllowed() const;
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_root;
|
std::string m_root;
|
||||||
std::string m_content;
|
std::string m_content;
|
||||||
std::string m_mimeType;
|
std::string m_mimeType;
|
||||||
bool m_raw;
|
|
||||||
bool m_withTaskbar;
|
|
||||||
bool m_withLibraryButton;
|
|
||||||
bool m_blockExternalLinks;
|
|
||||||
std::string m_bookName;
|
|
||||||
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
|
class ContentResponseBlueprint
|
||||||
{
|
{
|
||||||
public: // functions
|
public: // functions
|
||||||
|
@ -165,9 +137,6 @@ public: // functions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ContentResponseBlueprint& operator+(const TaskbarInfo& taskbarInfo);
|
|
||||||
ContentResponseBlueprint& operator+=(const TaskbarInfo& taskbarInfo);
|
|
||||||
|
|
||||||
protected: // functions
|
protected: // functions
|
||||||
std::string getMessage(const std::string& msgId) const;
|
std::string getMessage(const std::string& msgId) const;
|
||||||
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
|
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
|
||||||
|
@ -179,7 +148,6 @@ public: //data
|
||||||
const std::string m_mimeType;
|
const std::string m_mimeType;
|
||||||
const std::string m_template;
|
const std::string m_template;
|
||||||
kainjow::mustache::data m_data;
|
kainjow::mustache::data m_data;
|
||||||
std::unique_ptr<TaskbarInfo> m_taskbarInfo;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HTTPErrorResponse : ContentResponseBlueprint
|
struct HTTPErrorResponse : ContentResponseBlueprint
|
||||||
|
@ -191,8 +159,6 @@ struct HTTPErrorResponse : ContentResponseBlueprint
|
||||||
const std::string& headingMsgId,
|
const std::string& headingMsgId,
|
||||||
const std::string& cssUrl = "");
|
const std::string& cssUrl = "");
|
||||||
|
|
||||||
using ContentResponseBlueprint::operator+;
|
|
||||||
using ContentResponseBlueprint::operator+=;
|
|
||||||
HTTPErrorResponse& operator+(const std::string& msg);
|
HTTPErrorResponse& operator+(const std::string& msg);
|
||||||
HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
|
HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
|
||||||
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
|
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
|
||||||
|
@ -238,7 +204,7 @@ private: // overrides
|
||||||
class ItemResponse : public Response {
|
class ItemResponse : public Response {
|
||||||
public:
|
public:
|
||||||
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||||
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw = false);
|
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||||
|
|
|
@ -75,41 +75,3 @@ std::string replaceRegex(const std::string& content,
|
||||||
uresult.toUTF8String(tmp);
|
uresult.toUTF8String(tmp);
|
||||||
return tmp;
|
return tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string appendToFirstOccurence(const std::string& content,
|
|
||||||
const std::string& regex,
|
|
||||||
const std::string& replacement)
|
|
||||||
{
|
|
||||||
ucnv_setDefaultName("UTF-8");
|
|
||||||
icu::UnicodeString ucontent(content.c_str());
|
|
||||||
icu::UnicodeString ureplacement(replacement.c_str());
|
|
||||||
auto matcher = buildMatcher(regex, ucontent);
|
|
||||||
if (matcher->find()) {
|
|
||||||
UErrorCode status = U_ZERO_ERROR;
|
|
||||||
ucontent.insert(matcher->end(status), ureplacement);
|
|
||||||
std::string tmp;
|
|
||||||
ucontent.toUTF8String(tmp);
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string prependToFirstOccurence(const std::string& content,
|
|
||||||
const std::string& regex,
|
|
||||||
const std::string& replacement)
|
|
||||||
{
|
|
||||||
ucnv_setDefaultName("UTF-8");
|
|
||||||
icu::UnicodeString ucontent(content.c_str());
|
|
||||||
icu::UnicodeString ureplacement(replacement.c_str());
|
|
||||||
auto matcher = buildMatcher(regex, ucontent);
|
|
||||||
if (matcher->find()) {
|
|
||||||
UErrorCode status = U_ZERO_ERROR;
|
|
||||||
ucontent.insert(matcher->start(status), ureplacement);
|
|
||||||
std::string tmp;
|
|
||||||
ucontent.toUTF8String(tmp);
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
|
@ -26,11 +26,5 @@ bool matchRegex(const std::string& content, const std::string& regex);
|
||||||
std::string replaceRegex(const std::string& content,
|
std::string replaceRegex(const std::string& content,
|
||||||
const std::string& replacement,
|
const std::string& replacement,
|
||||||
const std::string& regex);
|
const std::string& regex);
|
||||||
std::string appendToFirstOccurence(const std::string& content,
|
|
||||||
const std::string& regex,
|
|
||||||
const std::string& replacement);
|
|
||||||
std::string prependToFirstOccurence(const std::string& content,
|
|
||||||
const std::string& regex,
|
|
||||||
const std::string& replacement);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -4,7 +4,6 @@ skin/magnet.png
|
||||||
skin/download.png
|
skin/download.png
|
||||||
skin/hash.png
|
skin/hash.png
|
||||||
skin/search-icon.svg
|
skin/search-icon.svg
|
||||||
skin/taskbar.js
|
|
||||||
skin/iso6391To3.js
|
skin/iso6391To3.js
|
||||||
skin/isotope.pkgd.min.js
|
skin/isotope.pkgd.min.js
|
||||||
skin/index.js
|
skin/index.js
|
||||||
|
@ -13,17 +12,16 @@ skin/taskbar.css
|
||||||
skin/index.css
|
skin/index.css
|
||||||
skin/fonts/Poppins.ttf
|
skin/fonts/Poppins.ttf
|
||||||
skin/fonts/Roboto.ttf
|
skin/fonts/Roboto.ttf
|
||||||
skin/block_external.js
|
|
||||||
skin/search_results.css
|
skin/search_results.css
|
||||||
|
skin/blank.html
|
||||||
|
skin/viewer.js
|
||||||
|
viewer.html
|
||||||
templates/search_result.html
|
templates/search_result.html
|
||||||
templates/search_result.xml
|
templates/search_result.xml
|
||||||
templates/error.html
|
templates/error.html
|
||||||
templates/error.xml
|
templates/error.xml
|
||||||
templates/index.html
|
templates/index.html
|
||||||
templates/suggestion.json
|
templates/suggestion.json
|
||||||
templates/head_taskbar.html
|
|
||||||
templates/taskbar_part.html
|
|
||||||
templates/external_blocker_part.html
|
|
||||||
templates/captured_external.html
|
templates/captured_external.html
|
||||||
templates/catalog_entries.xml
|
templates/catalog_entries.xml
|
||||||
templates/catalog_v2_root.xml
|
templates/catalog_v2_root.xml
|
||||||
|
@ -32,6 +30,7 @@ templates/catalog_v2_entry.xml
|
||||||
templates/catalog_v2_categories.xml
|
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
|
||||||
|
templates/viewer_settings.js
|
||||||
opensearchdescription.xml
|
opensearchdescription.xml
|
||||||
ft_opensearchdescription.xml
|
ft_opensearchdescription.xml
|
||||||
catalog_v2_searchdescription.xml
|
catalog_v2_searchdescription.xml
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Blank page</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,74 +0,0 @@
|
||||||
const root = document.querySelector( `link[type='root']` ).getAttribute("href");
|
|
||||||
// `block_path` variable used by openzim/warc2zim to detect whether URL blocking is enabled or not
|
|
||||||
var block_path = `${root}/catch/external`;
|
|
||||||
// called only on external links
|
|
||||||
function capture_event(e, target) { target.setAttribute("href", encodeURI(block_path + "?source=" + target.href)); }
|
|
||||||
|
|
||||||
// called on all link clicks. filters external and call capture_event
|
|
||||||
function on_click_event(e) {
|
|
||||||
var target = findParent("a", e.target);
|
|
||||||
if (target !== null && "href" in target) {
|
|
||||||
var href = target.href;
|
|
||||||
if (window.location.pathname.indexOf(block_path) == 0) // already in catch page
|
|
||||||
return;
|
|
||||||
if (href.indexOf(window.location.origin) == 0)
|
|
||||||
return;
|
|
||||||
if (href.substr(0, 2) == "//")
|
|
||||||
return capture_event(e, target);
|
|
||||||
if (href.substr(0, 5) == "http:")
|
|
||||||
return capture_event(e, target);
|
|
||||||
if (href.substr(0, 6) == "https:")
|
|
||||||
return capture_event(e, target);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// script entrypoint (called on document ready)
|
|
||||||
function run() { live('a', 'click', on_click_event); }
|
|
||||||
|
|
||||||
// find first parent with tagname
|
|
||||||
function findParent(tagname, el) {
|
|
||||||
while (el) {
|
|
||||||
if ((el.nodeName || el.tagName).toLowerCase() === tagname.toLowerCase()) {
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
el = el.parentNode;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// matches polyfill
|
|
||||||
this.Element && function(ElementPrototype) {
|
|
||||||
ElementPrototype.matches = ElementPrototype.matches ||
|
|
||||||
ElementPrototype.matchesSelector ||
|
|
||||||
ElementPrototype.webkitMatchesSelector ||
|
|
||||||
ElementPrototype.msMatchesSelector ||
|
|
||||||
function(selector) {
|
|
||||||
var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
|
|
||||||
while (nodes[++i] && nodes[i] != node);
|
|
||||||
return !!nodes[i];
|
|
||||||
}
|
|
||||||
}(Element.prototype);
|
|
||||||
|
|
||||||
// helper for enabling IE 8 event bindings
|
|
||||||
function addEvent(el, type, handler) {
|
|
||||||
if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// live binding helper using matchesSelector
|
|
||||||
function live(selector, event, callback, context) {
|
|
||||||
addEvent(context || document, event, function(e) {
|
|
||||||
var found, el = e.target || e.srcElement;
|
|
||||||
while (el && el.matches && el !== context && !(found = el.matches(selector))) el = el.parentElement;
|
|
||||||
if (found) callback.call(el, e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// in case the document is already rendered
|
|
||||||
if (document.readyState!='loading') run();
|
|
||||||
// modern browsers
|
|
||||||
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run);
|
|
||||||
// IE <= 8
|
|
||||||
else document.attachEvent('onreadystatechange', function(){
|
|
||||||
if (document.readyState=='complete') run();
|
|
||||||
});
|
|
|
@ -110,6 +110,9 @@
|
||||||
} catch {
|
} catch {
|
||||||
downloadLink = '';
|
downloadLink = '';
|
||||||
}
|
}
|
||||||
|
const bookName = link.split('/').pop();
|
||||||
|
const viewerLink = `${root}/viewer#${bookName}`;
|
||||||
|
|
||||||
const humanFriendlyZimSize = humanFriendlySize(zimSize);
|
const humanFriendlyZimSize = humanFriendlySize(zimSize);
|
||||||
|
|
||||||
const divTag = document.createElement('div');
|
const divTag = document.createElement('div');
|
||||||
|
@ -122,7 +125,7 @@
|
||||||
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
|
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
|
||||||
divTag.innerHTML = `
|
divTag.innerHTML = `
|
||||||
<div class="book__wrapper">
|
<div class="book__wrapper">
|
||||||
<a class="book__link" href="${link}" data-hover="Preview">
|
<a class="book__link" href="${viewerLink}" data-hover="Preview">
|
||||||
<div class="book__link__wrapper">
|
<div class="book__link__wrapper">
|
||||||
<div class="book__icon" ${faviconAttr}></div>
|
<div class="book__icon" ${faviconAttr}></div>
|
||||||
<div class="book__header">
|
<div class="book__header">
|
||||||
|
@ -179,12 +182,12 @@
|
||||||
<div onclick="closeModal()" class="modal-close-button">
|
<div onclick="closeModal()" class="modal-close-button">
|
||||||
<div>
|
<div>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
|
||||||
0.683417 13.7071 0.292893C13.3166 -0.0976311 12.6834 -0.0976311 12.2929 0.292893L7 5.58579L1.70711
|
0.683417 13.7071 0.292893C13.3166 -0.0976311 12.6834 -0.0976311 12.2929 0.292893L7 5.58579L1.70711
|
||||||
0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417
|
0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417
|
||||||
-0.0976311 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976311 12.6834
|
-0.0976311 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976311 12.6834
|
||||||
-0.0976311 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7
|
-0.0976311 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7
|
||||||
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
|
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
|
||||||
14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z" fill="black" />
|
14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z" fill="black" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,7 +206,7 @@
|
||||||
<div>Sha256 hash</div>
|
<div>Sha256 hash</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
${magnetLink ?
|
${magnetLink ?
|
||||||
`<div class="modal-regular-download">
|
`<div class="modal-regular-download">
|
||||||
<a href="${magnetLink}" target="_blank">
|
<a href="${magnetLink}" target="_blank">
|
||||||
<img src="../skin/magnet.png?KIWIXCACHEID" alt="download magnet" />
|
<img src="../skin/magnet.png?KIWIXCACHEID" alt="download magnet" />
|
||||||
|
@ -388,7 +391,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTagElement(tagValue, resetFilter) {
|
function addTagElement(tagValue, resetFilter) {
|
||||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||||
tagElement.style.display = 'inline-block';
|
tagElement.style.display = 'inline-block';
|
||||||
|
@ -475,7 +478,7 @@
|
||||||
const currentLink = window.location.search;
|
const currentLink = window.location.search;
|
||||||
const newLink = `?${params.toString()}`;
|
const newLink = `?${params.toString()}`;
|
||||||
if (currentLink != newLink) {
|
if (currentLink != newLink) {
|
||||||
window.history.pushState({}, null, newLink);
|
window.history.pushState({}, null, newLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateVisibleParams();
|
updateVisibleParams();
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
#kiwixtoolbar {
|
#kiwixtoolbar {
|
||||||
position: fixed;
|
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
background-position-y: 0;
|
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -135,10 +129,6 @@ a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
|
||||||
column-count: 1 !important;
|
column-count: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
padding-top: calc(3em - 5px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(min-width:420px) {
|
@media(min-width:420px) {
|
||||||
.kiwix_button_cont {
|
.kiwix_button_cont {
|
||||||
display: inline-block !important;
|
display: inline-block !important;
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
function htmlDecode(input) {
|
|
||||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
|
||||||
return doc.documentElement.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupAutoHidingOfTheToolbar() {
|
|
||||||
let lastScrollTop = 0;
|
|
||||||
const delta = 5;
|
|
||||||
let didScroll = false;
|
|
||||||
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
didScroll = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
setInterval(function() {
|
|
||||||
if (didScroll) {
|
|
||||||
hasScrolled();
|
|
||||||
didScroll = false;
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
function hasScrolled() {
|
|
||||||
const st = document.documentElement.scrollTop || document.body.scrollTop;
|
|
||||||
if (Math.abs(lastScrollTop - st) <= delta)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (st > lastScrollTop) {
|
|
||||||
kiwixToolBar.style.top = '-100%';
|
|
||||||
} else {
|
|
||||||
kiwixToolBar.style.top = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollTop = st;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
const root = document.querySelector(`link[type='root']`).getAttribute("href");
|
|
||||||
const bookName = (window.location.pathname == `${root}/search`)
|
|
||||||
? (new URLSearchParams(window.location.search)).get('content')
|
|
||||||
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
|
|
||||||
|
|
||||||
const autoCompleteJS = new autoComplete(
|
|
||||||
{
|
|
||||||
selector: "#kiwixsearchbox",
|
|
||||||
placeHolder: document.querySelector("#kiwixsearchbox").title,
|
|
||||||
threshold: 1,
|
|
||||||
debounce: 300,
|
|
||||||
data : {
|
|
||||||
src: async (query) => {
|
|
||||||
try {
|
|
||||||
// Fetch Data from external Source
|
|
||||||
const source = await fetch(`${root}/suggest?content=${encodeURIComponent(bookName)}&term=${encodeURIComponent(query)}`);
|
|
||||||
const data = await source.json();
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keys: ['label'],
|
|
||||||
},
|
|
||||||
submit: true,
|
|
||||||
searchEngine: (query, record) => {
|
|
||||||
// We accept all records
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
resultsList: {
|
|
||||||
noResults: true,
|
|
||||||
/* We must display 10 results (requested) + 1 potential link to do a full text search. */
|
|
||||||
maxResults: 11,
|
|
||||||
},
|
|
||||||
resultItem: {
|
|
||||||
element: (item, data) => {
|
|
||||||
let searchLink;
|
|
||||||
if (data.value.kind == "path") {
|
|
||||||
searchLink = `${root}/${bookName}/${htmlDecode(data.value.path)}`;
|
|
||||||
} else {
|
|
||||||
searchLink = `${root}/search?content=${encodeURIComponent(bookName)}&pattern=${encodeURIComponent(htmlDecode(data.value.value))}`;
|
|
||||||
}
|
|
||||||
item.innerHTML = `<a class="suggest" href="${searchLink}">${htmlDecode(data.value.label)}</a>`;
|
|
||||||
},
|
|
||||||
highlight: "autoComplete_highlight",
|
|
||||||
selected: "autoComplete_selected"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
document.querySelector('#kiwixsearchform').addEventListener('submit', function(event) {
|
|
||||||
try {
|
|
||||||
const selectedElemLink = document.querySelector('.autoComplete_selected > a').href;
|
|
||||||
if (selectedElemLink) {
|
|
||||||
event.preventDefault();
|
|
||||||
window.location = selectedElemLink;
|
|
||||||
}
|
|
||||||
} catch (err) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
const kiwixSearchBox = document.querySelector('#kiwixsearchbox');
|
|
||||||
const kiwixSearchForm = document.querySelector('.kiwix_searchform');
|
|
||||||
kiwixSearchBox.addEventListener('focus', () => {
|
|
||||||
kiwixSearchForm.classList.add('full_width');
|
|
||||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.add('searching');
|
|
||||||
document.querySelector('.kiwix_button_cont').classList.add('searching');
|
|
||||||
});
|
|
||||||
kiwixSearchBox.addEventListener('blur', () => {
|
|
||||||
kiwixSearchForm.classList.remove('full_width');
|
|
||||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.remove('searching');
|
|
||||||
document.querySelector('.kiwix_button_cont').classList.remove('searching');
|
|
||||||
});
|
|
||||||
|
|
||||||
// cybook hack
|
|
||||||
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
|
|
||||||
document.querySelector('html').classList.add('cybook');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.body.clientWidth < 520) {
|
|
||||||
setupAutoHidingOfTheToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -0,0 +1,409 @@
|
||||||
|
// Terminology
|
||||||
|
//
|
||||||
|
// user url: identifier of the page that has to be displayed in the viewer
|
||||||
|
// and that is used as the hash component of the viewer URL. For
|
||||||
|
// book resources the address url is {book}/{resource} .
|
||||||
|
//
|
||||||
|
// iframe url: the URL to be loaded in the viewer iframe.
|
||||||
|
|
||||||
|
function userUrl2IframeUrl(url) {
|
||||||
|
if ( url == '' ) {
|
||||||
|
return blankPageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( url.startsWith('search?') ) {
|
||||||
|
return `${root}/${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${root}/content/${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBookFromUserUrl(url) {
|
||||||
|
if ( url == '' ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( url.startsWith('search?') ) {
|
||||||
|
const p = new URLSearchParams(url.slice("search?".length));
|
||||||
|
return p.get('books.name') || p.get('content');
|
||||||
|
}
|
||||||
|
return url.split('/')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentBook = getBookFromUserUrl(location.hash.slice(1));
|
||||||
|
let currentBookTitle = null;
|
||||||
|
|
||||||
|
const bookUIGroup = document.getElementById('kiwix_serve_taskbar_book_ui_group');
|
||||||
|
const homeButton = document.getElementById('kiwix_serve_taskbar_home_button');
|
||||||
|
const contentIframe = document.getElementById('content_iframe');
|
||||||
|
|
||||||
|
|
||||||
|
function gotoMainPageOfCurrentBook() {
|
||||||
|
location.hash = currentBook + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoUrl(url) {
|
||||||
|
contentIframe.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoRandomPage() {
|
||||||
|
gotoUrl(`${root}/random?content=${currentBook}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function performSearch() {
|
||||||
|
const searchbox = document.getElementById('kiwixsearchbox');
|
||||||
|
const q = encodeURIComponent(searchbox.value);
|
||||||
|
gotoUrl(`${root}/search?books.name=${currentBook}&pattern=${q}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestionsApiURL()
|
||||||
|
{
|
||||||
|
return `${root}/suggest?content=${encodeURIComponent(currentBook)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentBook(book, title) {
|
||||||
|
currentBook = book;
|
||||||
|
currentBookTitle = title;
|
||||||
|
homeButton.title = `Go to the main page of '${title}'`;
|
||||||
|
homeButton.setAttribute("aria-label", homeButton.title);
|
||||||
|
homeButton.innerHTML = `<button>${title}</button>`;
|
||||||
|
bookUIGroup.style.display = 'inline';
|
||||||
|
updateSearchBoxForBookChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function noCurrentBook() {
|
||||||
|
currentBook = null;
|
||||||
|
currentBookTitle = null;
|
||||||
|
bookUIGroup.style.display = 'none';
|
||||||
|
updateSearchBoxForBookChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentBookIfNeeded(userUrl) {
|
||||||
|
const book = getBookFromUserUrl(userUrl);
|
||||||
|
if ( currentBook != book ) {
|
||||||
|
updateCurrentBook(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentBook(book) {
|
||||||
|
if ( book == null ) {
|
||||||
|
noCurrentBook();
|
||||||
|
} else {
|
||||||
|
fetch(`./raw/${book}/meta/Title`).then(async (resp) => {
|
||||||
|
if ( resp.ok ) {
|
||||||
|
setCurrentBook(book, await resp.text());
|
||||||
|
} else {
|
||||||
|
noCurrentBook();
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log("Error fetching book title: " + err);
|
||||||
|
noCurrentBook();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iframeUrl2UserUrl(url, query) {
|
||||||
|
if ( url == blankPageUrl ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( url == `${root}/search` ) {
|
||||||
|
return `search${query}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = url.slice(root.length);
|
||||||
|
|
||||||
|
return url.split('/').slice(2).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchPattern() {
|
||||||
|
const url = window.location.hash.slice(1);
|
||||||
|
if ( url.startsWith('search?') ) {
|
||||||
|
const p = new URLSearchParams(url.slice("search?".length));
|
||||||
|
return p.get("pattern");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let autoCompleteJS = null;
|
||||||
|
|
||||||
|
function closeSuggestions() {
|
||||||
|
if ( autoCompleteJS ) {
|
||||||
|
autoCompleteJS.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSearchBoxForLocationChange() {
|
||||||
|
closeSuggestions();
|
||||||
|
document.getElementById("kiwixsearchbox").value = getSearchPattern();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSearchBoxForBookChange() {
|
||||||
|
const searchbox = document.getElementById('kiwixsearchbox');
|
||||||
|
const kiwixSearchFormWrapper = document.querySelector('.kiwix_searchform');
|
||||||
|
if ( currentBookTitle ) {
|
||||||
|
searchbox.title = `Search '${currentBookTitle}'`;
|
||||||
|
searchbox.placeholder = searchbox.title;
|
||||||
|
searchbox.setAttribute("aria-label", searchbox.title);
|
||||||
|
kiwixSearchFormWrapper.style.display = 'inline';
|
||||||
|
} else {
|
||||||
|
kiwixSearchFormWrapper.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_visual_viewport_change() {
|
||||||
|
contentIframe.height = window.visualViewport.height - contentIframe.offsetTop - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_location_hash_change() {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
console.log("handle_location_hash_change: " + hash);
|
||||||
|
updateCurrentBookIfNeeded(hash);
|
||||||
|
const iframeContentUrl = userUrl2IframeUrl(hash);
|
||||||
|
if ( iframeContentUrl != contentIframe.contentWindow.location.pathname ) {
|
||||||
|
contentIframe.contentWindow.location.replace(iframeContentUrl);
|
||||||
|
}
|
||||||
|
updateSearchBoxForLocationChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_content_url_change() {
|
||||||
|
const iframeLocation = contentIframe.contentWindow.location;
|
||||||
|
console.log('handle_content_url_change: ' + iframeLocation.href);
|
||||||
|
document.title = contentIframe.contentDocument.title;
|
||||||
|
const iframeContentUrl = iframeLocation.pathname;
|
||||||
|
const iframeContentQuery = iframeLocation.search;
|
||||||
|
const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery);
|
||||||
|
const viewerURL = location.origin + location.pathname + location.search;
|
||||||
|
window.location.replace(viewerURL + '#' + newHash);
|
||||||
|
updateCurrentBookIfNeeded(newHash);
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// External link blocking
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function matchingAncestorElement(el, context, selector) {
|
||||||
|
while (el && el.matches && el !== context) {
|
||||||
|
if ( el.matches(selector) )
|
||||||
|
return el;
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const block_path = `${root}/catch/external`;
|
||||||
|
|
||||||
|
function blockLink(target) {
|
||||||
|
const encodedHref = encodeURIComponent(target.href);
|
||||||
|
target.setAttribute("href", block_path + "?source=" + encodedHref);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExternalUrl(url) {
|
||||||
|
if ( url.startsWith(window.location.origin) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return url.startsWith("//")
|
||||||
|
|| url.startsWith("http:")
|
||||||
|
|| url.startsWith("https:");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickEvent(e) {
|
||||||
|
const iframeDocument = contentIframe.contentDocument;
|
||||||
|
const target = matchingAncestorElement(e.target, iframeDocument, "a");
|
||||||
|
if (target !== null && "href" in target) {
|
||||||
|
if ( isExternalUrl(target.href) ) {
|
||||||
|
target.setAttribute("target", "_top");
|
||||||
|
if ( viewerSettings.linkBlockingEnabled ) {
|
||||||
|
return blockLink(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper for enabling IE 8 event bindings
|
||||||
|
function addEventHandler(el, eventType, handler) {
|
||||||
|
if (el.attachEvent)
|
||||||
|
el.attachEvent('on'+eventType, handler);
|
||||||
|
else
|
||||||
|
el.addEventListener(eventType, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupEventHandler(context, selector, eventType, callback) {
|
||||||
|
addEventHandler(context, eventType, function(e) {
|
||||||
|
const eventElement = e.target || e.srcElement;
|
||||||
|
const el = matchingAncestorElement(eventElement, context, selector);
|
||||||
|
if (el)
|
||||||
|
callback.call(el, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches polyfill
|
||||||
|
this.Element && function(ElementPrototype) {
|
||||||
|
ElementPrototype.matches = ElementPrototype.matches ||
|
||||||
|
ElementPrototype.matchesSelector ||
|
||||||
|
ElementPrototype.webkitMatchesSelector ||
|
||||||
|
ElementPrototype.msMatchesSelector ||
|
||||||
|
function(selector) {
|
||||||
|
var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
|
||||||
|
while (nodes[++i] && nodes[i] != node);
|
||||||
|
return !!nodes[i];
|
||||||
|
}
|
||||||
|
}(Element.prototype);
|
||||||
|
|
||||||
|
function setup_external_link_blocker() {
|
||||||
|
setupEventHandler(contentIframe.contentDocument, 'a', 'click', onClickEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// End of external link blocking
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
function on_content_load() {
|
||||||
|
handle_content_url_change();
|
||||||
|
setup_external_link_blocker();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onresize = handle_visual_viewport_change;
|
||||||
|
window.onhashchange = handle_location_hash_change;
|
||||||
|
|
||||||
|
updateCurrentBook(currentBook);
|
||||||
|
handle_location_hash_change();
|
||||||
|
|
||||||
|
function htmlDecode(input) {
|
||||||
|
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||||
|
return doc.documentElement.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAutoHidingOfTheToolbar() {
|
||||||
|
let lastScrollTop = 0;
|
||||||
|
const delta = 5;
|
||||||
|
let didScroll = false;
|
||||||
|
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
|
||||||
|
|
||||||
|
contentIframe.contentWindow.addEventListener('scroll', () => {
|
||||||
|
didScroll = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(function() {
|
||||||
|
if (didScroll) {
|
||||||
|
hasScrolled();
|
||||||
|
didScroll = false;
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
function hasScrolled() {
|
||||||
|
const iframeDoc = contentIframe.contentDocument;
|
||||||
|
const st = iframeDoc.documentElement.scrollTop || iframeDoc.body.scrollTop;
|
||||||
|
if (Math.abs(lastScrollTop - st) <= delta)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (st > lastScrollTop) {
|
||||||
|
kiwixToolBar.style.position = 'fixed';
|
||||||
|
kiwixToolBar.style.top = '-100%';
|
||||||
|
} else {
|
||||||
|
kiwixToolBar.style.position = 'static';
|
||||||
|
kiwixToolBar.style.top = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollTop = st;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSuggestions() {
|
||||||
|
const kiwixSearchBox = document.querySelector('#kiwixsearchbox');
|
||||||
|
const kiwixSearchFormWrapper = document.querySelector('.kiwix_searchform');
|
||||||
|
|
||||||
|
autoCompleteJS = new autoComplete(
|
||||||
|
{
|
||||||
|
selector: "#kiwixsearchbox",
|
||||||
|
placeHolder: kiwixSearchBox.title,
|
||||||
|
threshold: 1,
|
||||||
|
debounce: 300,
|
||||||
|
data : {
|
||||||
|
src: async (query) => {
|
||||||
|
try {
|
||||||
|
// Fetch Data from external Source
|
||||||
|
const source = await fetch(`${suggestionsApiURL()}&term=${encodeURIComponent(query)}`);
|
||||||
|
const data = await source.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keys: ['label'],
|
||||||
|
},
|
||||||
|
submit: true,
|
||||||
|
searchEngine: (query, record) => {
|
||||||
|
// We accept all records
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
resultsList: {
|
||||||
|
noResults: true,
|
||||||
|
// We must display 10 results (requested) + 1 potential link to do a full text search.
|
||||||
|
maxResults: 11,
|
||||||
|
},
|
||||||
|
resultItem: {
|
||||||
|
element: (item, data) => {
|
||||||
|
let searchLink;
|
||||||
|
if (data.value.kind == "path") {
|
||||||
|
searchLink = `${root}/${currentBook}/${htmlDecode(data.value.path)}`;
|
||||||
|
} else {
|
||||||
|
searchLink = `${root}/search?content=${encodeURIComponent(currentBook)}&pattern=${encodeURIComponent(htmlDecode(data.value.value))}`;
|
||||||
|
}
|
||||||
|
item.innerHTML = `<a class="suggest" href="javascript:gotoUrl('${searchLink}')">${htmlDecode(data.value.label)}</a>`;
|
||||||
|
},
|
||||||
|
highlight: "autoComplete_highlight",
|
||||||
|
selected: "autoComplete_selected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.querySelector('#kiwixsearchform').addEventListener('submit', function(event) {
|
||||||
|
closeSuggestions();
|
||||||
|
try {
|
||||||
|
const selectedElem = document.querySelector('.autoComplete_selected > a');
|
||||||
|
if (selectedElem) {
|
||||||
|
event.preventDefault();
|
||||||
|
selectedElem.click();
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
kiwixSearchBox.addEventListener('focus', () => {
|
||||||
|
kiwixSearchFormWrapper.classList.add('full_width');
|
||||||
|
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.add('searching');
|
||||||
|
document.querySelector('.kiwix_button_cont').classList.add('searching');
|
||||||
|
});
|
||||||
|
kiwixSearchBox.addEventListener('blur', () => {
|
||||||
|
kiwixSearchFormWrapper.classList.remove('full_width');
|
||||||
|
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.remove('searching');
|
||||||
|
document.querySelector('.kiwix_button_cont').classList.remove('searching');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupViewer() {
|
||||||
|
setInterval(handle_visual_viewport_change, 0);
|
||||||
|
|
||||||
|
const kiwixToolBarWrapper = document.getElementById('kiwixtoolbarwrapper');
|
||||||
|
if ( ! viewerSettings.toolbarEnabled ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
kiwixToolBarWrapper.style.display = 'block';
|
||||||
|
if ( ! viewerSettings.libraryButtonEnabled ) {
|
||||||
|
document.getElementById("kiwix_serve_taskbar_library_button").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSuggestions();
|
||||||
|
|
||||||
|
// cybook hack
|
||||||
|
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
|
||||||
|
document.querySelector('html').classList.add('cybook');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body.clientWidth < 520) {
|
||||||
|
setupAutoHidingOfTheToolbar();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
<script type="text/javascript" src="{{root}}/skin/block_external.js"></script>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<link type="text/css" href="{{root}}/skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
|
|
||||||
<link type="text/css" href="{{root}}/skin/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
|
||||||
<script type="text/javascript" src="{{root}}/skin/taskbar.js?KIWIXCACHEID" defer></script>
|
|
||||||
<script type="text/javascript" src="{{root}}/skin/autoComplete.min.js?KIWIXCACHEID"></script>
|
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<link type="root" href="{{root}}">
|
||||||
<title>Welcome to Kiwix Server</title>
|
<title>Welcome to Kiwix Server</title>
|
||||||
<link
|
<link
|
||||||
type="text/css"
|
type="text/css"
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<span class="kiwix">
|
|
||||||
<span id="kiwixtoolbar" class="ui-widget-header">
|
|
||||||
<div class="kiwix_centered">
|
|
||||||
<div class="kiwix_searchform">
|
|
||||||
<form class="kiwixsearch" method="GET" action="{{root}}/search" id="kiwixsearchform">
|
|
||||||
{{#hascontent}}<input type="hidden" name="content" value="{{content}}" />{{/hascontent}}
|
|
||||||
<label for="kiwixsearchbox">🔍</label>
|
|
||||||
<input autocomplete="off" id="kiwixsearchbox" name="pattern" type="text" size="50" title="{{{SEARCHBOX_TOOLTIP}}}" aria-label="{{{SEARCHBOX_TOOLTIP}}}">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<input type="checkbox" id="kiwix_button_show_toggle">
|
|
||||||
<label for="kiwix_button_show_toggle"><img src="{{root}}/skin/caret.png?KIWIXCACHEID" alt=""></label>
|
|
||||||
<div class="kiwix_button_cont">
|
|
||||||
{{#withlibrarybutton}}
|
|
||||||
<a id="kiwix_serve_taskbar_library_button" title="{{{LIBRARY_BUTTON_TEXT}}}" aria-label="{{{LIBRARY_BUTTON_TEXT}}}" href="{{root}}/"><button>🏠</button></a>
|
|
||||||
{{/withlibrarybutton}}
|
|
||||||
{{#hascontent}}
|
|
||||||
<a id="kiwix_serve_taskbar_home_button" title="{{{HOME_BUTTON_TEXT}}}" aria-label="{{{HOME_BUTTON_TEXT}}}" href="{{root}}/{{content}}/"><button>{{title}}</button></a>
|
|
||||||
<a id="kiwix_serve_taskbar_random_button" title="{{{RANDOM_PAGE_BUTTON_TEXT}}}" aria-label="{{{RANDOM_PAGE_BUTTON_TEXT}}}"
|
|
||||||
href="{{root}}/random?content={{#urlencoded}}{{{content}}}{{/urlencoded}}"><button>🎲</button></a>
|
|
||||||
{{/hascontent}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
const viewerSettings = {
|
||||||
|
toolbarEnabled: {{enable_toolbar}},
|
||||||
|
linkBlockingEnabled: {{enable_link_blocking}},
|
||||||
|
libraryButtonEnabled: {{enable_library_button}}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ZIM Viewer</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link type="text/css" href="./skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||||
|
<link type="text/css" href="./skin/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||||
|
<script type="text/javascript" src="./viewer_settings.js"></script>
|
||||||
|
<script type="text/javascript" src="./skin/viewer.js?KIWIXCACHEID" defer></script>
|
||||||
|
<script type="text/javascript" src="./skin/autoComplete.min.js?KIWIXCACHEID"></script>
|
||||||
|
<script>
|
||||||
|
function getRootLocation() {
|
||||||
|
const p = location.pathname;
|
||||||
|
return p.slice(0, p.length - '/viewer'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = getRootLocation();
|
||||||
|
const blankPageUrl = `${root}/skin/blank.html`;
|
||||||
|
|
||||||
|
if ( location.hash == '' ) {
|
||||||
|
location.href = root + '/';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="margin:0" onload="setupViewer()">
|
||||||
|
<div class="kiwix" style="display:none" id="kiwixtoolbarwrapper">
|
||||||
|
<div id="kiwixtoolbar" class="ui-widget-header">
|
||||||
|
<div class="kiwix_centered">
|
||||||
|
<div class="kiwix_searchform">
|
||||||
|
<form class="kiwixsearch" method="GET" action="javascript:performSearch()" id="kiwixsearchform">
|
||||||
|
<label for="kiwixsearchbox">🔍</label>
|
||||||
|
<input autocomplete="off" class="ui-autocomplete-input" id="kiwixsearchbox" name="pattern" type="text" title="Search '{{title}}'" aria-label="Search '{{title}}'">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" id="kiwix_button_show_toggle">
|
||||||
|
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?KIWIXCACHEID" alt=""></label>
|
||||||
|
<div class="kiwix_button_cont">
|
||||||
|
<a id="kiwix_serve_taskbar_library_button" title="Go to welcome page" aria-label="Go to welcome page" href="./"><button>🏠</button></a>
|
||||||
|
<span id="kiwix_serve_taskbar_book_ui_group">
|
||||||
|
<a id="kiwix_serve_taskbar_home_button"
|
||||||
|
title="Go to the main page of the current book"
|
||||||
|
aria-label="Go to the main page of the current book"
|
||||||
|
onclick="gotoMainPageOfCurrentBook()"></a>
|
||||||
|
<a id="kiwix_serve_taskbar_random_button"
|
||||||
|
title="Go to a randomly selected page"
|
||||||
|
aria-label="Go to a randomly selected page"
|
||||||
|
onclick="gotoRandomPage()">
|
||||||
|
<button>🎲</button>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<iframe id="content_iframe"
|
||||||
|
referrerpolicy="same-origin"
|
||||||
|
onload="on_content_load()"
|
||||||
|
src="skin/blank.html" title="ZIM content" width="100%"
|
||||||
|
style="border:0px">
|
||||||
|
</iframe>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -73,31 +73,4 @@ TEST(ReplaceRegex, middle)
|
||||||
EXPECT_EQ(replaceRegex("abcdefghij", "----", "F"), "abcde----ghij");
|
EXPECT_EQ(replaceRegex("abcdefghij", "----", "F"), "abcde----ghij");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(append, beggining)
|
|
||||||
{
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "abcd", "----"), "abcd----efghij");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "abcde", "----"), "abcde----fghij");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "a.*i", "----"), "abcdefghi----j");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "AbCd", "----"), "abcd----efghij");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "A", "----"), "a----bcdefghij");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(append, end)
|
|
||||||
{
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "ghij", "----"), "abcdefghij----");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "fghij", "----"), "abcdefghij----");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "c.*j", "----"), "abcdefghij----");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "GhIj", "----"), "abcdefghij----");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "J", "----"), "abcdefghij----");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(append, middle)
|
|
||||||
{
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "cdef", "----"), "abcdef----ghij");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "cdefgh", "----"), "abcdefgh----ij");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "c.*f", "----"), "abcdef----ghij");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "DeFg", "----"), "abcdefg----hij");
|
|
||||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "F", "----"), "abcdef----ghij");
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
213
test/server.cpp
213
test/server.cpp
|
@ -43,9 +43,9 @@ typedef std::vector<Resource> ResourceCollection;
|
||||||
const ResourceCollection resources200Compressible{
|
const ResourceCollection resources200Compressible{
|
||||||
{ WITH_ETAG, "/ROOT/" },
|
{ WITH_ETAG, "/ROOT/" },
|
||||||
|
|
||||||
{ WITH_ETAG, "/ROOT/skin/taskbar.js" },
|
{ WITH_ETAG, "/ROOT/skin/autoComplete.min.js" },
|
||||||
|
{ WITH_ETAG, "/ROOT/skin/css/autoComplete.css" },
|
||||||
{ WITH_ETAG, "/ROOT/skin/taskbar.css" },
|
{ WITH_ETAG, "/ROOT/skin/taskbar.css" },
|
||||||
{ WITH_ETAG, "/ROOT/skin/block_external.js" },
|
|
||||||
|
|
||||||
{ NO_ETAG, "/ROOT/catalog/search" },
|
{ NO_ETAG, "/ROOT/catalog/search" },
|
||||||
|
|
||||||
|
@ -53,8 +53,6 @@ const ResourceCollection resources200Compressible{
|
||||||
|
|
||||||
{ NO_ETAG, "/ROOT/suggest?content=zimfile&term=ray" },
|
{ NO_ETAG, "/ROOT/suggest?content=zimfile&term=ray" },
|
||||||
|
|
||||||
{ NO_ETAG, "/ROOT/catch/external?source=www.example.com" },
|
|
||||||
|
|
||||||
{ WITH_ETAG, "/ROOT/content/zimfile/A/index" },
|
{ WITH_ETAG, "/ROOT/content/zimfile/A/index" },
|
||||||
{ WITH_ETAG, "/ROOT/content/zimfile/A/Ray_Charles" },
|
{ WITH_ETAG, "/ROOT/content/zimfile/A/Ray_Charles" },
|
||||||
|
|
||||||
|
@ -64,6 +62,7 @@ const ResourceCollection resources200Compressible{
|
||||||
|
|
||||||
const ResourceCollection resources200Uncompressible{
|
const ResourceCollection resources200Uncompressible{
|
||||||
{ WITH_ETAG, "/ROOT/skin/caret.png" },
|
{ WITH_ETAG, "/ROOT/skin/caret.png" },
|
||||||
|
{ WITH_ETAG, "/ROOT/skin/css/images/search.svg" },
|
||||||
|
|
||||||
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Title" },
|
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Title" },
|
||||||
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Description" },
|
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Description" },
|
||||||
|
@ -76,6 +75,8 @@ const ResourceCollection resources200Uncompressible{
|
||||||
|
|
||||||
{ NO_ETAG, "/ROOT/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" },
|
{ NO_ETAG, "/ROOT/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" },
|
||||||
|
|
||||||
|
{ NO_ETAG, "/ROOT/catch/external?source=www.example.com" },
|
||||||
|
|
||||||
{ WITH_ETAG, "/ROOT/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
|
{ WITH_ETAG, "/ROOT/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
|
||||||
|
|
||||||
{ WITH_ETAG, "/ROOT/content/corner_cases/A/empty.html" },
|
{ WITH_ETAG, "/ROOT/content/corner_cases/A/empty.html" },
|
||||||
|
@ -103,7 +104,7 @@ TEST(indexTemplateStringTest, emptyIndexTemplate) {
|
||||||
"./test/corner_cases.zim"
|
"./test/corner_cases.zim"
|
||||||
};
|
};
|
||||||
|
|
||||||
ZimFileServer zfs(PORT, /*withTaskbar=*/true, ZIMFILES, "");
|
ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, "");
|
||||||
EXPECT_EQ(200, zfs.GET("/ROOT/")->status);
|
EXPECT_EQ(200, zfs.GET("/ROOT/")->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,13 +115,12 @@ TEST(indexTemplateStringTest, indexTemplateCheck) {
|
||||||
"./test/corner_cases.zim"
|
"./test/corner_cases.zim"
|
||||||
};
|
};
|
||||||
|
|
||||||
ZimFileServer zfs(PORT, /*withTaskbar=*/true, ZIMFILES, "<!DOCTYPE html><head>"
|
ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, "<!DOCTYPE html><head>"
|
||||||
"<title>Welcome to kiwix library</title>"
|
"<title>Welcome to kiwix library</title>"
|
||||||
"</head>"
|
"</head>"
|
||||||
"</html>");
|
"</html>");
|
||||||
EXPECT_EQ("<!DOCTYPE html><head>"
|
EXPECT_EQ("<!DOCTYPE html><head>"
|
||||||
"<title>Welcome to kiwix library</title>"
|
"<title>Welcome to kiwix library</title>"
|
||||||
"<link type=\"root\" href=\"/ROOT\">"
|
|
||||||
"</head>"
|
"</head>"
|
||||||
"</html>", zfs.GET("/ROOT/")->body);
|
"</html>", zfs.GET("/ROOT/")->body);
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=3b470cee"
|
||||||
src: url("/ROOT/skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
|
src: url("/ROOT/skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
|
||||||
<script src="/ROOT/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
|
<script src="/ROOT/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
|
||||||
<script src="/ROOT/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
|
<script src="/ROOT/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
|
||||||
<script type="text/javascript" src="/ROOT/skin/index.js?cacheid=76440e7a" defer></script>
|
<script type="text/javascript" src="/ROOT/skin/index.js?cacheid=2f5a81ac" defer></script>
|
||||||
)EXPECTEDRESULT"
|
)EXPECTEDRESULT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -196,24 +196,24 @@ R"EXPECTEDRESULT( <img src="../skin/download.png?
|
||||||
)EXPECTEDRESULT"
|
)EXPECTEDRESULT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
/* url */ "/ROOT/content/zimfile/A/index",
|
/* url */ "/ROOT/viewer",
|
||||||
R"EXPECTEDRESULT(<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/taskbar.css?cacheid=26082885" rel="Stylesheet" />
|
R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=216d6b5d" rel="Stylesheet" />
|
||||||
<link type="text/css" href="/ROOT/skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
<link type="text/css" href="./skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
||||||
<script type="text/javascript" src="/ROOT/skin/taskbar.js?cacheid=1aec4a68" defer></script>
|
<script type="text/javascript" src="./skin/viewer.js?cacheid=9a336712" defer></script>
|
||||||
<script type="text/javascript" src="/ROOT/skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
<script type="text/javascript" src="./skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||||
<label for="kiwix_button_show_toggle"><img src="/ROOT/skin/caret.png?cacheid=22b942b4" alt=""></label>
|
const blankPageUrl = `${root}/skin/blank.html`;
|
||||||
|
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
|
||||||
)EXPECTEDRESULT"
|
)EXPECTEDRESULT"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
/* url */ "/ROOT/content/zimfile/A/index",
|
||||||
|
""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Searching in a ZIM file without a full-text index returns
|
// Searching in a ZIM file without a full-text index returns
|
||||||
// a page rendered from static/templates/no_search_result_html
|
// a page rendered from static/templates/no_search_result_html
|
||||||
/* url */ "/ROOT/search?content=poor&pattern=whatever",
|
/* url */ "/ROOT/search?content=poor&pattern=whatever",
|
||||||
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT/skin/search_results.css?cacheid=76d39c84" rel="Stylesheet" />
|
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT/skin/search_results.css?cacheid=76d39c84" rel="Stylesheet" />
|
||||||
<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/taskbar.css?cacheid=26082885" rel="Stylesheet" />
|
|
||||||
<link type="text/css" href="/ROOT/skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
|
||||||
<script type="text/javascript" src="/ROOT/skin/taskbar.js?cacheid=1aec4a68" defer></script>
|
|
||||||
<script type="text/javascript" src="/ROOT/skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
|
||||||
<label for="kiwix_button_show_toggle"><img src="/ROOT/skin/caret.png?cacheid=22b942b4" alt=""></label>
|
|
||||||
)EXPECTEDRESULT"
|
)EXPECTEDRESULT"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -429,13 +429,8 @@ public:
|
||||||
std::string expectedResponse() const;
|
std::string expectedResponse() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isTranslatedVersion() const;
|
|
||||||
virtual std::string pageTitle() const;
|
virtual std::string pageTitle() const;
|
||||||
std::string pageCssLink() const;
|
std::string pageCssLink() const;
|
||||||
std::string hiddenBookNameInput() const;
|
|
||||||
std::string searchPatternInput() const;
|
|
||||||
std::string taskbarLinks() const;
|
|
||||||
std::string goToWelcomePageText() const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string TestContentIn404HtmlResponse::expectedResponse() const
|
std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||||
|
@ -451,40 +446,8 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||||
)FRAG",
|
)FRAG",
|
||||||
|
|
||||||
R"FRAG(
|
R"FRAG(
|
||||||
<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/taskbar.css?cacheid=26082885" rel="Stylesheet" />
|
</head>
|
||||||
<link type="text/css" href="/ROOT/skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
<body>)FRAG",
|
||||||
<script type="text/javascript" src="/ROOT/skin/taskbar.js?cacheid=1aec4a68" defer></script>
|
|
||||||
<script type="text/javascript" src="/ROOT/skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
|
||||||
</head>
|
|
||||||
<body><span class="kiwix">
|
|
||||||
<span id="kiwixtoolbar" class="ui-widget-header">
|
|
||||||
<div class="kiwix_centered">
|
|
||||||
<div class="kiwix_searchform">
|
|
||||||
<form class="kiwixsearch" method="GET" action="/ROOT/search" id="kiwixsearchform">
|
|
||||||
)FRAG",
|
|
||||||
|
|
||||||
R"FRAG(
|
|
||||||
<label for="kiwixsearchbox">🔍</label>
|
|
||||||
)FRAG",
|
|
||||||
|
|
||||||
R"FRAG( </form>
|
|
||||||
</div>
|
|
||||||
<input type="checkbox" id="kiwix_button_show_toggle">
|
|
||||||
<label for="kiwix_button_show_toggle"><img src="/ROOT/skin/caret.png?cacheid=22b942b4" alt=""></label>
|
|
||||||
<div class="kiwix_button_cont">
|
|
||||||
<a id="kiwix_serve_taskbar_library_button" title=")FRAG",
|
|
||||||
|
|
||||||
R"FRAG(" aria-label=")FRAG",
|
|
||||||
|
|
||||||
R"FRAG(" href="/ROOT/"><button>🏠</button></a>
|
|
||||||
)FRAG",
|
|
||||||
|
|
||||||
R"FRAG(
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)FRAG",
|
|
||||||
|
|
||||||
R"FRAG( </body>
|
R"FRAG( </body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -496,18 +459,8 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||||
+ frag[1]
|
+ frag[1]
|
||||||
+ pageCssLink()
|
+ pageCssLink()
|
||||||
+ frag[2]
|
+ frag[2]
|
||||||
+ hiddenBookNameInput()
|
|
||||||
+ frag[3]
|
|
||||||
+ searchPatternInput()
|
|
||||||
+ frag[4]
|
|
||||||
+ goToWelcomePageText()
|
|
||||||
+ frag[5]
|
|
||||||
+ goToWelcomePageText()
|
|
||||||
+ frag[6]
|
|
||||||
+ taskbarLinks()
|
|
||||||
+ frag[7]
|
|
||||||
+ expectedBody
|
+ expectedBody
|
||||||
+ frag[8];
|
+ frag[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TestContentIn404HtmlResponse::pageTitle() const
|
std::string TestContentIn404HtmlResponse::pageTitle() const
|
||||||
|
@ -527,71 +480,6 @@ std::string TestContentIn404HtmlResponse::pageCssLink() const
|
||||||
+ R"(" rel="Stylesheet" />)";
|
+ R"(" rel="Stylesheet" />)";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TestContentIn404HtmlResponse::hiddenBookNameInput() const
|
|
||||||
{
|
|
||||||
return bookName.empty()
|
|
||||||
? ""
|
|
||||||
: R"(<input type="hidden" name="content" value=")" + bookName + R"(" />)";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string TestContentIn404HtmlResponse::searchPatternInput() const
|
|
||||||
{
|
|
||||||
const std::string searchboxTooltip = isTranslatedVersion()
|
|
||||||
? "Որոնել '" + bookTitle + "'֊ում"
|
|
||||||
: "Search '" + bookTitle + "'";
|
|
||||||
return R"( <input autocomplete="off" id="kiwixsearchbox" name="pattern" type="text" size="50" title=")"
|
|
||||||
+ searchboxTooltip
|
|
||||||
+ R"(" aria-label=")"
|
|
||||||
+ searchboxTooltip
|
|
||||||
+ R"(">
|
|
||||||
)";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string TestContentIn404HtmlResponse::taskbarLinks() const
|
|
||||||
{
|
|
||||||
if ( bookName.empty() )
|
|
||||||
return "";
|
|
||||||
|
|
||||||
const auto goToMainPageOfBook = isTranslatedVersion()
|
|
||||||
? "Դեպի '" + bookTitle + "'֊ի գլխավոր էջը"
|
|
||||||
: "Go to the main page of '" + bookTitle + "'";
|
|
||||||
|
|
||||||
const std::string goToRandomPage = isTranslatedVersion()
|
|
||||||
? "Բացել պատահական էջ"
|
|
||||||
: "Go to a randomly selected page";
|
|
||||||
|
|
||||||
return R"(<a id="kiwix_serve_taskbar_home_button" title=")"
|
|
||||||
+ goToMainPageOfBook
|
|
||||||
+ R"(" aria-label=")"
|
|
||||||
+ goToMainPageOfBook
|
|
||||||
+ R"(" href="/ROOT/)"
|
|
||||||
+ bookName
|
|
||||||
+ R"(/"><button>)"
|
|
||||||
+ bookTitle
|
|
||||||
+ R"(</button></a>
|
|
||||||
<a id="kiwix_serve_taskbar_random_button" title=")"
|
|
||||||
+ goToRandomPage
|
|
||||||
+ R"(" aria-label=")"
|
|
||||||
+ goToRandomPage
|
|
||||||
+ R"("
|
|
||||||
href="/ROOT/random?content=)"
|
|
||||||
+ bookName
|
|
||||||
+ R"("><button>🎲</button></a>)";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TestContentIn404HtmlResponse::isTranslatedVersion() const
|
|
||||||
{
|
|
||||||
return url.find("userlang=hy") != std::string::npos;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string TestContentIn404HtmlResponse::goToWelcomePageText() const
|
|
||||||
{
|
|
||||||
return isTranslatedVersion()
|
|
||||||
? "Գրադարանի էջ"
|
|
||||||
: "Go to welcome page";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
|
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -1139,11 +1027,13 @@ TEST_F(ServerTest, RawEntry)
|
||||||
EXPECT_EQ(200, p->status);
|
EXPECT_EQ(200, p->status);
|
||||||
EXPECT_EQ(std::string(p->body), std::string(entry.getItem(true).getData()));
|
EXPECT_EQ(std::string(p->body), std::string(entry.getItem(true).getData()));
|
||||||
|
|
||||||
|
/* Now normal content is not decorated in any way, either
|
||||||
// ... but the "normal" content is not
|
// ... but the "normal" content is not
|
||||||
p = zfs1_->GET("/ROOT/content/zimfile/A/Ray_Charles");
|
p = zfs1_->GET("/ROOT/content/zimfile/A/Ray_Charles");
|
||||||
EXPECT_EQ(200, p->status);
|
EXPECT_EQ(200, p->status);
|
||||||
EXPECT_NE(std::string(p->body), std::string(entry.getItem(true).getData()));
|
EXPECT_NE(std::string(p->body), std::string(entry.getItem(true).getData()));
|
||||||
EXPECT_TRUE(p->body.find("taskbar") != std::string::npos);
|
EXPECT_TRUE(p->body.find("<link type=\"root\" href=\"/ROOT\">") != std::string::npos);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(ServerTest, HeadMethodIsSupported)
|
TEST_F(ServerTest, HeadMethodIsSupported)
|
||||||
|
@ -1200,7 +1090,7 @@ TEST_F(ServerTest, ETagIsTheSameAcrossHeadAndGet)
|
||||||
|
|
||||||
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETags)
|
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETags)
|
||||||
{
|
{
|
||||||
ZimFileServer zfs2(SERVER_PORT + 1, /*withTaskbar=*/true, ZIMFILES);
|
ZimFileServer zfs2(SERVER_PORT + 1, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES);
|
||||||
for ( const Resource& res : all200Resources() ) {
|
for ( const Resource& res : all200Resources() ) {
|
||||||
if ( !res.etag_expected ) continue;
|
if ( !res.etag_expected ) continue;
|
||||||
const auto h1 = zfs1_->HEAD(res.url);
|
const auto h1 = zfs1_->HEAD(res.url);
|
||||||
|
@ -1591,3 +1481,54 @@ TEST_F(ServerTest, suggestions_in_range)
|
||||||
ASSERT_EQ(currCount, 0);
|
ASSERT_EQ(currCount, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ServerTest, viewerSettings)
|
||||||
|
{
|
||||||
|
const auto JS_CONTENT_TYPE = "application/javascript; charset=utf-8";
|
||||||
|
{
|
||||||
|
resetServer(ZimFileServer::NO_TASKBAR_NO_LINK_BLOCKING);
|
||||||
|
const auto r = zfs1_->GET("/ROOT/viewer_settings.js");
|
||||||
|
ASSERT_EQ(r->status, 200);
|
||||||
|
ASSERT_EQ(getHeaderValue(r->headers, "Content-Type"), JS_CONTENT_TYPE);
|
||||||
|
ASSERT_EQ(r->body,
|
||||||
|
R"(const viewerSettings = {
|
||||||
|
toolbarEnabled: false,
|
||||||
|
linkBlockingEnabled: false,
|
||||||
|
libraryButtonEnabled: false
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
resetServer(ZimFileServer::BLOCK_EXTERNAL_LINKS);
|
||||||
|
ASSERT_EQ(zfs1_->GET("/ROOT/viewer_settings.js")->body,
|
||||||
|
R"(const viewerSettings = {
|
||||||
|
toolbarEnabled: false,
|
||||||
|
linkBlockingEnabled: true,
|
||||||
|
libraryButtonEnabled: false
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
resetServer(ZimFileServer::WITH_TASKBAR);
|
||||||
|
ASSERT_EQ(zfs1_->GET("/ROOT/viewer_settings.js")->body,
|
||||||
|
R"(const viewerSettings = {
|
||||||
|
toolbarEnabled: true,
|
||||||
|
linkBlockingEnabled: false,
|
||||||
|
libraryButtonEnabled: false
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
|
||||||
|
ASSERT_EQ(zfs1_->GET("/ROOT/viewer_settings.js")->body,
|
||||||
|
R"(const viewerSettings = {
|
||||||
|
toolbarEnabled: true,
|
||||||
|
linkBlockingEnabled: false,
|
||||||
|
libraryButtonEnabled: true
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ std::string makeSearchResultsHtml(const std::string& pattern,
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<title>Search: %PATTERN%</title>
|
<title>Search: %PATTERN%</title>
|
||||||
<link type="root" href="/ROOT"></head>
|
</head>
|
||||||
<body bgcolor="white">
|
<body bgcolor="white">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
%HEADER%
|
%HEADER%
|
||||||
|
@ -1453,16 +1453,13 @@ TEST_F(ServerTest, searchResults)
|
||||||
|
|
||||||
for ( const auto& t : testData ) {
|
for ( const auto& t : testData ) {
|
||||||
const std::string htmlSearchUrl = t.url();
|
const std::string htmlSearchUrl = t.url();
|
||||||
const auto htmlRes = taskbarlessZimFileServer().GET(htmlSearchUrl.c_str());
|
const auto htmlRes = zfs1_->GET(htmlSearchUrl.c_str());
|
||||||
EXPECT_EQ(htmlRes->status, 200);
|
EXPECT_EQ(htmlRes->status, 200);
|
||||||
t.checkHtml(htmlRes->body);
|
t.checkHtml(htmlRes->body);
|
||||||
|
|
||||||
const std::string xmlSearchUrl = t.xmlSearchUrl();
|
const std::string xmlSearchUrl = t.xmlSearchUrl();
|
||||||
const auto xmlRes1 = zfs1_->GET(xmlSearchUrl.c_str());
|
const auto xmlRes = zfs1_->GET(xmlSearchUrl.c_str());
|
||||||
const auto xmlRes2 = taskbarlessZimFileServer().GET(xmlSearchUrl.c_str());
|
EXPECT_EQ(xmlRes->status, 200);
|
||||||
EXPECT_EQ(xmlRes1->status, 200);
|
t.checkXml(xmlRes->body);
|
||||||
EXPECT_EQ(xmlRes2->status, 200);
|
|
||||||
EXPECT_EQ(xmlRes1->body, xmlRes2->body);
|
|
||||||
t.checkXml(xmlRes1->body);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,10 +54,23 @@ public: // types
|
||||||
typedef std::shared_ptr<httplib::Response> Response;
|
typedef std::shared_ptr<httplib::Response> Response;
|
||||||
typedef std::vector<std::string> FilePathCollection;
|
typedef std::vector<std::string> FilePathCollection;
|
||||||
|
|
||||||
|
enum Options
|
||||||
|
{
|
||||||
|
NO_TASKBAR_NO_LINK_BLOCKING = 0,
|
||||||
|
|
||||||
|
WITH_TASKBAR = 1 << 1,
|
||||||
|
WITH_LIBRARY_BUTTON = 1 << 2,
|
||||||
|
BLOCK_EXTERNAL_LINKS = 1 << 3,
|
||||||
|
|
||||||
|
WITH_TASKBAR_AND_LIBRARY_BUTTON = WITH_TASKBAR | WITH_LIBRARY_BUTTON,
|
||||||
|
|
||||||
|
DEFAULT_OPTIONS = WITH_TASKBAR | WITH_LIBRARY_BUTTON
|
||||||
|
};
|
||||||
|
|
||||||
public: // functions
|
public: // functions
|
||||||
ZimFileServer(int serverPort, std::string libraryFilePath);
|
ZimFileServer(int serverPort, std::string libraryFilePath);
|
||||||
ZimFileServer(int serverPort,
|
ZimFileServer(int serverPort,
|
||||||
bool withTaskbar,
|
Options options,
|
||||||
const FilePathCollection& zimpaths,
|
const FilePathCollection& zimpaths,
|
||||||
std::string indexTemplateString = "");
|
std::string indexTemplateString = "");
|
||||||
~ZimFileServer();
|
~ZimFileServer();
|
||||||
|
@ -81,7 +94,7 @@ private: // data
|
||||||
std::unique_ptr<kiwix::HumanReadableNameMapper> nameMapper;
|
std::unique_ptr<kiwix::HumanReadableNameMapper> nameMapper;
|
||||||
std::unique_ptr<kiwix::Server> server;
|
std::unique_ptr<kiwix::Server> server;
|
||||||
std::unique_ptr<httplib::Client> client;
|
std::unique_ptr<httplib::Client> client;
|
||||||
const bool withTaskbar = true;
|
const Options options = DEFAULT_OPTIONS;
|
||||||
};
|
};
|
||||||
|
|
||||||
ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
|
ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
|
||||||
|
@ -94,11 +107,11 @@ ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
ZimFileServer::ZimFileServer(int serverPort,
|
ZimFileServer::ZimFileServer(int serverPort,
|
||||||
bool _withTaskbar,
|
Options _options,
|
||||||
const FilePathCollection& zimpaths,
|
const FilePathCollection& zimpaths,
|
||||||
std::string indexTemplateString)
|
std::string indexTemplateString)
|
||||||
: manager(&this->library)
|
: manager(&this->library)
|
||||||
, withTaskbar(_withTaskbar)
|
, options(_options)
|
||||||
{
|
{
|
||||||
for ( const auto& zimpath : zimpaths ) {
|
for ( const auto& zimpath : zimpaths ) {
|
||||||
if (!manager.addBookFromPath(zimpath, zimpath, "", false))
|
if (!manager.addBookFromPath(zimpath, zimpath, "", false))
|
||||||
|
@ -117,7 +130,8 @@ void ZimFileServer::run(int serverPort, std::string indexTemplateString)
|
||||||
server->setPort(serverPort);
|
server->setPort(serverPort);
|
||||||
server->setNbThreads(2);
|
server->setNbThreads(2);
|
||||||
server->setVerbose(false);
|
server->setVerbose(false);
|
||||||
server->setTaskbar(withTaskbar, withTaskbar);
|
server->setTaskbar(options & WITH_TASKBAR, options & WITH_LIBRARY_BUTTON);
|
||||||
|
server->setBlockExternalLinks(options & BLOCK_EXTERNAL_LINKS);
|
||||||
server->setMultiZimSearchLimit(3);
|
server->setMultiZimSearchLimit(3);
|
||||||
if (!indexTemplateString.empty()) {
|
if (!indexTemplateString.empty()) {
|
||||||
server->setIndexTemplateString(indexTemplateString);
|
server->setIndexTemplateString(indexTemplateString);
|
||||||
|
@ -136,9 +150,6 @@ ZimFileServer::~ZimFileServer()
|
||||||
|
|
||||||
class ServerTest : public ::testing::Test
|
class ServerTest : public ::testing::Test
|
||||||
{
|
{
|
||||||
private:
|
|
||||||
std::unique_ptr<ZimFileServer> taskbarlessZfs_;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::unique_ptr<ZimFileServer> zfs1_;
|
std::unique_ptr<ZimFileServer> zfs1_;
|
||||||
|
|
||||||
|
@ -151,19 +162,15 @@ protected:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void SetUp() override {
|
void SetUp() override {
|
||||||
zfs1_.reset(new ZimFileServer(SERVER_PORT, /*withTaskbar=*/true, ZIMFILES));
|
resetServer(ZimFileServer::DEFAULT_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
ZimFileServer& taskbarlessZimFileServer()
|
void resetServer(ZimFileServer::Options options) {
|
||||||
{
|
zfs1_.reset();
|
||||||
if ( ! taskbarlessZfs_ ) {
|
zfs1_.reset(new ZimFileServer(SERVER_PORT, options, ZIMFILES));
|
||||||
taskbarlessZfs_.reset(new ZimFileServer(SERVER_PORT+1, /*withTaskbar=*/false, ZIMFILES));
|
|
||||||
}
|
|
||||||
return *taskbarlessZfs_;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TearDown() override {
|
void TearDown() override {
|
||||||
zfs1_.reset();
|
zfs1_.reset();
|
||||||
taskbarlessZfs_.reset();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue