Compare commits

..

6 Commits

Author SHA1 Message Date
Nikhil Tanwar
fc87def18b Add documentation for widget
Added documentation for current widget usage, currently supported arguments, using custom CSS/JS
2022-08-15 18:22:23 +05:30
Nikhil Tanwar
09dc6f90fd Add custom CSS and JS support
Added an event listener for message event.
The idea is the website which embeds the widget will send a message using postMessage().
The expected message is:
{
css: // custom CSS code
js: // custom JS code
}
2022-08-15 18:22:22 +05:30
Nikhil Tanwar
58c04b3f77 Basic widget handling
Adds handling for parameters:
disablefilter - disable search filters
disableclick - disable book click action
disabledownload - disable download button
disabledesc - disable description
2022-08-15 18:21:41 +05:30
Nikhil Tanwar
d27220f65d Give name to IIFE in index.js and expose updateBookCount
Gives a name to the IIFE wrapping code in index.js - kiwixServe
and exposes updateBookCount through it
2022-08-11 21:18:21 +05:30
Nikhil Tanwar
efe42c9bbe Add widget endpoint
Adds an endpoint /widget to provide kiwix serve widget.
2022-08-11 21:18:21 +05:30
Nikhil Tanwar
489dfc1123 Give a name to function for updating book count
Extracts function updateBookCount() from the unnamed function in resize event.
2022-08-11 21:10:47 +05:30
51 changed files with 1016 additions and 961 deletions

View File

@@ -101,33 +101,6 @@ meson . build -Dwrapper=android -Dwerror=false
ninja -C build
```
Static files compilation
------------------------
Libkiwix has a few static files 'compiled' within the binary
code. This is mostly Javascript/HTML/pictures necessary for the HTTP
daemon.
These static files are available in the `static` directory and are
compiled by custom Python code available in this repository `scripts`
directory. This happens automatically at compilation time without any
additional command to run.
To avoid HTTP caching issues, the URLs (to the static content) are
appended with a `cacheid` parameter (this is called "cache
busting"). This `cacheid` value derived from the
[sha1sum](https://en.wikipedia.org/wiki/Sha1sum) of each targeted
static file. As a consequence, each time you change a static file, the
corresponding `cacheid` value will change.
To properly test this feature, this `cacheid` needs to be added
manually to the automated tests and has to be commited. After
modifying the needed static file, [run the automated
tests](#Testing). They will fail, but the inspection of the testing
log will give you the new `cacheid` value(s). Finally update
`test/server.cpp` with the appropriate `cacheid` value(s) which have
changed.
Testing
-------
@@ -151,7 +124,7 @@ where you want to install the libraries. After the installation
succeeded, you may need to run `ldconfig` (as `root`).
Uninstallation
--------------
------------
If you want to uninstall the Kiwix library:
```bash
@@ -161,6 +134,28 @@ ninja -C build uninstall
Like for the installation, you might need to run the command as `root`
(or using `sudo`).
Troubleshooting
---------------
If you need to install Meson "manually":
```bash
virtualenv -p python3 ./ # Create virtualenv
source bin/activate # Activate the virtualenv
pip3 install meson # Install Meson
hash -r # Refresh bash paths
```
If you need to install Ninja "manually":
```bash
git clone git://github.com/ninja-build/ninja.git
cd ninja
git checkout release
./configure.py --bootstrap
mkdir ../bin
cp ninja ../bin
cd ..
```
Custom Index Page
-----------------
@@ -210,28 +205,6 @@ distribution. Try then with a source tarball distributed by the
problematic upstream project or even directly from the source code
repository.
Troubleshooting
---------------
If you need to install Meson "manually":
```bash
virtualenv -p python3 ./ # Create virtualenv
source bin/activate # Activate the virtualenv
pip3 install meson # Install Meson
hash -r # Refresh bash paths
```
If you need to install Ninja "manually":
```bash
git clone git://github.com/ninja-build/ninja.git
cd ninja
git checkout release
./configure.py --bootstrap
mkdir ../bin
cp ninja ../bin
cd ..
```
License
-------

View File

@@ -12,3 +12,4 @@ Welcome to libkiwix's documentation!
usage
api/ref_api
widget

82
docs/widget.rst Normal file
View File

@@ -0,0 +1,82 @@
Kiwix serve widget
====================
Introduction
------------
The kiwix-serve widget provides an easy to embed way to show the `kiwix-serve` homepage.
Usage
-----
To use the widget, simply add an iframe with its `src` attribute set to the `widget` endpoint.
Example HTML Page ::
<!DOCTYPE html>
<html lang="en">
<head>
<title>Widget Test</title>
</head>
<body>
<iframe src="http://192.168.18.8:8080/widget?disabledesc&disablefilter&disabledownload" width=1000 height=1000></iframe>
</body>
</html>
This creates an iframe with the kiwix-serve homepage contents.
Arguments are explained below.
Possible Arguments
-------------------
Currently, the following arguments are supported.
disabledesc (value = N/A)
Disables the description part of a tile.
disablefilter (value = N/A)
Disables the search filters: language, category, tag and search function.
disableclick (value = N/A)
Disables clicking the book to open it for reading.
disabledownload (value = N/A)
Disables the download button (if avaialable at all) on the tile.
Custom CSS and JS
-----------------
You can add your custom CSS rules and Javascript code to the widget.
To do that, use the following code as template::
<iframe id="receiver" src="http://192.168.18.8:8080/widget?disabledesc=&disablefilter=&disabledownload=" width="1000" height="1000">
<p>Your browser does not support iframes.</p>
</iframe>
<script>
window.onload = function() {
var receiver = document.getElementById('receiver').contentWindow;
function sendMessage() {
let msg = {
css: `
.book__header {
color:red;
}`,
js: `
function widgetTest() {
console.log("Testing widget");
}
widgetTest();
`
}
receiver.postMessage(msg, 'http://192.168.18.8:8080/widget');
}
sendMessage();
}
</script>
The CSS/JS fields are optional, you may send both or only one.

View File

