Merge pull request #716 from kiwix/iframe_based_content_viewer

Iframe-based content viewer
This commit is contained in:
Matthieu Gautier 2022-09-22 09:28:50 +02:00 committed by GitHub
commit 3a75facfdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 673 additions and 659 deletions

View File

@ -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);
} }
} }

View File

@ -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);
}; };
} }

View File

@ -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;

View File

@ -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);

View File

@ -83,58 +83,30 @@ 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
@ -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);

View File

@ -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;
}

View File

@ -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

View File

@ -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

11
static/skin/blank.html Normal file
View File

@ -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>

View File

@ -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();
});

View File

@ -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">

View File

@ -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;

View File

@ -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();
}
});

409
static/skin/viewer.js Normal file
View File

@ -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();
}
}

View File

@ -1 +0,0 @@
<script type="text/javascript" src="{{root}}/skin/block_external.js"></script>

View File

@ -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>

View File

@ -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"

View File

@ -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">&#x1f50d;</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>&#x1f3e0;</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>&#x1F3B2;</button></a>
{{/hascontent}}
</div>
</div>
</span>
</span>

View File

@ -0,0 +1,5 @@
const viewerSettings = {
toolbarEnabled: {{enable_toolbar}},
linkBlockingEnabled: {{enable_link_blocking}},
libraryButtonEnabled: {{enable_library_button}}
}

68
static/viewer.html Normal file
View File

@ -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">&#x1f50d;</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>&#x1f3e0;</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>&#x1F3B2;</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>

View File

@ -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");
}
}; };

View File

@ -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" />
<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>
</head> </head>
<body><span class="kiwix"> <body>)FRAG",
<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">&#x1f50d;</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>&#x1f3e0;</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>&#x1F3B2;</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
}
)");
}
}

View File

@ -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);
} }
} }

View File

@ -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();
} }
}; };