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

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

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" /> </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">&#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();
} }
}; };