@@ -54,7 +54,6 @@ enum supportedListMode {
class Filter {
public: // types
using Tags = std::vector<std::string>;
using AliasNames = std::vector<std::string>;
private: // data
uint64_t activeFilters;
@@ -68,7 +67,6 @@ class Filter {
std::string _query;
bool _queryIsPartial;
std::string _name;
AliasNames _aliasNames;
public: // functions
Filter();
@@ -114,7 +112,6 @@ class Filter {
Filter& maxSize(size_t size);
Filter& query(std::string query, bool partial=true);
Filter& name(std::string name);
Filter& aliasNames(const AliasNames& aliasNames);
bool hasQuery() const;
const std::string& getQuery() const { return _query; }
@@ -138,8 +135,6 @@ class Filter {
const Tags& getAcceptTags() const { return _acceptTags; }
const Tags& getRejectTags() const { return _rejectTags; }
const AliasNames& getAliasNames() const { return _aliasNames; }
private: // functions
friend class Library;

View File

@@ -54,7 +54,6 @@ class HumanReadableNameMapper : public NameMapper {
virtual ~HumanReadableNameMapper() = default;
virtual std::string getNameForId(const std::string& id) const;
virtual std::string getIdForName(const std::string& name) const;
static std::string removeDateFromBookId(const std::string& bookId);
};
class UpdatableNameMapper : public NameMapper {

View File

@@ -28,7 +28,6 @@
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "tools/concurrent_cache.h"
#include "name_mapper.h"
#include <pugixml.hpp>
#include <algorithm>
@@ -462,9 +461,6 @@ void Library::updateBookDB(const Book& book)
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
indexer.index_text(normalizeText(book.getName()), 1, "XN");
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
const auto bookName = book.getHumanReadableIdFromPath();
const auto aliasName = HumanReadableNameMapper::removeDateFromBookId(bookName);
indexer.index_text(normalizeText(aliasName), 1, "XF");
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
doc.add_boolean_term("XT" + tag);
@@ -509,7 +505,6 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
queryParser.add_prefix("publisher", "XP");
queryParser.add_prefix("creator", "A");
queryParser.add_prefix("tag", "XT");
queryParser.add_prefix("filename", "XF");
const auto partialQueryFlag = filter.queryIsPartial()
? Xapian::QueryParser::FLAG_PARTIAL
: 0;
@@ -526,16 +521,6 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
return queryParser.parse_query(normalizeText(filter.getQuery()), flags);
}
Xapian::Query makePhraseQuery(const std::string& query, const std::string& prefix)
{
Xapian::QueryParser queryParser;
queryParser.set_default_op(Xapian::Query::OP_OR);
queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_NONE);
const auto flags = 0;
const auto q = queryParser.parse_query(normalizeText(query), flags, prefix);
return Xapian::Query(Xapian::Query::OP_PHRASE, q.get_terms_begin(), q.get_terms_end(), q.get_length());
}
Xapian::Query nameQuery(const std::string& name)
{
return Xapian::Query("XN" + normalizeText(name));
@@ -546,18 +531,6 @@ Xapian::Query categoryQuery(const std::string& category)
return Xapian::Query("XC" + normalizeText(category));
}
Xapian::Query aliasNamesQuery(const Filter::AliasNames& aliasNames)
{
Xapian::Query q = Xapian::Query(std::string());
std::vector<Xapian::Query> queryVec;
for (const auto& aliasName : aliasNames) {
queryVec.push_back(makePhraseQuery(aliasName, "XF"));
}
Xapian::Query combinedQuery(Xapian::Query::OP_OR, queryVec.begin(), queryVec.end());
q = Xapian::Query(Xapian::Query::OP_FILTER, q, combinedQuery);
return q;
}
Xapian::Query langQuery(const std::string& lang)
{
return Xapian::Query("L" + normalizeText(lang));
@@ -565,12 +538,22 @@ Xapian::Query langQuery(const std::string& lang)
Xapian::Query publisherQuery(const std::string& publisher)
{
return makePhraseQuery(publisher, "XP");
Xapian::QueryParser queryParser;
queryParser.set_default_op(Xapian::Query::OP_OR);
queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_NONE);
const auto flags = 0;
const auto q = queryParser.parse_query(normalizeText(publisher), flags, "XP");
return Xapian::Query(Xapian::Query::OP_PHRASE, q.get_terms_begin(), q.get_terms_end(), q.get_length());
}
Xapian::Query creatorQuery(const std::string& creator)
{
return makePhraseQuery(creator, "A");
Xapian::QueryParser queryParser;
queryParser.set_default_op(Xapian::Query::OP_OR);
queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_NONE);
const auto flags = 0;
const auto q = queryParser.parse_query(normalizeText(creator), flags, "A");
return Xapian::Query(Xapian::Query::OP_PHRASE, q.get_terms_begin(), q.get_terms_end(), q.get_length());
}
Xapian::Query tagsQuery(const Filter::Tags& acceptTags, const Filter::Tags& rejectTags)
@@ -610,9 +593,6 @@ Xapian::Query buildXapianQuery(const Filter& filter)
const auto tq = tagsQuery(filter.getAcceptTags(), filter.getRejectTags());
q = Xapian::Query(Xapian::Query::OP_AND, q, tq);;
}
if ( !filter.getAliasNames().empty() ) {
q = Xapian::Query(Xapian::Query::OP_AND, q, aliasNamesQuery(filter.getAliasNames()));
}
return q;
}
@@ -762,7 +742,6 @@ enum filterTypes {
QUERY = FLAG(12),
NAME = FLAG(13),
CATEGORY = FLAG(14),
ALIASNAMES = FLAG(15),
};
Filter& Filter::local(bool accept)
@@ -865,13 +844,6 @@ Filter& Filter::name(std::string name)
return *this;
}
Filter& Filter::aliasNames(const AliasNames& aliasNames)
{
_aliasNames = aliasNames;
activeFilters |= ALIASNAMES;
return *this;
}
#define ACTIVE(X) (activeFilters & (X))
#define FILTER(TAG, TEST) if (ACTIVE(TAG) && !(TEST)) { return false; }
bool Filter::hasQuery() const

View File

