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