@@ -45,7 +45,7 @@ config_h = configure_file(output : 'kiwix_config.h',
input : 'config.h.in')
install_headers(config_h, subdir:'kiwix')
libkiwix = library('kiwix',
kiwixlib = library('kiwix',
kiwix_sources,
include_directories : inc,
dependencies : all_deps,

View File

@@ -34,7 +34,7 @@ HumanReadableNameMapper::HumanReadableNameMapper(kiwix::Library& library, bool w
if (!withAlias)
continue;
auto aliasName = removeDateFromBookId(bookName);
auto aliasName = replaceRegex(bookName, "", "_[[:digit:]]{4}-[[:digit:]]{2}$");
if (aliasName == bookName) {
continue;
}
@@ -51,10 +51,6 @@ HumanReadableNameMapper::HumanReadableNameMapper(kiwix::Library& library, bool w
}
}
std::string HumanReadableNameMapper::removeDateFromBookId(const std::string& bookId) {
return replaceRegex(bookId, "", "_[[:digit:]]{4}-[[:digit:]]{2}$");
}
std::string HumanReadableNameMapper::getNameForId(const std::string& id) const {
return m_idToName.at(id);
}

View File

@@ -20,7 +20,7 @@
#include "opds_dumper.h"
#include "book.h"
#include "libkiwix-resources.h"
#include "kiwixlib-resources.h"
#include <mustache.hpp>
#include "tools/stringTools.h"

View File

@@ -29,7 +29,7 @@
#include <zim/search.h>
#include <mustache.hpp>
#include "libkiwix-resources.h"
#include "kiwixlib-resources.h"
#include "tools/stringTools.h"
namespace kiwix

View File

@@ -68,7 +68,7 @@ extern "C" {
#include <vector>
#include <chrono>
#include <fstream>
#include "libkiwix-resources.h"
#include "kiwixlib-resources.h"
#ifndef _WIN32
# include <arpa/inet.h>
@@ -119,9 +119,6 @@ Filter get_search_filter(const RequestContext& request, const std::string& prefi
try {
filter.rejectTags(kiwix::split(request.get_argument(prefix+"notag"), ";"));
} catch (...) {}
try {
filter.aliasNames(request.get_arguments(prefix + "book"));
} catch (...) {}
return filter;
}
@@ -556,12 +553,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
if (url == "/" )
return build_homepage(request);
if (isEndpointUrl(url, "viewer") || isEndpointUrl(url, "skin"))
if (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);
@@ -583,6 +577,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
if (isEndpointUrl(url, "catch"))
return handle_catch(request);
if (isEndpointUrl(url, "widget"))
return handle_widget(request);
std::string contentUrl = m_root + "/content" + url;
const std::string query = request.get_query();
if ( ! query.empty() )
@@ -629,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");
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8", true);
}
/**
@@ -659,7 +656,8 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
if (archive == nullptr) {
return HTTP404Response(*this, request)
+ noSuchBookErrorMsg(bookName);
+ noSuchBookErrorMsg(bookName)
+ TaskbarInfo(bookName);
}
const auto queryString = request.get_optional_param("term", std::string());
@@ -719,30 +717,13 @@ 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");
}
const bool isRequestForViewer = request.get_url() == "/viewer";
auto resourceName = isRequestForViewer
? "viewer.html"
: request.get_url().substr(1);
auto resourceName = request.get_url().substr(1);
try {
auto response = ContentResponse::build(
*this,
@@ -799,15 +780,11 @@ 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;
}
@@ -837,18 +814,16 @@ 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");
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");
// 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)
@@ -880,7 +855,8 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
if (archive == nullptr) {
return HTTP404Response(*this, request)
+ noSuchBookErrorMsg(bookName);
+ noSuchBookErrorMsg(bookName)
+ TaskbarInfo(bookName);
}
try {
@@ -888,10 +864,16 @@ 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");
+ nonParameterizedMessage("random-article-failure")
+ TaskbarInfo(bookName, archive.get());
}
}
std::unique_ptr<Response> InternalServer::handle_widget(const RequestContext& request)
{
return ContentResponse::build(*this, RESOURCE::templates::widget_html, get_default_data(), "text/html; charset=utf-8", true);
}
std::unique_ptr<Response> InternalServer::handle_captured_external(const RequestContext& request)
{
std::string source = "";
@@ -1036,7 +1018,8 @@ 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));
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName);
}
auto urlStr = url.substr(prefixLength + bookName.size());
@@ -1052,6 +1035,9 @@ 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());
@@ -1066,7 +1052,8 @@ 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));
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName, archive.get());
}
}
@@ -1114,13 +1101,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);
return ItemResponse::build(*this, request, item, /*raw=*/true);
} else {
auto entry = archive->getEntryByPath(itemPath);
if (entry.isRedirect()) {
return build_redirect(bookName, entry.getItem(true));
}
return ItemResponse::build(*this, request, entry.getItem());
return ItemResponse::build(*this, request, entry.getItem(), /*raw=*/true);
}
} catch (zim::EntryNotFound& e ) {
if (m_verbose.load()) {
@@ -1157,7 +1144,9 @@ std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(con
return ContentResponse::build(*this,
resourceData,
crd.mimeType);
crd.mimeType,
/*isHomePage=*/false,
/*raw=*/true);
}
}

View File

@@ -126,7 +126,6 @@ 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);
@@ -144,6 +143,7 @@ class InternalServer {
std::unique_ptr<Response> handle_content(const RequestContext& request);
std::unique_ptr<Response> handle_raw(const RequestContext& request);
std::unique_ptr<Response> handle_locally_customized_resource(const RequestContext& request);
std::unique_ptr<Response> handle_widget(const RequestContext& request);
std::vector<std::string> search_catalog(const RequestContext& request,
kiwix::OPDSDumper& opdsDumper);
@@ -184,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);
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
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);
};
}

View File

@@ -24,7 +24,7 @@
#include "request_context.h"
#include "response.h"
#include "tools/otherTools.h"
#include "libkiwix-resources.h"
#include "kiwixlib-resources.h"
#include <mustache.hpp>
@@ -158,11 +158,7 @@ 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;

View File

@@ -20,7 +20,7 @@
#include "response.h"
#include "request_context.h"
#include "internalServer.h"
#include "libkiwix-resources.h"
#include "kiwixlib-resources.h"
#include "tools/regexTools.h"
#include "tools/stringTools.h"
@@ -140,6 +140,9 @@ 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;
}
@@ -233,12 +236,29 @@ HTTP500Response::HTTP500Response(const InternalServer& server,
std::unique_ptr<ContentResponse> HTTP500Response::generateResponseObject() const
{
const std::string mimeType = "text/html;charset=utf-8";
// We want a 500 response to be a minimalistic one (so that the server doesn't
// have to provide additional resources required for its proper rendering)
// ";raw=true" in the MIME-type below disables response decoration
// (see ContentResponse::contentDecorationAllowed())
const std::string mimeType = "text/html;charset=utf-8;raw=true";
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
r->set_code(m_httpStatusCode);
return r;
}
ContentResponseBlueprint& ContentResponseBlueprint::operator+(const TaskbarInfo& taskbarInfo)
{
this->m_taskbarInfo.reset(new TaskbarInfo(taskbarInfo));
return *this;
}
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);
@@ -317,6 +337,52 @@ 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
{
@@ -325,6 +391,16 @@ 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)
{
@@ -335,6 +411,17 @@ 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(
@@ -374,11 +461,24 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
return ret;
}
ContentResponse::ContentResponse(const std::string& root, bool verbose, const std::string& content, const std::string& mimetype) :
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) :
Response(verbose),
m_root(root),
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);
}
@@ -386,11 +486,17 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, const st
std::unique_ptr<ContentResponse> ContentResponse::build(
const InternalServer& server,
const std::string& content,
const std::string& mimetype)
const std::string& mimetype,
bool isHomePage,
bool raw)
{
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));
}
@@ -399,10 +505,11 @@ std::unique_ptr<ContentResponse> ContentResponse::build(
const InternalServer& server,
const std::string& template_str,
kainjow::mustache::data data,
const std::string& mimetype)
const std::string& mimetype,
bool isHomePage)
{
auto content = render_template(template_str, data);
return ContentResponse::build(server, content, mimetype);
return ContentResponse::build(server, content, mimetype, isHomePage);
}
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
@@ -415,14 +522,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)
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw)
{
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);
auto response = ContentResponse::build(server, item.getData(), mimetype, /*isHomePage=*/false, raw);
response->set_cacheable();
response->m_byteRange = byteRange;
return std::move(response);

View File

@@ -83,32 +83,60 @@ 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);
const std::string& mimetype,
bool isHomePage = false,
bool raw = false);
static std::unique_ptr<ContentResponse> build(
const InternalServer& server,
const std::string& template_str,
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:
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
@@ -137,6 +165,9 @@ 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;
@@ -148,6 +179,7 @@ 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
@@ -159,6 +191,8 @@ 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);
@@ -204,7 +238,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);
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw = false);
private:
MHD_Response* create_mhd_response(const RequestContext& request);

View File

@@ -75,3 +75,41 @@ 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;
}

View File

@@ -26,5 +26,11 @@ 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

View File

@@ -1,31 +0,0 @@
{
"@metadata": {
"authors": [
"Spotter"
]
},
"name": "Čeština",
"suggest-full-text-search": "obsahující '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Žádná taková kniha: {{BOOK_NAME}}",
"too-many-books": "Bylo požadováno příliš mnoho knih ({{NB_BOOKS}}), kde je limit {{LIMIT}}",
"no-book-found": "Výběrovým kritériím nevyhovuje žádná kniha",
"url-not-found": "Požadovaná adresa URL \"{{url}}\" nebyla na tomto serveru nalezena.",
"suggest-search": "Proveďte fulltextové vyhledávání <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Jejda! Nepodařilo se vybrat náhodný článek :(",
"invalid-raw-data-type": "{{DATATYPE}} není platný požadavek na nezpracovaný obsah.",
"no-value-for-arg": "Pro argument {{ARGUMENT}} nebyla zadána žádná hodnota",
"no-query": "Nebyl poskytnut žádný dotaz.",
"raw-entry-not-found": "Nelze najít položku {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Neplatný požadavek",
"400-page-heading": "Neplatný požadavek",
"404-page-title": "Obsah nenalezen",
"404-page-heading": "Nenalezeno",
"500-page-title": "Interní chyba serveru",
"500-page-heading": "Interní chyba serveru",
"fulltext-search-unavailable": "Fulltextové vyhledávání není k dispozici",
"no-search-results": "Fulltextový vyhledávač není pro tento obsah dostupný.",
"library-button-text": "Přejít na uvítací stránku",
"home-button-text": "Přejít na hlavní stránku '{{BOOK_TITLE}}'",
"random-page-button-text": "Přejít na náhodně vybranou stránku",
"searchbox-tooltip": "Hledat '{{BOOK_TITLE}}'"
}

View File

@@ -1,27 +1,15 @@
{
"@metadata": {
"authors": [
"Albano",
"Beta16"
]
},
"name": "italiano",
"suggest-full-text-search": "contenente '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Nessun libro del genere: {{BOOK_NAME}}",
"too-many-books": "Troppi libri richiesti ({{NB_BOOKS}}) dove il limite è {{LIMIT}}",
"no-book-found": "Nessun libro corrisponde ai criteri di selezione",
"url-not-found": "L'URL richiesto \"{{url}}\" non è stato trovato in questo server.",
"suggest-search": "Effettua una ricerca di testo completo per <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ops! Impossibile selezionare un articolo casuale :(",
"no-value-for-arg": "Nessun valore fornito per l'argomento {{ARGUMENT}}",
"400-page-title": "Richiesta non valida",
"400-page-heading": "Richiesta non valida",
"404-page-title": "Contenuto non trovato",
"404-page-heading": "Non trovato",
"500-page-title": "Errore interno del server",
"500-page-heading": "Errore interno del server",
"library-button-text": "Vai alla pagina di benvenuto",
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'",
"random-page-button-text": "Vai a una pagina selezionata casualmente",
"searchbox-tooltip": "Cerca '{{BOOK_TITLE}}'"
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'"
}

View File

@@ -1,31 +0,0 @@
{
"@metadata": {
"authors": [
"Lancine.kounfantoh.fofana"
]
},
"name": "ߒߞߏ",
"suggest-full-text-search": "ߞߣߐߞߍߣߍ߲߫ ߦߋ߫ '{{{SEARCH_TERMS}}}'...",
"no-such-book": "ߞߊ߬ߝߊ߫ ߛߎ߮ ߏ߬ ߕߴߦߋ߲߬: {{BOOK_NAME}}",
"too-many-books": "ߞߝߊ߬ ߛߌߦߊߡߊ߲߫ ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲߫ ߞߏߖߎ߰ {{NB_BOOKS}} ߡߍ߲ ߞߐߘߊ߲ ߦߋ߫ {{LIMIT}} ߘߌ߫",
"no-book-found": "ߞߝߊ߬ ߛߌ߫ ߓߍ߬ߣߍ߲߬ ߕߍ߫ ߛߎߥߊ߲ߘߟߌ ߛߙߊߕߌ ߏ߬ ߡߊ߬",
"url-not-found": "ߍ߲߬ߤߍ߲߫ ''URL ''{{url}} ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲ ߡߊ߫ ߛߐ߬ߘߐ߲߫ ߡߊ߬ߟߐ߬ߟߊ߲ ߣߌ߲߬ ߠߊ߫.",
"suggest-search": "ߛߓߍߟߌ߫ ߘߝߊߣߍ߲ ߢߌߣߌ߲ߠߌ߲ ߞߍ߫ <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "ߏ߯߹ ߞߢߊ߬ ߓߘߊ߫ ߞߍ߫ ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߎߡߘߊ ߛߐ߬ߘߐ߲߬ ߠߊ߫:(",
"invalid-raw-data-type": "{{DATATYPE}} ߕߍ߫ ߡߞߊ߬ߣߌ߲߬ߣߍ߲߫ ߓߍ߲߬ߣߍ߲߫ ߘߌ߫ ߞߣߐߘߐ߫ ߘߝߊߣߍ߲ ߞߊ߲ߡߊ߬.",
"no-value-for-arg": "ߡߐ߬ߟߐ߲߬ ߡߊ߫ ߡߊߛߐ߫ ߘߊߘߐߡߌߣߊߞߎ߲ߧߊ {{ARGUMENT}} ߞߊ߲ߡߊ߬",
"no-query": "ߡߞߊ߬ߣߌ߲߬ߠߌ߲߫ ߛߌ߫ ߡߊ߫ ߡߊߛߐ߫.",
"raw-entry-not-found": "ߟߊ߬ߘߏ߲߬ߣߍ߲ {{ENTRY}} {{DATATYPE}} ߕߍ߫ ߣߊ߬ ߡߊߛߐ߬ߘߐ߲߫ ߠߊ߫",
"400-page-title": "ߡߞߊ߬ߣߌ߲߬ߠߌ߲߫ ߓߍ߲߬ߓߊߟߌ",
"400-page-heading": "ߡߞߊ߬ߣߌ߲߬ߠߌ߲߫ ߓߍ߲߬ߓߊߟߌ",
"404-page-title": "ߞߣߐߘߐ ߡߊ߫ ߛߐ߬ߘߐ߲߫",
"404-page-heading": "ߡߊ߫ ߛߐ߬ߘߐ߲߫",
"500-page-title": "ߞߣߐߟߊ ߡߊ߬ߛߐ߬ߟߊ߲ ߝߎ߬ߕߎ߲߬ߕߌ",
"500-page-heading": "ߞߣߐߟߊ ߡߊ߬ߛߐ߬ߟߊ߲ ߝߎ߬ߕߎ߲߬ߕߌ",
"fulltext-search-unavailable": "ߛߓߍߟߌ߫ ߘߝߊߣߍ߲ ߢߌߣߌ߲ߠߌ߲ ߕߍ߫ ߦߋ߲߬",
"no-search-results": "ߛߓߍߟߌ߫ ߘߝߊߣߍ߲ ߢߌߣߌ߲ߠߌ߲߫ ߣߌߘߐ ߕߍ߫ ߦߋ߲߬ ߞߣߐߘߐ ߣߌ߲߬ ߢߍ߫.",
"library-button-text": "ߕߊ߯ ߟߊ߬ߛߣߍ߬ߟߌ߫ ߞߐߜߍ ߞߊ߲߬",
"home-button-text": "ߕߊ߯ {{BOOK_TITLE}} ߓߏ߬ߟߏ߲߬ߘߊ ߞߐߜߍ ߞߊ߲߬",
"random-page-button-text": "ߕߊ߯ ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ߫ ߛߎߥߊ߲ߘߌߣߍ߲ ߠߎ߬ ߞߊ߲߬",
"searchbox-tooltip": "ߕߌߙߌ߲ߠߌ߲ {{BOOK_TITLE}}"
}

View File

@@ -3,22 +3,15 @@
"authors": [
"Fenixs-ru",
"Kareyac",
"Okras",
"Pacha Tchernof"
]
},
"name": "русский",
"suggest-full-text-search": "содержащее '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Такой книги нет: {{BOOK_NAME}}",
"too-many-books": "Запрошено слишком много книг ({{NB_BOOKS}}), максимальное количество — {{LIMIT}}.",
"no-book-found": "Ни одна книга не соответствует критериям отбора",
"url-not-found": "Запрошенный URL \"{{url}}\" не найден на этом сервере.",
"suggest-search": "Выполните полнотекстовый поиск для <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ой! Не удалось выбрать случайную статью :(",
"invalid-raw-data-type": "{{DATATYPE}} не является допустимым запросом необработанного контента.",
"no-value-for-arg": "Не указано значение для аргумента {{ARGUMENT}}",
"no-query": "Не предоставлен запрос.",
"raw-entry-not-found": "Не удаётся найти запись {{ENTRY}} типа {{DATATYPE}}",
"400-page-title": "Недействительный запрос",
"400-page-heading": "Недействительный запрос",
"404-page-title": "Содержание не найдено",

View File

@@ -7,14 +7,10 @@
"name": "Sardu",
"suggest-full-text-search": "chi cuntenet '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Perunu libru cun custu nùmene: {{BOOK_NAME}}",
"too-many-books": "Tropu libros pedidos, {{NB_BOOKS}} cando su lìmite est de {{LIMIT}}",
"no-book-found": "Perunu libru currispondet a sos critèrios de seletzione",
"url-not-found": "S'URL pedidu \"{{url}}\" non s'est atzapadu in custu serbidore.",
"suggest-search": "Faghe una chirca de testu intreu pro <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oops! Sa seletzione de un'artìculu a casu est fallida :(",
"invalid-raw-data-type": "{{DATATYPE}} no est una rechesta vàlida pro cuntenutu puru.",
"no-value-for-arg": "Perunu valore frunidu pro s'argumentu {{ARGUMENT}}",
"no-query": "Peruna chirca frunida.",
"raw-entry-not-found": "Non faghet a atzapare s'elementu {{ENTRY}} de genia {{DATATYPE}}",
"400-page-title": "Rechesta non vàlida",
"400-page-heading": "Rechesta non vàlida",

View File

@@ -7,14 +7,10 @@
"name": "slovenčina",
"suggest-full-text-search": "obsahuje '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Žiadna kniha ako: {{BOOK_NAME}}",
"too-many-books": "Príliš veľa požadovaných kníh ({{NB_BOOKS}}), limit je {{LIMIT}}",
"no-book-found": "Kritériám výberu nevyhovuje žiadna kniha",
"url-not-found": "Požadovaná adresa URL \"{{url}}\" na tomto serveri nebola nájdená.",
"suggest-search": "Spustite hľadanie celého textu <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Nepodarilo sa vybrať náhodný článok :(",
"invalid-raw-data-type": "{{DATATYPE}} nie je platná požiadavka pre surový obsah.",
"no-value-for-arg": "Pre argument {{ARGUMENT}} nebola poskytnutá žiadna hodnota",
"no-query": "Nebol poskytnutý žiadny dopyt.",
"raw-entry-not-found": "Nepodarilo sa nájsť {{DATATYPE}} položka {{ENTRY}}",
"400-page-title": "Neplatná požiadavka",
"400-page-heading": "Neplatná požiadavka",

View File

@@ -1,14 +1,11 @@
{
"@metadata": {
"authors": [
"Sabelöga",
"WikiPhoenix"
"Sabelöga"
]
},
"name": "Svenska",
"suggest-full-text-search": "innehåller '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Ingen sådan bok: {{BOOK_NAME}}",
"too-many-books": "För många böcker begärda ({{NB_BOOKS}}) där gränsen är {{LIMIT}}",
"url-not-found": "Den begärda webbadressen \"{{url}}\" hittades inte på denna server.",
"suggest-search": "Utför en fulltextsökning för <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Hoppsan! Kunde inte välja en slumpartikel :(",

View File

@@ -7,14 +7,10 @@
"name": "Türkçe",
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}' içeriyor...",
"no-such-book": "Böyle bir kitap yok: {{BOOK_NAME}}",
"too-many-books": "Sınır {{LIMIT}} olduğunda çok fazla ({{NB_BOOKS}}) kitap istendi",
"no-book-found": "Seçim kriterleriyle eşleşen kitap yok",
"url-not-found": "İstenen \"{{url}}\" URL'si bu sunucuda bulunamadı.",
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> için tam metin araması yapın",
"random-article-failure": "Hata! Rastgele bir madde seçilemedi :(",
"invalid-raw-data-type": "{{DATATYPE}}, ham içerik için geçerli bir istek değil.",
"no-value-for-arg": "{{ARGUMENT}} bağımsız değişkeni için değer sağlanmadı",
"no-query": "Sorgu sağlanmadı.",
"raw-entry-not-found": "{{DATATYPE}} {{ENTRY}} girişi bulunamadı",
"400-page-title": "Geçersiz istek",
"400-page-heading": "Geçersiz istek",

View File

@@ -1,16 +0,0 @@
{
"@metadata": {
"authors": [
"GuoPC",
"StarrySky"
]
},
"name": "英语",
"no-query": "未提供查询。",
"400-page-title": "无效请求",
"400-page-heading": "无效请求",
"404-page-heading": "未找到",
"500-page-title": "内部服务器错误",
"500-page-heading": "内部服务器错误",
"library-button-text": "前往欢迎页面"
}

View File

@@ -1,32 +1,15 @@
{
"@metadata": {
"authors": [
"Kly",
"Winston Sung"
]
},
"name": "繁體中文",
"suggest-full-text-search": "正在包含「{{{SEARCH_TERMS}}}」…",
"no-such-book": "沒有這樣的書籍:{{BOOK_NAME}}",
"too-many-books": "請求太多個書籍({{NB_BOOKS}}),上限是 {{LIMIT}} 個",
"no-book-found": "沒有書籍符合選擇標準",
"url-not-found": "在此伺服器上找不到請求的 URL「{{url}}」。",
"suggest-search": "建立 <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> 使用的全文搜尋",
"random-article-failure": "哎呀!隨機挑選條目失敗 :(",
"invalid-raw-data-type": "{{DATATYPE}}不是原始內容的有效請求。",
"no-value-for-arg": "沒有為引數 {{ARGUMENT}} 提供內容",
"no-query": "未提供查詢。",
"raw-entry-not-found": "找不到{{DATATYPE}}項目{{ENTRY}}",
"400-page-title": "無效請求",
"400-page-heading": "無效請求",
"404-page-title": "查無內容",
"404-page-heading": "查無頁面",
"500-page-title": "內部伺服器錯誤",
"500-page-heading": "內部伺服器錯誤",
"fulltext-search-unavailable": "全文搜尋無效",
"no-search-results": "全文搜尋引擎不適用此內容。",
"library-button-text": "前往歡迎首頁",
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
"random-page-button-text": "前往隨機選取頁面",
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋"
}

View File

@@ -1,5 +1,4 @@
i18n/bn.json
i18n/cs.json
i18n/en.json
i18n/fr.json
i18n/he.json
@@ -9,12 +8,10 @@ i18n/ja.json
i18n/ko.json
i18n/ku-latn.json
i18n/mk.json
i18n/nqo.json
i18n/pl.json
i18n/ru.json
i18n/sc.json
i18n/sk.json
i18n/sv.json
i18n/tr.json
i18n/zh-hans.json
i18n/zh-hant.json

View File

@@ -15,7 +15,7 @@ preprocessed_resources = custom_target('preprocessed_resource_files',
lib_resources = custom_target('resources',
input: preprocessed_resources,
output: ['libkiwix-resources.cpp', 'libkiwix-resources.h'],
output: ['kiwixlib-resources.cpp', 'kiwixlib-resources.h'],
command:[res_compiler,
'--cxxfile', '@OUTPUT0@',
'--hfile', '@OUTPUT1@',

View File

@@ -4,24 +4,28 @@ 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
skin/autoComplete.min.js
skin/widget.js
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/widget.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
@@ -30,7 +34,6 @@ 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

View File

@@ -1,11 +0,0 @@
<!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

@@ -0,0 +1,74 @@
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

@@ -61,7 +61,7 @@ body {
padding: 7px 10px 10px;
cursor: pointer;
}
.kiwixNav__kiwixFilter:-ms-expand {
display: none;
}
@@ -213,7 +213,7 @@ body {
display: grid;
font-family: poppins;
color: black;
padding: 5px 10px 0 7px;
padding: 12px 10px 0 2px;
width: 100%;
height: 100%;
align-items: center;
@@ -482,4 +482,4 @@ body {
.kiwixNav__filters {
grid-template-columns: 1fr;
}
}
}

View File

@@ -1,4 +1,4 @@
(function() {
const kiwixServe = (function() {
const root = document.querySelector(`link[type='root']`).getAttribute('href');
const incrementalLoadingParams = {
start: 0,
@@ -17,6 +17,7 @@
let params = new URLSearchParams(window.location.search || filters || '');
let timer;
let languages = {};
let allowBookClick = true;
function queryUrlBuilder() {
let url = `${root}/catalog/search?`;
@@ -85,7 +86,7 @@
}
function generateBookHtml(book, sort = false) {
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
let link = book.querySelector('link[type="text/html"]').getAttribute('href');
let iconUrl;
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
@@ -110,9 +111,6 @@
} catch {
downloadLink = '';
}
const bookName = link.split('/').pop();
const viewerLink = `${root}/viewer#${bookName}`;
const humanFriendlyZimSize = humanFriendlySize(zimSize);
const divTag = document.createElement('div');
@@ -123,9 +121,12 @@
}
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
if (!allowBookClick) {
link = "javascript:void(0)";
}
divTag.innerHTML = `
<div class="book__wrapper">
<a class="book__link" href="${viewerLink}" data-hover="Preview">
<a class="book__link" href="${link}" data-hover="Preview">
<div class="book__link__wrapper">
<div class="book__icon" ${faviconAttr}></div>
<div class="book__header">
@@ -182,12 +183,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>
@@ -206,7 +207,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" />
@@ -250,14 +251,16 @@
toggleFooter();
}
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
if (kiwixResultText) {
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
loader.style.display = 'none';
return books;
@@ -268,16 +271,20 @@
await fetch(query).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
let optionStr = '';
data.querySelectorAll('entry').forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
const hfTitle = humanFriendlyTitle(title);
if (valueEntryNode == 'language') {
languages[value] = hfTitle;
}
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
});
document.querySelector(nodeQuery).innerHTML += optionStr;
const entryList = data.querySelectorAll('entry');
const nodeQueryElem = document.querySelector(nodeQuery);
if (entryList && nodeQueryElem) {
entryList.forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
const hfTitle = humanFriendlyTitle(title);
if (valueEntryNode == 'language') {
languages[value] = hfTitle;
}
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
});
nodeQueryElem.innerHTML += optionStr;
}
});
}
@@ -392,6 +399,10 @@
});
}
function disableBookClick() {
allowBookClick = false;
}
function addTagElement(tagValue, resetFilter) {
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
tagElement.style.display = 'inline-block';
@@ -432,13 +443,15 @@
}
}
window.addEventListener('resize', (event) => {
function updateBookCount(event) {
if (timer) {clearTimeout(timer)}
timer = setTimeout(() => {
incrementalLoadingParams.count = incrementalLoadingParams.count && viewPortToCount();
loadSubset();
}, 100, event);
});
}
window.addEventListener('resize', (event) => updateBookCount(event));
window.addEventListener('scroll', loadSubset);
@@ -478,10 +491,11 @@
const currentLink = window.location.search;
const newLink = `?${params.toString()}`;
if (currentLink != newLink) {
window.history.pushState({}, null, newLink);
window.history.pushState({}, null, newLink);
}
}
updateVisibleParams();
updateBookCount();
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
if (!window.location.search) {
const browserLang = navigator.language.split('-')[0];
@@ -494,5 +508,10 @@
}
setCookie(filterCookieName, params.toString());
}
return {
updateBookCount,
disableBookClick
};
})();

View File

@@ -1,5 +1,11 @@
#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;
@@ -129,6 +135,10 @@ 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;

122
static/skin/taskbar.js Normal file
View File

@@ -0,0 +1,122 @@
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();
}
});

View File

@@ -1,409 +0,0 @@
// 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();
}
}

107
static/skin/widget.js Normal file
View File

@@ -0,0 +1,107 @@
function disableSearchFilters(widgetStyles) {
const hideNavRule = `
.kiwixNav {
display: none;
}`;
const hideResultsLabelRule = `
.kiwixHomeBody__results {
display: none;
}`;
const hideTagFilterRule = `
.book__tags {
pointer-events: none;
}`;
insertNewCssRules(widgetStyles, [hideNavRule, hideResultsLabelRule, hideTagFilterRule]);
}
function disableBookClick() {
kiwixServe.disableBookClick();
}
function disableDownload(widgetStyles) {
const hideBookDownloadRule = `
.book__download {
display: none;
}`;
insertNewCssRules(widgetStyles, [hideBookDownloadRule]);
}
function disableDescription(widgetStyles) {
const decreaseHeightRule = `
.book__wrapper {
height:128px;
grid-template-rows: 70px 0 1fr 1fr;
}`;
const hideDescRule = `
.book__description {
display: none;
}`;
insertNewCssRules(widgetStyles, [decreaseHeightRule, hideDescRule]);
}
function hideFooter(widgetStyles) {
const hideFooterRule = `
.kiwixfooter {
display: none !important;
}`;
insertNewCssRules(widgetStyles, [hideFooterRule]);
}
function insertNewCssRules(stylesheet, ruleList) {
if (stylesheet) {
for (rule of ruleList) {
stylesheet.insertRule(rule, 0);
}
}
}
function addCustomCss(cssCode) {
let customCSS = document.createElement('style');
customCSS.innerHTML = cssCode;
document.head.appendChild(customCSS);
}
function addCustomJs(jsCode) {
new Function(`"use strict";${jsCode}`)();
}
function handleMessages(event) {
if ('css' in event.data) {
addCustomCss(event.data.css);
}
if ('js' in event.data) {
addCustomJs(event.data.js);
}
}
function handleWidget() {
const params = new URLSearchParams(window.location.search || filters || '');
const widgetStyleElem = document.createElement('style');
document.head.appendChild(widgetStyleElem);
const widgetStyles = widgetStyleElem.sheet;
const disableFilters = params.has('disablefilter');
const disableClick = params.has('disableclick');
const disableDwld = params.has('disabledownload');
const disableDesc = params.has('disabledesc');
const blankBase = document.createElement('base');
blankBase.target = '_blank';
document.head.appendChild(blankBase); // open all links in new tab
if (disableFilters)
disableSearchFilters(widgetStyles);
if (disableClick)
disableBookClick();
if (disableDwld)
disableDownload(widgetStyles);
if (disableDesc)
disableDescription(widgetStyles);
hideFooter(widgetStyles);
kiwixServe.updateBookCount();
}
window.addEventListener('message', handleMessages);
handleWidget();

View File

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

View File

@@ -0,0 +1,4 @@
<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,7 +3,6 @@
<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"

View File

@@ -0,0 +1,25 @@
<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

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

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Kiwix Server</title>
<link
type="text/css"
href="{{root}}/skin/index.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<style>
@font-face {
font-family: "poppins";
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
}
@font-face {
font-family: "roboto";
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
}
</style>
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
<script src="{{root}}/skin/iso6391To3.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="{{root}}/skin/widget.js?KIWIXCACHEID" defer></script>
</head>
<body>
<div class='kiwixNav'>
<div class="kiwixNav__filters">
<div class="kiwixNav__select">
<select name="lang" id="languageFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All languages</option>
</select>
</div>
<div class="kiwixNav__select">
<select name="category" id="categoryFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All categories</option>
</select>
</div>
</div>
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<span class="kiwixButton tagFilterLabel"></span>
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
</form>
</div>
<div class="kiwixHomeBody">
<div class="book__list">
<h3 class="kiwixHomeBody__results"></h3>
</div>
<div id="fadeOut" class="fadeOut"></div>
</div>
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
<div id="kiwixfooter" class="kiwixfooter">Powered by&nbsp;<a href="https://kiwix.org">Kiwix</a></div>
</body>
<script>
function closeModal() {
for(modal of document.getElementsByClassName('modal-wrapper')) {
modal.remove();
}
}
</script>
</html>

View File

@@ -1,68 +0,0 @@
<!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

@@ -500,24 +500,6 @@ TEST_F(LibraryTest, filterByTags)
);
}
TEST_F(LibraryTest, filterByAliasNames)
{
// filtering for one book
EXPECT_FILTER_RESULTS(kiwix::Filter().aliasNames({"zimfile"}),
"Ray Charles"
);
// filerting for more than one book
EXPECT_FILTER_RESULTS(kiwix::Filter().aliasNames({"zimfile", "example"}),
"An example ZIM archive",
"Ray Charles"
);
// filtering by alias name requires full text match
EXPECT_FILTER_RESULTS(kiwix::Filter().aliasNames({"wrong_name"}),
/* no results */
);
}
TEST_F(LibraryTest, filterByQuery)
{

View File

@@ -69,7 +69,7 @@ if gtest_dep.found() and not meson.is_cross_build()
test_exe = executable(test_name, [test_name+'.cpp'],
implicit_include_directories: false,
include_directories : inc,
link_with : libkiwix,
link_with : kiwixlib,
link_args: extra_link_args,
dependencies : all_deps + [gtest_dep],
build_rpath : '$ORIGIN')

View File

@@ -73,4 +73,31 @@ 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");
}
};

View File

@@ -43,9 +43,9 @@ typedef std::vector<Resource> ResourceCollection;
const ResourceCollection resources200Compressible{
{ WITH_ETAG, "/ROOT/" },
{ WITH_ETAG, "/ROOT/skin/autoComplete.min.js" },
{ WITH_ETAG, "/ROOT/skin/css/autoComplete.css" },
{ WITH_ETAG, "/ROOT/skin/taskbar.js" },
{ WITH_ETAG, "/ROOT/skin/taskbar.css" },
{ WITH_ETAG, "/ROOT/skin/block_external.js" },
{ NO_ETAG, "/ROOT/catalog/search" },
@@ -53,6 +53,8 @@ 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" },
@@ -62,7 +64,6 @@ 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" },
@@ -75,8 +76,6 @@ 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" },
@@ -104,7 +103,7 @@ TEST(indexTemplateStringTest, emptyIndexTemplate) {
"./test/corner_cases.zim"
};
ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, "");
ZimFileServer zfs(PORT, /*withTaskbar=*/true, ZIMFILES, "");
EXPECT_EQ(200, zfs.GET("/ROOT/")->status);
}
@@ -115,12 +114,13 @@ TEST(indexTemplateStringTest, indexTemplateCheck) {
"./test/corner_cases.zim"
};
ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, "<!DOCTYPE html><head>"
ZimFileServer zfs(PORT, /*withTaskbar=*/true, 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);
}
@@ -172,7 +172,7 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
const std::vector<UrlAndExpectedResult> testData{
{
/* url */ "/ROOT/",
R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=3b470cee"
R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=56e818cd"
<link rel="apple-touch-icon" sizes="180x180" href="/ROOT/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3">
<link rel="icon" type="image/png" sizes="32x32" href="/ROOT/skin/favicon/favicon-32x32.png?cacheid=79ded625">
<link rel="icon" type="image/png" sizes="16x16" href="/ROOT/skin/favicon/favicon-16x16.png?cacheid=a986fedc">
@@ -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=2f5a81ac" defer></script>
<script type="text/javascript" src="/ROOT/skin/index.js?cacheid=2fcc4ac4" defer></script>
)EXPECTEDRESULT"
},
{
@@ -193,27 +193,27 @@ R"EXPECTEDRESULT( <img src="../skin/download.png?
<img src="../skin/hash.png?cacheid=f836e872" alt="download hash" />
<img src="../skin/magnet.png?cacheid=73b6bddf" alt="download magnet" />
<img src="../skin/bittorrent.png?cacheid=4f5c6882" alt="download torrent" />
)EXPECTEDRESULT"
},
{
/* 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",
""
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>
)EXPECTEDRESULT"
},
{
// 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,8 +429,13 @@ 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
@@ -446,8 +451,40 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
)FRAG",
R"FRAG(
</head>
<body>)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">&#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>
</html>
@@ -459,8 +496,18 @@ 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[3];
+ frag[8];
}
std::string TestContentIn404HtmlResponse::pageTitle() const
@@ -480,6 +527,71 @@ 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>&#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
{
public:
@@ -1027,13 +1139,11 @@ 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("<link type=\"root\" href=\"/ROOT\">") != std::string::npos);
*/
EXPECT_TRUE(p->body.find("taskbar") != std::string::npos);
}
TEST_F(ServerTest, HeadMethodIsSupported)
@@ -1090,7 +1200,7 @@ TEST_F(ServerTest, ETagIsTheSameAcrossHeadAndGet)
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETags)
{
ZimFileServer zfs2(SERVER_PORT + 1, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES);
ZimFileServer zfs2(SERVER_PORT + 1, /*withTaskbar=*/true, ZIMFILES);
for ( const Resource& res : all200Resources() ) {
if ( !res.etag_expected ) continue;
const auto h1 = zfs1_->HEAD(res.url);
@@ -1481,54 +1591,3 @@ 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
}
)");
}
}

View File

@@ -112,7 +112,7 @@ std::string makeSearchResultsHtml(const std::string& pattern,
</style>
<title>Search: %PATTERN%</title>
</head>
<link type="root" href="/ROOT"></head>
<body bgcolor="white">
<div class="header">
%HEADER%
@@ -1453,13 +1453,16 @@ TEST_F(ServerTest, searchResults)
for ( const auto& t : testData ) {
const std::string htmlSearchUrl = t.url();
const auto htmlRes = zfs1_->GET(htmlSearchUrl.c_str());
const auto htmlRes = taskbarlessZimFileServer().GET(htmlSearchUrl.c_str());
EXPECT_EQ(htmlRes->status, 200);
t.checkHtml(htmlRes->body);
const std::string xmlSearchUrl = t.xmlSearchUrl();
const auto xmlRes = zfs1_->GET(xmlSearchUrl.c_str());
EXPECT_EQ(xmlRes->status, 200);
t.checkXml(xmlRes->body);
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);
}
}

View File

@@ -54,23 +54,10 @@ 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,
Options options,
bool withTaskbar,
const FilePathCollection& zimpaths,
std::string indexTemplateString = "");
~ZimFileServer();
@@ -94,7 +81,7 @@ private: // data
std::unique_ptr<kiwix::HumanReadableNameMapper> nameMapper;
std::unique_ptr<kiwix::Server> server;
std::unique_ptr<httplib::Client> client;
const Options options = DEFAULT_OPTIONS;
const bool withTaskbar = true;
};
ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
@@ -107,11 +94,11 @@ ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
}
ZimFileServer::ZimFileServer(int serverPort,
Options _options,
bool _withTaskbar,
const FilePathCollection& zimpaths,
std::string indexTemplateString)
: manager(&this->library)
, options(_options)
, withTaskbar(_withTaskbar)
{
for ( const auto& zimpath : zimpaths ) {
if (!manager.addBookFromPath(zimpath, zimpath, "", false))
@@ -130,8 +117,7 @@ void ZimFileServer::run(int serverPort, std::string indexTemplateString)
server->setPort(serverPort);
server->setNbThreads(2);
server->setVerbose(false);
server->setTaskbar(options & WITH_TASKBAR, options & WITH_LIBRARY_BUTTON);
server->setBlockExternalLinks(options & BLOCK_EXTERNAL_LINKS);
server->setTaskbar(withTaskbar, withTaskbar);
server->setMultiZimSearchLimit(3);
if (!indexTemplateString.empty()) {
server->setIndexTemplateString(indexTemplateString);
@@ -150,6 +136,9 @@ ZimFileServer::~ZimFileServer()
class ServerTest : public ::testing::Test
{
private:
std::unique_ptr<ZimFileServer> taskbarlessZfs_;
protected:
std::unique_ptr<ZimFileServer> zfs1_;
@@ -162,15 +151,19 @@ protected:
protected:
void SetUp() override {
resetServer(ZimFileServer::DEFAULT_OPTIONS);
zfs1_.reset(new ZimFileServer(SERVER_PORT, /*withTaskbar=*/true, ZIMFILES));
}
void resetServer(ZimFileServer::Options options) {
zfs1_.reset();
zfs1_.reset(new ZimFileServer(SERVER_PORT, options, ZIMFILES));
ZimFileServer& taskbarlessZimFileServer()
{
if ( ! taskbarlessZfs_ ) {
taskbarlessZfs_.reset(new ZimFileServer(SERVER_PORT+1, /*withTaskbar=*/false, ZIMFILES));
}
return *taskbarlessZfs_;
}
void TearDown() override {
zfs1_.reset();
taskbarlessZfs_.reset();
}
};