Compare commits

..

6 Commits

Author SHA1 Message Date
Nikhil Tanwar
2cd057941e Add test for aliasName filtering
Adds test to check if filtering by alias name works.
2022-09-24 20:16:20 +02:00
Nikhil Tanwar
8a3a0b08c2 fixup! Add catalog filtering using ZIM aliasname 2022-09-24 20:16:20 +02:00
Nikhil Tanwar
956c597e80 fixup! Extract parseQuery() 2022-09-24 20:16:20 +02:00
Nikhil Tanwar
bd38ea97f9 Multivalue support for book query
Adds support for putting multiple `book` query parameter.
2022-09-24 20:16:20 +02:00
Nikhil Tanwar
48a0b3bdc7 Add catalog filtering using ZIM aliasname
Adds mechanism to get a ZIM using its alias name.
To make a search, one needs to visit:
`TLD/?book=aliasNameHere`
2022-09-24 20:16:20 +02:00
Nikhil Tanwar
b84eaad748 Extract parseQuery()
Extracts the duplicate code from publisherQuery() and nameQuery() into a new function parseQuery().
2022-09-24 20:16:20 +02:00
62 changed files with 839 additions and 1990 deletions

View File

@@ -12,14 +12,14 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup python 3.9
uses: actions/setup-python@v1
- name: Setup python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.9'
python-version: '3.10'
- name: Install packages
run: |
brew update
brew install gcovr pkg-config ninja || brew link --overwrite python
brew install gcovr pkg-config ninja
- name: Install python modules
run: pip3 install meson==0.49.2 pytest
- name: Install deps

View File

@@ -8,8 +8,8 @@ jobs:
fail-fast: false
matrix:
distro:
- ubuntu-kinetic
- ubuntu-jammy
- ubuntu-impish
- ubuntu-focal
- ubuntu-bionic
steps:
@@ -34,14 +34,6 @@ jobs:
email: release+launchpad@kiwix.org
distro: ${{ matrix.distro }}
- uses: legoktm/gh-action-build-deb@ubuntu-kinetic
if: matrix.distro == 'ubuntu-kinetic'
name: Build package for ubuntu-kinetic
id: build-ubuntu-kinetic
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
if: matrix.distro == 'ubuntu-jammy'
name: Build package for ubuntu-jammy
@@ -50,6 +42,14 @@ jobs:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-impish
if: matrix.distro == 'ubuntu-impish'
name: Build package for ubuntu-impish
id: build-ubuntu-impish
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-focal
if: matrix.distro == 'ubuntu-focal'
name: Build package for ubuntu-focal

View File

@@ -1,43 +1,3 @@
libkiwix 12.0.0
===============
* [API Break] Remove wrapper around libzim (@mgautierfr #789)
* Allow kiwix-serve to use custom resource files (@veloman-yunkan #779)
* Properly handle searchProtocolPrefix when rendering search result (@veloman-yunkan #823)
* Prevent search on multi language content (@veloman-yunkan #838)
* Use new `zim::Archive::getMediaCount` from libzim (@mgautierfr #836)
* Catalog:
- Include tags in free text catalog search (@veloman-yunkan #802)
- Illustration's url is based on book's uuid (@veloman-yunkan #804)
- Cleanup of the opds-dumper (@veloman-yunkan #829)
- Allow filtering of catalog content using multiple languages (@veloman-yunkan #841)
- Make opds-dumper respect the namemapper (@mgautierfr #837)
* Server:
- Correctly handle `\` in suggestion json generation (@veloman-yunkan #843)
- Better http caching (@veloman-yunkan #833)
- Make `/suggest` endpoint thread-safe (@veloman-yunkan #834)
- Better redirection of main page (@veloman-yunkan #827)
- Remove jquery (@mgautierfr @juuz0 #796)
- Better Viewer of zim content :
. Introduce `/content` endpoints (@veloman-yunkan #806)
. Switch to iframe based content viewer (@veloman-yunkan #716)
- Optimised design of the welcome page:
. Alignement (@juuz0 @kelson42 #786)
. Exit download modal on pressing escape key (@juzz0 #800)
. Add favicon for different devices (@juzz0 #805)
. Fix auto hidding of the toolbar (@veloman-yunkan #821)
. Allow user to filter books by tags in the front page (@juuz0 #711)
* CI :
- Trigger CI on pull_request (@kelson42 #791)
- Drop Ubuntu Impish packaging (@legoktm #825)
- Add Ubuntu Kinetic packaging (@legoktm #801)
* Testing:
- Test ICULanguageInfo (@veloman-yunkan #795)
- Introduce fake `test` language to test i18n (@veloman-yunkan #848)
* Fix documentation (@kelson42 #816)
* Udpate translation (#787 #839 #847)
libkiwix 11.0.0
===============
@@ -45,7 +5,7 @@ libkiwix 11.0.0
* [server] Use gzip compression instead of deflat (mgautierfr #757)
* [server] Version the static resources. This allow better invalidating
browser cache when resources are changed (@veloman-yunkan #712)
* [server|front] Use integer to query the host for page length (@juuz0 #772)
* [server|front] Use integer to query the host for page length (@juuz #772)
* [server] Improve multizim search API:
- Improvement of the cache system
- Better API to select on which books to search in.

38
format_code.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/bash
files=(
"include/library.h"
"include/common/stringTools.h"
"include/common/pathTools.h"
"include/common/otherTools.h"
"include/common/regexTools.h"
"include/common/networkTools.h"
"include/common/archiveTools.h"
"include/manager.h"
"include/reader.h"
"include/kiwix.h"
"include/xapianSearcher.h"
"include/searcher.h"
"src/library.cpp"
"src/android/kiwix.cpp"
"src/android/org/kiwix/kiwixlib/JNIKiwixBool.java"
"src/android/org/kiwix/kiwixlib/JNIKiwix.java"
"src/android/org/kiwix/kiwixlib/JNIKiwixString.java"
"src/android/org/kiwix/kiwixlib/JNIKiwixInt.java"
"src/searcher.cpp"
"src/common/pathTools.cpp"
"src/common/regexTools.cpp"
"src/common/otherTools.cpp"
"src/common/archiveTools.cpp"
"src/common/networkTools.cpp"
"src/common/stringTools.cpp"
"src/xapianSearcher.cpp"
"src/manager.cpp"
"src/reader.cpp"
)
for i in "${files[@]}"
do
echo $i
clang-format -i -style=file $i
done

View File

@@ -54,6 +54,7 @@ enum supportedListMode {
class Filter {
public: // types
using Tags = std::vector<std::string>;
using AliasNames = std::vector<std::string>;
private: // data
uint64_t activeFilters;
@@ -67,6 +68,7 @@ class Filter {
std::string _query;
bool _queryIsPartial;
std::string _name;
AliasNames _aliasNames;
public: // functions
Filter();
@@ -106,20 +108,13 @@ class Filter {
Filter& rejectTags(const Tags& tags);
Filter& category(std::string category);
/**
* Set the filter to only accept books in the specified language.
*
* Multiple languages can be specified as a comma-separated list (in
* which case a book in any of those languages will match).
*/
Filter& lang(std::string lang);
Filter& publisher(std::string publisher);
Filter& creator(std::string creator);
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; }
@@ -143,6 +138,8 @@ 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;
@@ -340,8 +337,8 @@ class Library
/**
* Return the current revision of the library.
*
* The revision of the library is updated (incremented by one) by
* the addBook() and removeBookById() operations.
* The revision of the library is updated (incremented by one) only by
* the addBook() operation.
*
* @return Current revision of the library.
*/

View File

@@ -54,6 +54,7 @@ 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

@@ -27,7 +27,6 @@
#include <pugixml.hpp>
#include "library.h"
#include "name_mapper.h"
using namespace std;
@@ -42,7 +41,7 @@ class OPDSDumper
{
public:
OPDSDumper() = default;
OPDSDumper(Library* library, NameMapper* NameMapper);
OPDSDumper(Library* library);
~OPDSDumper();
/**
@@ -111,7 +110,6 @@ class OPDSDumper
protected:
kiwix::Library* library;
kiwix::NameMapper* nameMapper;
std::string libraryId;
std::string rootLocation;
int m_totalResults;

View File

@@ -1,5 +1,5 @@
project('libkiwix', 'cpp',
version : '12.0.0',
version : '11.0.0',
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
@@ -35,7 +35,7 @@ else
error('Cannot found header mustache.hpp')
endif
libzim_dep = dependency('libzim', version : '>=8.1.0', static:static_deps)
libzim_dep = dependency('libzim', version : '>=7.2.0', static:static_deps)
if not compiler.has_header_symbol('zim/zim.h', 'LIBZIM_WITH_XAPIAN')
error('Libzim seems to be compiled without xapian. Xapian support is mandatory.')
endif

View File

@@ -1,14 +0,0 @@
#!/usr/bin/bash
# Compute 'src' path
SCRIPT_DIR=$(dirname "$0")
REPO_DIR=$(readlink -f "$SCRIPT_DIR"/..)
DIRS="src include"
# Apply formating to all *.cpp and *.h files
cd "$REPO_DIR"
for FILE in $(find $DIRS -name '*.h' -o -name '*.cpp')
do
echo $FILE
clang-format -i -style=file "$FILE"
done

View File

@@ -52,21 +52,15 @@ resource_getter_template = """
return RESOURCE::{identifier};
"""
resource_cacheid_getter_template = """
if (name == "{common_name}")
return "{cacheid}";
"""
resource_decl_template = """{namespaces_open}
extern const std::string {identifier};
{namespaces_close}"""
class Resource:
def __init__(self, base_dirs, filename, cacheid=None):
filename = filename
def __init__(self, base_dirs, filename):
filename = filename.strip()
self.filename = filename
self.identifier = full_identifier(filename)
self.cacheid = cacheid
found = False
for base_dir in base_dirs:
try:
@@ -77,7 +71,7 @@ class Resource:
except FileNotFoundError:
continue
if not found:
raise Exception("Resource not found: {}".format(filename))
raise Exception("Impossible to found {}".format(filename))
def dump_impl(self):
nb_row = len(self.data)//16 + (1 if len(self.data) % 16 else 0)
@@ -99,12 +93,6 @@ class Resource:
identifier="::".join(self.identifier)
)
def dump_cacheid_getter(self):
return resource_cacheid_getter_template.format(
common_name=self.filename,
cacheid=self.cacheid
)
def dump_decl(self):
return resource_decl_template.format(
namespaces_open=" ".join("namespace {} {{".format(id) for id in self.identifier[:-1]),
@@ -135,12 +123,7 @@ static std::string init_resource(const char* name, const unsigned char* content,
const std::string& getResource_{basename}(const std::string& name) {{
{RESOURCES_GETTER}
throw ResourceNotFound("Resource not found: " + name);
}}
const char* getResourceCacheId_{basename}(const std::string& name) {{
{RESOURCE_CACHEID_GETTER}
return nullptr;
throw ResourceNotFound("Resource not found.");
}}
{RESOURCES}
@@ -151,7 +134,6 @@ def gen_c_file(resources, basename):
return master_c_template.format(
RESOURCES="\n\n".join(r.dump_impl() for r in resources),
RESOURCES_GETTER="\n\n".join(r.dump_getter() for r in resources),
RESOURCE_CACHEID_GETTER="\n\n".join(r.dump_cacheid_getter() for r in resources if r.cacheid is not None),
include_file=basename,
basename=to_identifier(basename)
)
@@ -177,10 +159,8 @@ class ResourceNotFound : public std::runtime_error {{
}};
const std::string& getResource_{basename}(const std::string& name);
const char* getResourceCacheId_{basename}(const std::string& name);
#define getResource(a) (getResource_{basename}(a))
#define getResourceCacheId(a) (getResourceCacheId_{basename}(a))
#endif // KIWIX_{BASENAME}
@@ -209,8 +189,8 @@ if __name__ == "__main__":
base_dir = os.path.dirname(os.path.realpath(args.resource_file))
source_dir = args.source_dir or []
with open(args.resource_file, 'r') as f:
resources = [Resource([base_dir]+source_dir, *line.strip().split())
for line in f.readlines()]
resources = [Resource([base_dir]+source_dir, filename)
for filename in f.readlines()]
h_identifier = to_identifier(os.path.basename(args.hfile))
with open(args.hfile, 'w') as f:

View File

@@ -99,24 +99,16 @@ def preprocess_resource(resource_path):
print(preprocessed_content, end='', file=target)
def copy_resource_list_file(src_path, dst_path):
with open(src_path, 'r') as src:
with open(dst_path, 'w') as dst:
for line in src:
res = line.strip()
if line.startswith("skin/") and res in resource_revisions:
dst.write(res + " " + resource_revisions[res] + "\n")
else:
dst.write(line)
def copy_file(src_path, dst_path):
with open(src_path, 'rb') as src:
with open(dst_path, 'wb') as dst:
dst.write(src.read())
def preprocess_resources(resource_file_path):
resource_filename = os.path.basename(resource_file_path)
for resource in read_resource_file(resource_file_path):
if resource.startswith('skin/'):
get_resource_revision(resource)
else:
preprocess_resource(resource)
copy_resource_list_file(resource_file_path, os.path.join(OUT_DIR, resource_filename))
preprocess_resource(resource)
copy_file(resource_file_path, os.path.join(OUT_DIR, resource_filename))
if __name__ == "__main__":
parser = argparse.ArgumentParser()

View File

@@ -66,7 +66,7 @@ bool Book::update(const kiwix::Book& other)
void Book::update(const zim::Archive& archive) {
m_path = archive.getFilename();
m_pathValid = true;
m_id = std::string(archive.getUuid());
m_id = getArchiveId(archive);
m_title = getArchiveTitle(archive);
m_description = getMetaDescription(archive);
m_language = getMetaLanguage(archive);
@@ -77,8 +77,8 @@ void Book::update(const zim::Archive& archive) {
m_flavour = getMetaFlavour(archive);
m_tags = getMetaTags(archive);
m_category = getCategoryFromTags();
m_articleCount = archive.getArticleCount();
m_mediaCount = archive.getMediaCount();
m_articleCount = getArchiveArticleCount(archive);
m_mediaCount = getArchiveMediaCount(archive);
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
m_illustrations.clear();

View File

@@ -28,6 +28,7 @@
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "tools/concurrent_cache.h"
#include "name_mapper.h"
#include <pugixml.hpp>
#include <algorithm>
@@ -221,11 +222,7 @@ bool Library::removeBookById(const std::string& id)
// Having a too big cache is not a problem here (or it would have been before)
// (And setMaxSize doesn't actually reduce the cache size, extra cached items
// will be removed in put or getOrPut).
const bool bookWasRemoved = mp_impl->m_books.erase(id) == 1;
if ( bookWasRemoved ) {
++mp_impl->m_revision;
}
return bookWasRemoved;
return mp_impl->m_books.erase(id) == 1;
}
Library::Revision Library::getRevision() const
@@ -465,6 +462,9 @@ 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,6 +509,7 @@ 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;
@@ -525,6 +526,16 @@ 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));
@@ -535,40 +546,31 @@ Xapian::Query categoryQuery(const std::string& category)
return Xapian::Query("XC" + normalizeText(category));
}
Xapian::Query langQuery(const std::string& commaSeparatedLanguageList)
Xapian::Query aliasNamesQuery(const Filter::AliasNames& aliasNames)
{
Xapian::Query q;
bool firstIteration = true;
for ( const auto& lang : kiwix::split(commaSeparatedLanguageList, ",") ) {
const Xapian::Query singleLangQuery("L" + normalizeText(lang));
if ( firstIteration ) {
q = singleLangQuery;
firstIteration = false;
} else {
q = Xapian::Query(Xapian::Query::OP_OR, q, singleLangQuery);
}
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));
}
Xapian::Query publisherQuery(const std::string& publisher)
{
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());
return makePhraseQuery(publisher, "XP");
}
Xapian::Query creatorQuery(const std::string& creator)
{
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());
return makePhraseQuery(creator, "A");
}
Xapian::Query tagsQuery(const Filter::Tags& acceptTags, const Filter::Tags& rejectTags)
@@ -608,6 +610,9 @@ 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;
}
@@ -757,6 +762,7 @@ enum filterTypes {
QUERY = FLAG(12),
NAME = FLAG(13),
CATEGORY = FLAG(14),
ALIASNAMES = FLAG(15),
};
Filter& Filter::local(bool accept)
@@ -859,6 +865,13 @@ 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

@@ -34,7 +34,7 @@ HumanReadableNameMapper::HumanReadableNameMapper(kiwix::Library& library, bool w
if (!withAlias)
continue;
auto aliasName = replaceRegex(bookName, "", "_[[:digit:]]{4}-[[:digit:]]{2}$");
auto aliasName = removeDateFromBookId(bookName);
if (aliasName == bookName) {
continue;
}
@@ -51,6 +51,10 @@ 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

@@ -30,9 +30,8 @@ namespace kiwix
{
/* Constructor */
OPDSDumper::OPDSDumper(Library* library, NameMapper* nameMapper)
: library(library),
nameMapper(nameMapper)
OPDSDumper::OPDSDumper(Library* library)
: library(library)
{
}
/* Destructor */
@@ -50,8 +49,6 @@ void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
namespace
{
const std::string XML_HEADER(R"(<?xml version="1.0" encoding="UTF-8"?>)");
typedef kainjow::mustache::data MustacheData;
typedef kainjow::mustache::list BooksData;
typedef kainjow::mustache::list IllustrationInfo;
@@ -72,17 +69,16 @@ IllustrationInfo getBookIllustrationInfo(const Book& book)
return illustrations;
}
std::string fullEntryXML(const Book& book, const std::string& rootLocation, const std::string& contentId)
kainjow::mustache::object getSingleBookData(const Book& book)
{
const auto bookDate = book.getDate() + "T00:00:00Z";
const kainjow::mustache::object data{
{"root", rootLocation},
return kainjow::mustache::object{
{"id", book.getId()},
{"name", book.getName()},
{"title", book.getTitle()},
{"description", book.getDescription()},
{"language", book.getLanguage()},
{"content_id", urlEncode(contentId, true)},
{"content_id", urlEncode(book.getHumanReadableIdFromPath(), true)},
{"updated", bookDate}, // XXX: this should be the entry update datetime
{"book_date", bookDate},
{"category", book.getCategory()},
@@ -96,34 +92,27 @@ std::string fullEntryXML(const Book& book, const std::string& rootLocation, cons
{"size", to_string(book.getSize())},
{"icons", getBookIllustrationInfo(book)},
};
return render_template(RESOURCE::templates::catalog_v2_entry_xml, data);
}
std::string partialEntryXML(const Book& book, const std::string& rootLocation)
std::string getSingleBookEntryXML(const Book& book, bool withXMLHeader, const std::string& rootLocation, const std::string& endpointRoot, bool partial)
{
const auto bookDate = book.getDate() + "T00:00:00Z";
const kainjow::mustache::object data{
{"root", rootLocation},
{"endpoint_root", rootLocation + "/catalog/v2"},
{"id", book.getId()},
{"title", book.getTitle()},
{"updated", bookDate}, // XXX: this should be the entry update datetime
};
const auto xmlTemplate = RESOURCE::templates::catalog_v2_partial_entry_xml;
return render_template(xmlTemplate, data);
auto data = getSingleBookData(book);
data["with_xml_header"] = MustacheData(withXMLHeader);
data["dump_partial_entries"] = MustacheData(partial);
data["endpoint_root"] = endpointRoot;
data["root"] = rootLocation;
return render_template(RESOURCE::templates::catalog_v2_entry_xml, data);
}
BooksData getBooksData(const Library* library, const NameMapper* nameMapper, const std::vector<std::string>& bookIds, const std::string& rootLocation, bool partial)
BooksData getBooksData(const Library* library, const std::vector<std::string>& bookIds, const std::string& rootLocation, const std::string& endpointRoot, bool partial)
{
BooksData booksData;
for ( const auto& bookId : bookIds ) {
try {
const Book book = library->getBookByIdThreadSafe(bookId);
const std::string contentId = nameMapper->getNameForId(bookId);
const auto entryXML = partial
? partialEntryXML(book, rootLocation)
: fullEntryXML(book, rootLocation, contentId);
booksData.push_back(kainjow::mustache::object{ {"entry", entryXML} });
booksData.push_back(kainjow::mustache::object{
{"entry", getSingleBookEntryXML(book, false, rootLocation, endpointRoot, partial)}
});
} catch ( const std::out_of_range& ) {
// the book was removed from the library since its id was obtained
// ignore it
@@ -190,7 +179,7 @@ std::string getLanguageSelfName(const std::string& lang) {
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
{
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, false);
const auto booksData = getBooksData(library, bookIds, rootLocation, "", false);
const kainjow::mustache::object template_data{
{"date", gen_date_str()},
{"root", rootLocation},
@@ -208,7 +197,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const s
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const
{
const auto endpointRoot = rootLocation + "/catalog/v2";
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, partial);
const auto booksData = getBooksData(library, bookIds, rootLocation, endpointRoot, partial);
const char* const endpoint = partial ? "/partial_entries" : "/entries";
const kainjow::mustache::object template_data{
@@ -229,11 +218,7 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const
std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
{
const auto book = library->getBookById(bookId);
const std::string contentId = nameMapper->getNameForId(bookId);
return XML_HEADER
+ "\n"
+ fullEntryXML(book, rootLocation, contentId);
return getSingleBookEntryXML(library->getBookById(bookId), true, rootLocation, "", false);
}
std::string OPDSDumper::categoriesOPDSFeed() const

View File

@@ -166,7 +166,7 @@ kainjow::mustache::data buildPagination(
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
{
const std::string absPathPrefix = protocolPrefix;
const std::string absPathPrefix = protocolPrefix + "content/";
// Build the results list
kainjow::mustache::data items{kainjow::mustache::data::type::list};
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
@@ -206,7 +206,7 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
kainjow::mustache::data allData;
allData.set("searchProtocolPrefix", searchProtocolPrefix);
allData.set("protocolPrefix", protocolPrefix);
allData.set("results", results);
allData.set("pagination", pagination);
allData.set("query", query);

View File

@@ -37,11 +37,11 @@ namespace {
// into the ETag for ETag::Option opt.
// IMPORTANT: The characters in all_options must come in sorted order (so that
// IMPORTANT: isValidOptionsString() works correctly).
const char all_options[] = "Zz";
const char all_options[] = "cz";
static_assert(ETag::OPTION_COUNT == sizeof(all_options) - 1, "");
bool isValidETagBody(const std::string& s)
bool isValidServerId(const std::string& s)
{
return !s.empty() && s.find_first_of("\"/") == std::string::npos;
}
@@ -83,17 +83,17 @@ bool ETag::get_option(Option opt) const
std::string ETag::get_etag() const
{
if ( m_body.empty() )
if ( m_serverId.empty() )
return std::string();
return "\"" + m_body + "/" + m_options + "\"";
return "\"" + m_serverId + "/" + m_options + "\"";
}
ETag::ETag(const std::string& body, const std::string& options)
ETag::ETag(const std::string& serverId, const std::string& options)
{
if ( isValidETagBody(body) && isValidOptionsString(options) )
if ( isValidServerId(serverId) && isValidOptionsString(options) )
{
m_body = body;
m_serverId = serverId;
m_options = options;
}
}
@@ -115,7 +115,7 @@ ETag ETag::parse(std::string s)
return ETag(s.substr(0, i), s.substr(i+1));
}
ETag ETag::match(const std::string& etags, const std::string& body)
ETag ETag::match(const std::string& etags, const std::string& server_id)
{
std::istringstream ss(etags);
std::string etag_str;
@@ -125,7 +125,7 @@ ETag ETag::match(const std::string& etags, const std::string& body)
etag_str.pop_back();
const ETag etag = parse(etag_str);
if ( etag && etag.m_body == body )
if ( etag && etag.m_serverId == server_id )
return etag;
}

View File

@@ -28,11 +28,10 @@ namespace kiwix {
// The ETag string used by Kiwix server (more precisely, its value inside the
// double quotes) consists of two parts:
//
// 1. Body - A string uniquely identifying the object or state from which
// the resource has been obtained.
// 1. ServerId - The string obtained on server start up
//
// 2. Options - Zero or more characters encoding the type of the ETag and/or
// the values of some of the headers of the response
// 2. Options - Zero or more characters encoding the values of some of the
// headers of the response
//
// The two parts are separated with a slash (/) symbol (which is always present,
// even when the the options part is empty). Neither portion of a Kiwix ETag
@@ -41,7 +40,7 @@ namespace kiwix {
//
// "abcdefghijklmn/"
// "1234567890/z"
// "6f1d19d0-633f-087b-fb55-7ac324ff9baf/Zz"
// "1234567890/cz"
//
// The options part of the Kiwix ETag allows to correctly set the required
// headers when responding to a conditional If-None-Match request with a 304
@@ -52,7 +51,7 @@ class ETag
{
public: // types
enum Option {
ZIM_CONTENT,
CACHEABLE_ENTITY,
COMPRESSED_CONTENT,
OPTION_COUNT
};
@@ -60,10 +59,10 @@ class ETag
public: // functions
ETag() {}
void set_body(const std::string& s) { m_body = s; }
void set_server_id(const std::string& id) { m_serverId = id; }
void set_option(Option opt);
explicit operator bool() const { return !m_body.empty(); }
explicit operator bool() const { return !m_serverId.empty(); }
bool get_option(Option opt) const;
std::string get_etag() const;
@@ -77,7 +76,7 @@ class ETag
static ETag parse(std::string s);
private: // data
std::string m_body;
std::string m_serverId;
std::string m_options;
};

View File

@@ -77,6 +77,7 @@ extern "C" {
#include "request_context.h"
#include "response.h"
#define MAX_SEARCH_LEN 140
#define DEFAULT_CACHE_SIZE 2
namespace kiwix {
@@ -118,6 +119,9 @@ 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;
}
@@ -138,6 +142,15 @@ std::string renderUrl(const std::string& root, const std::string& urlTemplate)
return url;
}
std::string makeFulltextSearchSuggestion(const std::string& lang, const std::string& queryString)
{
return i18n::expandParameterizedString(lang, "suggest-full-text-search",
{
{"SEARCH_TERMS", queryString}
}
);
}
ParameterizedMessage noSuchBookErrorMsg(const std::string& bookName)
{
return ParameterizedMessage("no-such-book", { {"BOOK_NAME", bookName} });
@@ -202,40 +215,12 @@ void checkBookNumber(const Library::BookIdSet& bookIds, size_t limit) {
}
}
typedef std::set<std::string> Languages;
Languages getLanguages(const Library& lib, const Library::BookIdSet& bookIds) {
Languages langs;
for ( const auto& b : bookIds ) {
langs.insert(lib.getBookById(b).getLanguage());
}
return langs;
}
struct CustomizedResourceData
{
std::string mimeType;
std::string resourceFilePath;
};
bool responseMustBeETaggedWithLibraryId(const Response& response, const RequestContext& request)
{
return response.getReturnCode() == MHD_HTTP_OK
&& response.get_kind() == Response::DYNAMIC_CONTENT
&& request.get_url() != "/random";
}
ETag
get_matching_if_none_match_etag(const RequestContext& r, const std::string& etagBody)
{
try {
const std::string etag_list = r.get_header(MHD_HTTP_HEADER_IF_NONE_MATCH);
return ETag::match(etag_list, etagBody);
} catch (const std::out_of_range&) {
return ETag();
}
}
} // unnamed namespace
std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const RequestContext& request) const
@@ -307,10 +292,6 @@ SearchInfo InternalServer::getSearchInfo(const RequestContext& request) const
{
auto bookIds = selectBooks(request);
checkBookNumber(bookIds.second, m_multizimSearchLimit);
if ( getLanguages(*mp_library, bookIds.second).size() != 1 ) {
throw Error(nonParameterizedMessage("confusion-of-tongues"));
}
auto pattern = request.get_optional_param<std::string>("pattern", "");
GeoQuery geoQuery;
@@ -465,6 +446,7 @@ bool InternalServer::start() {
}
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
m_server_id = kiwix::to_string(server_start_time.count());
m_library_id = m_server_id;
return true;
}
@@ -532,9 +514,8 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
}
}
if ( responseMustBeETaggedWithLibraryId(*response, request) ) {
response->set_etag_body(getLibraryId());
}
if (response->getReturnCode() == MHD_HTTP_OK && !etag_not_needed(request))
response->set_server_id(m_server_id);
auto ret = response->send(request, connection);
auto end_time = std::chrono::steady_clock::now();
@@ -556,11 +537,6 @@ bool isEndpointUrl(const std::string& url, const std::string& endpoint)
} // unnamed namespace
std::string InternalServer::getLibraryId() const
{
return m_server_id + "." + kiwix::to_string(mp_library->getRevision());
}
std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& request)
{
try {
@@ -569,7 +545,7 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
+ urlNotFoundMsg;
}
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
const ETag etag = get_matching_if_none_match_etag(request);
if ( etag )
return Response::build_304(*this, etag);
@@ -630,6 +606,27 @@ MustacheData InternalServer::get_default_data() const
return data;
}
bool InternalServer::etag_not_needed(const RequestContext& request) const
{
const std::string url = request.get_url();
return kiwix::startsWith(url, "/catalog")
|| url == "/search"
|| url == "/suggest"
|| url == "/random"
|| url == "/catch/external";
}
ETag
InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
{
try {
const std::string etag_list = r.get_header(MHD_HTTP_HEADER_IF_NONE_MATCH);
return ETag::match(etag_list, m_server_id);
} catch (const std::out_of_range&) {
return ETag();
}
}
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
{
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
@@ -639,21 +636,6 @@ std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& r
* Archive and Zim handlers begin
**/
class InternalServer::LockableSuggestionSearcher : public zim::SuggestionSearcher
{
public:
explicit LockableSuggestionSearcher(const zim::Archive& archive)
: zim::SuggestionSearcher(archive)
{}
std::unique_lock<std::mutex> getLock() {
return std::unique_lock<std::mutex>(m_mutex);
}
virtual ~LockableSuggestionSearcher() = default;
private:
std::mutex m_mutex;
};
std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& request)
{
if (m_verbose.load()) {
@@ -691,27 +673,50 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
printf("Searching suggestions for: \"%s\"\n", queryString.c_str());
}
Suggestions results;
MustacheData results{MustacheData::type::list};
bool first = true;
/* Get the suggestions */
auto searcher = suggestionSearcherCache.getOrPut(bookId,
[=](){ return make_shared<LockableSuggestionSearcher>(*archive); }
[=](){ return make_shared<zim::SuggestionSearcher>(*archive); }
);
const auto lock(searcher->getLock());
auto search = searcher->suggest(queryString);
auto srs = search.getResults(start, count);
for(auto& suggestion: srs) {
results.add(suggestion);
MustacheData result;
result.set("label", suggestion.getTitle());
if (suggestion.hasSnippet()) {
result.set("label", suggestion.getSnippet());
}
result.set("value", suggestion.getTitle());
result.set("kind", "path");
result.set("path", suggestion.getPath());
result.set("first", first);
first = false;
results.push_back(result);
}
/* Propose the fulltext search if possible */
if (archive->hasFulltextIndex()) {
results.addFTSearchSuggestion(request.get_user_language(), queryString);
MustacheData result;
const auto lang = request.get_user_language();
result.set("label", makeFulltextSearchSuggestion(lang, queryString));
result.set("value", queryString + " ");
result.set("kind", "pattern");
result.set("first", first);
results.push_back(result);
}
return ContentResponse::build(*this, results.getJSON(), "application/json; charset=utf-8");
auto data = get_default_data();
data.set("suggestions", results);
auto response = ContentResponse::build(*this, RESOURCE::templates::suggestion_json, data, "application/json; charset=utf-8");
return std::move(response);
}
std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestContext& request)
@@ -728,25 +733,6 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
}
namespace
{
Response::Kind staticResourceAccessType(const RequestContext& req, const char* expectedCacheid)
{
if ( expectedCacheid == nullptr )
return Response::DYNAMIC_CONTENT;
try {
if ( expectedCacheid != req.get_argument("cacheid") )
throw ResourceNotFound("Wrong cacheid");
return Response::STATIC_RESOURCE;
} catch( const std::out_of_range& ) {
return Response::DYNAMIC_CONTENT;
}
}
} // unnamed namespace
std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& request)
{
if (m_verbose.load()) {
@@ -757,16 +743,12 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
auto resourceName = isRequestForViewer
? "viewer.html"
: request.get_url().substr(1);
const char* const resourceCacheId = getResourceCacheId(resourceName);
try {
const auto accessType = staticResourceAccessType(request, resourceCacheId);
auto response = ContentResponse::build(
*this,
getResource(resourceName),
getMimeTypeForFile(resourceName));
response->set_kind(accessType);
response->set_cacheable();
return std::move(response);
} catch (const ResourceNotFound& e) {
return HTTP404Response(*this, request)
@@ -793,93 +775,86 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
}
try {
return handle_search_request(request);
} catch (const Error& e) {
return HTTP400Response(*this, request)
+ invalidUrlMsg
+ e.message();
}
}
auto searchInfo = getSearchInfo(request);
auto bookIds = searchInfo.getBookIds();
namespace
{
/* Make the search */
// Try to get a search from the searchInfo, else build it
auto searcher = mp_library->getSearcherByIds(bookIds);
auto lock(searcher->getLock());
unsigned getSearchPageSize(const RequestContext& r)
{
const auto DEFAULT_PAGE_LEN = 25u;
const auto MAX_PAGE_LEN = 140u;
const auto pageLength = r.get_optional_param("pageLength", DEFAULT_PAGE_LEN);
return pageLength == 0
? DEFAULT_PAGE_LEN
: min(MAX_PAGE_LEN, pageLength);
}
} // unnamed namespace
std::unique_ptr<Response> InternalServer::handle_search_request(const RequestContext& request)
{
auto searchInfo = getSearchInfo(request);
auto bookIds = searchInfo.getBookIds();
/* Make the search */
// Try to get a search from the searchInfo, else build it
auto searcher = mp_library->getSearcherByIds(bookIds);
auto lock(searcher->getLock());
std::shared_ptr<zim::Search> search;
try {
search = searchCache.getOrPut(searchInfo,
[=](){
return make_shared<zim::Search>(searcher->search(searchInfo.getZimQuery(m_verbose.load())));
std::shared_ptr<zim::Search> search;
try {
search = searchCache.getOrPut(searchInfo,
[=](){
return make_shared<zim::Search>(searcher->search(searchInfo.getZimQuery(m_verbose.load())));
}
);
} catch(std::runtime_error& e) {
// Searcher->search will throw a runtime error if there is no valid xapian database to do the search.
// (in case of zim file not containing a index)
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css);
HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND,
"fulltext-search-unavailable",
"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());
}
);
} catch(std::runtime_error& e) {
// Searcher->search will throw a runtime error if there is no valid xapian database to do the search.
// (in case of zim file not containing a index)
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css);
HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND,
"fulltext-search-unavailable",
"404-page-heading",
cssUrl);
response += nonParameterizedMessage("no-search-results");
*/
return response;
}
auto start = 1;
try {
start = request.get_argument<unsigned int>("start");
} catch (const std::exception&) {}
start = max(1, start);
auto pageLength = 25;
try {
pageLength = request.get_argument<unsigned int>("pageLength");
} catch (const std::exception&) {}
if (pageLength > MAX_SEARCH_LEN) {
pageLength = MAX_SEARCH_LEN;
}
if (pageLength == 0) {
pageLength = 25;
}
/* Get the results */
SearchRenderer renderer(search->getResults(start-1, pageLength), mp_nameMapper, mp_library, start,
search->getEstimatedMatches());
renderer.setSearchPattern(searchInfo.pattern);
renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
renderer.setProtocolPrefix(m_root + "/");
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");
}
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 += TaskbarInfo(bookName, mp_library->getArchiveById(bookId).get());
response->set_taskbar(bookName, mp_library->getArchiveById(bookId).get());
}
*/
return response;
return std::move(response);
} catch (const Error& e) {
return HTTP400Response(*this, request)
+ invalidUrlMsg
+ e.message();
}
const auto start = max(1u, request.get_optional_param("start", 1u));
const auto pageLength = getSearchPageSize(request);
/* Get the results */
SearchRenderer renderer(search->getResults(start-1, pageLength), mp_nameMapper, mp_library, start,
search->getEstimatedMatches());
renderer.setSearchPattern(searchInfo.pattern);
renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
renderer.setProtocolPrefix(m_root + "/content/");
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");
}
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);
}
std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& request)
@@ -979,9 +954,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
zim::Uuid uuid;
kiwix::OPDSDumper opdsDumper(mp_library, mp_nameMapper);
kiwix::OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
opdsDumper.setLibraryId(m_library_id);
std::vector<std::string> bookIdsToDump;
if (url == "root.xml") {
uuid = zim::Uuid::generate(host);
@@ -1003,6 +978,9 @@ InternalServer::search_catalog(const RequestContext& request,
kiwix::OPDSDumper& opdsDumper)
{
const auto filter = get_search_filter(request);
const std::string q = filter.hasQuery()
? filter.getQuery()
: "<Empty query>";
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
const auto totalResults = bookIdsToDump.size();
const size_t count = request.get_optional_param("count", 10UL);
@@ -1061,11 +1039,6 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
}
const std::string archiveUuid(archive->getUuid());
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
if ( etag )
return Response::build_304(*this, etag);
auto urlStr = url.substr(prefixLength + bookName.size());
if (urlStr[0] == '/') {
urlStr = urlStr.substr(1);
@@ -1073,18 +1046,12 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
try {
auto entry = getEntryFromPath(*archive, urlStr);
if (entry.isRedirect() || urlStr != entry.getPath()) {
// In the condition above, the second case (an entry with a different
// URL was returned) can occur in the following situations:
// 1. urlStr is empty or equal to "/" and the ZIM file doesn't contain
// such an entry, in which case the main entry is returned instead.
// 2. The ZIM file uses old namespace scheme, and the resource at urlStr
// is not present but can be found under one of the 'A', 'I', 'J' or
// '-' namespaces, in which case that resource is returned instead.
if (entry.isRedirect() || urlStr.empty()) {
// If urlStr is empty, we want to mainPage.
// We must do a redirection to the real page.
return build_redirect(bookName, getFinalItem(*archive, entry));
}
auto response = ItemResponse::build(*this, request, entry.getItem());
response->set_etag_body(archiveUuid);
if (m_verbose.load()) {
printf("Found %s\n", entry.getPath().c_str());
@@ -1138,11 +1105,6 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
+ noSuchBookErrorMsg(bookName);
}
const std::string archiveUuid(archive->getUuid());
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
if ( etag )
return Response::build_304(*this, etag);
// Remove the beggining of the path:
// /raw/<bookName>/<kind>/foo
// ^^^^^ ^ ^
@@ -1152,17 +1114,13 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
try {
if (kind == "meta") {
auto item = archive->getMetadataItem(itemPath);
auto response = ItemResponse::build(*this, request, item);
response->set_etag_body(archiveUuid);
return response;
return ItemResponse::build(*this, request, item);
} else {
auto entry = archive->getEntryByPath(itemPath);
if (entry.isRedirect()) {
return build_redirect(bookName, entry.getItem(true));
}
auto response = ItemResponse::build(*this, request, entry.getItem());
response->set_etag_body(archiveUuid);
return response;
return ItemResponse::build(*this, request, entry.getItem());
}
} catch (zim::EntryNotFound& e ) {
if (m_verbose.load()) {

View File

@@ -88,6 +88,9 @@ class SearchInfo {
typedef kainjow::mustache::data MustacheData;
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;
typedef ConcurrentCache<std::string, std::shared_ptr<zim::SuggestionSearcher>> SuggestionSearcherCache;
class OPDSDumper;
class InternalServer {
@@ -134,7 +137,6 @@ class InternalServer {
std::unique_ptr<Response> handle_catalog_v2_languages(const RequestContext& request);
std::unique_ptr<Response> handle_catalog_v2_illustration(const RequestContext& request);
std::unique_ptr<Response> handle_search(const RequestContext& request);
std::unique_ptr<Response> handle_search_request(const RequestContext& request);
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
std::unique_ptr<Response> handle_random(const RequestContext& request);
std::unique_ptr<Response> handle_catch(const RequestContext& request);
@@ -148,18 +150,13 @@ class InternalServer {
MustacheData get_default_data() const;
bool etag_not_needed(const RequestContext& r) const;
ETag get_matching_if_none_match_etag(const RequestContext& request) const;
std::pair<std::string, Library::BookIdSet> selectBooks(const RequestContext& r) const;
SearchInfo getSearchInfo(const RequestContext& r) const;
bool isLocallyCustomizedResource(const std::string& url) const;
std::string getLibraryId() const;
private: // types
class LockableSuggestionSearcher;
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;
typedef ConcurrentCache<std::string, std::shared_ptr<LockableSuggestionSearcher>> SuggestionSearcherCache;
private: // data
std::string m_addr;
int m_port;
@@ -181,6 +178,7 @@ class InternalServer {
SuggestionSearcherCache suggestionSearcherCache;
std::string m_server_id;
std::string m_library_id;
class CustomizedResources;
std::unique_ptr<CustomizedResources> m_customizedResources;

View File

@@ -77,18 +77,17 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestContext& request)
{
const std::string libraryId = getLibraryId();
return ContentResponse::build(
*this,
RESOURCE::templates::catalog_v2_root_xml,
kainjow::mustache::object{
{"date", gen_date_str()},
{"endpoint_root", m_root + "/catalog/v2"},
{"feed_id", gen_uuid(libraryId)},
{"all_entries_feed_id", gen_uuid(libraryId + "/entries")},
{"partial_entries_feed_id", gen_uuid(libraryId + "/partial_entries")},
{"category_list_feed_id", gen_uuid(libraryId + "/categories")},
{"language_list_feed_id", gen_uuid(libraryId + "/languages")}
{"feed_id", gen_uuid(m_library_id)},
{"all_entries_feed_id", gen_uuid(m_library_id + "/entries")},
{"partial_entries_feed_id", gen_uuid(m_library_id + "/partial_entries")},
{"category_list_feed_id", gen_uuid(m_library_id + "/categories")},
{"language_list_feed_id", gen_uuid(m_library_id + "/languages")}
},
"application/atom+xml;profile=opds-catalog;kind=navigation"
);
@@ -96,9 +95,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
{
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
opdsDumper.setLibraryId(m_library_id);
const auto bookIds = search_catalog(request, opdsDumper);
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
return ContentResponse::build(
@@ -117,9 +116,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
+ urlNotFoundMsg;
}
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
opdsDumper.setLibraryId(m_library_id);
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
return ContentResponse::build(
*this,
@@ -130,9 +129,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
{
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
opdsDumper.setLibraryId(m_library_id);
return ContentResponse::build(
*this,
opdsDumper.categoriesOPDSFeed(),
@@ -142,9 +141,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
{
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
opdsDumper.setLibraryId(m_library_id);
return ContentResponse::build(
*this,
opdsDumper.languagesOPDSFeed(),

View File

@@ -107,14 +107,6 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
{
RequestContext *_this = static_cast<RequestContext*>(__this);
_this->arguments[key].push_back(value == nullptr ? "" : value);
if ( ! _this->queryString.empty() ) {
_this->queryString += "&";
}
_this->queryString += key;
if ( value ) {
_this->queryString += "=";
_this->queryString += value;
}
return MHD_YES;
}

View File

@@ -92,7 +92,9 @@ class RequestContext {
std::string get_url_part(int part) const;
std::string get_full_url() const;
std::string get_query() const { return queryString; }
std::string get_query(bool mustEncode = false) const {
return get_query([](const std::string& key) {return true;}, mustEncode);
}
template<class F>
std::string get_query(F filter, bool mustEncode) const {
@@ -130,7 +132,6 @@ class RequestContext {
ByteRange byteRange_;
std::map<std::string, std::string> headers;
std::map<std::string, std::vector<std::string>> arguments;
std::string queryString;
private: // functions
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);

View File

@@ -102,14 +102,6 @@ bool compress(std::string &content) {
}
const char* getCacheControlHeader(Response::Kind k)
{
switch(k) {
case Response::STATIC_RESOURCE: return "max-age=31536000, immutable";
case Response::ZIM_CONTENT: return "max-age=3600, must-revalidate";
default: return "max-age=0, must-revalidate";
}
}
} // unnamed namespace
@@ -120,13 +112,6 @@ Response::Response(bool verbose)
add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
}
void Response::set_kind(Kind k)
{
m_kind = k;
if ( k == ZIM_CONTENT )
m_etag.set_option(ETag::ZIM_CONTENT);
}
std::unique_ptr<Response> Response::build(const InternalServer& server)
{
return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
@@ -137,9 +122,6 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
auto response = Response::build(server);
response->set_code(MHD_HTTP_NOT_MODIFIED);
response->m_etag = etag;
if ( etag.get_option(ETag::ZIM_CONTENT) ) {
response->set_kind(Response::ZIM_CONTENT);
}
if ( etag.get_option(ETag::COMPRESSED_CONTENT) ) {
response->add_header(MHD_HTTP_HEADER_VARY, "Accept-Encoding");
}
@@ -373,7 +355,7 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
MHD_Response* response = create_mhd_response(request);
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
getCacheControlHeader(m_kind));
m_etag.get_option(ETag::CACHEABLE_ENTITY) ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
const std::string etag = m_etag.get_etag();
if ( ! etag.empty() )
MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etag.c_str());
@@ -429,7 +411,7 @@ ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::strin
m_mimeType(mimetype)
{
m_byteRange = byterange;
set_kind(Response::ZIM_CONTENT);
set_cacheable();
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
}
@@ -441,14 +423,14 @@ std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, cons
if (noRange && is_compressible_mime_type(mimetype)) {
// Return a contentResponse
auto response = ContentResponse::build(server, item.getData(), mimetype);
response->set_kind(Response::ZIM_CONTENT);
response->set_cacheable();
response->m_byteRange = byteRange;
return std::move(response);
}
if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
auto response = Response::build_416(server, item.getSize());
response->set_kind(Response::ZIM_CONTENT);
response->set_cacheable();
return response;
}

View File

@@ -45,14 +45,6 @@ class InternalServer;
class RequestContext;
class Response {
public:
enum Kind
{
STATIC_RESOURCE,
ZIM_CONTENT,
DYNAMIC_CONTENT
};
public:
Response(bool verbose);
virtual ~Response() = default;
@@ -65,9 +57,8 @@ class Response {
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
void set_code(int code) { m_returnCode = code; }
void set_kind(Kind k);
Kind get_kind() const { return m_kind; }
void set_etag_body(const std::string& id) { m_etag.set_body(id); }
void set_cacheable() { m_etag.set_option(ETag::CACHEABLE_ENTITY); }
void set_server_id(const std::string& id) { m_etag.set_server_id(id); }
void add_header(const std::string& name, const std::string& value) { m_customHeaders[name] = value; }
int getReturnCode() const { return m_returnCode; }
@@ -77,7 +68,6 @@ class Response {
MHD_Response* create_error_response(const RequestContext& request) const;
protected: // data
Kind m_kind = DYNAMIC_CONTENT;
bool m_verbose;
int m_returnCode;
ByteRange m_byteRange;

View File

@@ -93,6 +93,10 @@ std::string getMetaFlavour(const zim::Archive& archive) {
return getMetadata(archive, "Flavour");
}
std::string getArchiveId(const zim::Archive& archive) {
return (std::string) archive.getUuid();
}
bool getArchiveFavicon(const zim::Archive& archive, unsigned size,
std::string& content, std::string& mimeType){
try {
@@ -105,6 +109,46 @@ bool getArchiveFavicon(const zim::Archive& archive, unsigned size,
return false;
}
// should this be in libzim
unsigned int getArchiveMediaCount(const zim::Archive& archive) {
std::map<const std::string, unsigned int> counterMap = parseArchiveCounter(archive);
unsigned int counter = 0;
for (auto &pair:counterMap) {
if (startsWith(pair.first, "image/") ||
startsWith(pair.first, "video/") ||
startsWith(pair.first, "audio/")) {
counter += pair.second;
}
}
return counter;
}
unsigned int getArchiveArticleCount(const zim::Archive& archive) {
// [HACK]
// getArticleCount() returns different things depending of the "version" of the zim.
// On old zim (<=6), it returns the number of entry in `A` namespace
// On recent zim (>=7), it returns:
// - the number of entry in `C` namespace (==getEntryCount) if no frontArticleIndex is present
// - the number of front article if a frontArticleIndex is present
// The use case >=7 without frontArticleIndex is pretty rare so we don't care
// We can detect if we are reading a zim <= 6 by checking if we have a newNamespaceScheme.
if (archive.hasNewNamespaceScheme()) {
//The articleCount is "good"
return archive.getArticleCount();
} else {
// We have to parse the `M/Counter` metadata
unsigned int counter = 0;
for(const auto& pair:parseArchiveCounter(archive)) {
if (startsWith(pair.first, "text/html")) {
counter += pair.second;
}
}
return counter;
}
}
unsigned int getArchiveFileSize(const zim::Archive& archive) {
return archive.getFilesize() / 1024;
}
@@ -125,4 +169,14 @@ zim::Entry getEntryFromPath(const zim::Archive& archive, const std::string& path
}
throw zim::EntryNotFound("Cannot find entry for non empty path");
}
MimeCounterType parseArchiveCounter(const zim::Archive& archive) {
try {
auto counterContent = archive.getMetadata("Counter");
return parseMimetypeCounter(counterContent);
} catch (zim::EntryNotFound& e) {
return {};
}
}
} // kiwix

View File

@@ -40,6 +40,7 @@ namespace kiwix
std::string getMetaCreator(const zim::Archive& archive);
std::string getMetaPublisher(const zim::Archive& archive);
std::string getMetaFlavour(const zim::Archive& archive);
std::string getArchiveId(const zim::Archive& archive);
bool getArchiveFavicon(const zim::Archive& archive, unsigned size,
std::string& content, std::string& mimeType);
@@ -51,6 +52,9 @@ namespace kiwix
zim::Item getFinalItem(const zim::Archive& archive, const zim::Entry& entry);
zim::Entry getEntryFromPath(const zim::Archive& archive, const std::string& path);
MimeCounterType parseArchiveCounter(const zim::Archive& archive);
}
#endif

View File

@@ -32,15 +32,12 @@
#endif
#include "tools/stringTools.h"
#include "server/i18n.h"
#include "libkiwix-resources.h"
#include <map>
#include <sstream>
#include <pugixml.hpp>
#include <zim/uuid.h>
#include <zim/suggestion_iterator.h>
static std::map<std::string, std::string> codeisomapping {
@@ -291,6 +288,67 @@ bool kiwix::convertStrToBool(const std::string& value)
throw std::domain_error(ss.str());
}
namespace
{
// The counter metadata format is a list of item separated by a `;` :
// item0;item1;item2
// Each item is a "tuple" mimetype=number.
// However, the mimetype may contains parameters:
// text/html;raw=true;foo=bar
// So the final format may be complex to parse:
// key0=value0;key1;foo=bar=value1;key2=value2
typedef kiwix::MimeCounterType::value_type MimetypeAndCounter;
std::string readFullMimetypeAndCounterString(std::istream& in)
{
std::string mtcStr, params;
getline(in, mtcStr, ';');
if ( mtcStr.find('=') == std::string::npos )
{
do
{
if ( !getline(in, params, ';' ) )
return std::string();
mtcStr += ";" + params;
}
while ( std::count(params.begin(), params.end(), '=') != 2 );
}
return mtcStr;
}
MimetypeAndCounter parseASingleMimetypeCounter(const std::string& s)
{
const std::string::size_type k = s.find_last_of("=");
if ( k != std::string::npos )
{
const std::string mimeType = s.substr(0, k);
std::istringstream counterSS(s.substr(k+1));
unsigned int counter;
if (counterSS >> counter && counterSS.eof())
return MimetypeAndCounter{mimeType, counter};
}
return MimetypeAndCounter{"", 0};
}
} // unnamed namespace
kiwix::MimeCounterType kiwix::parseMimetypeCounter(const std::string& counterData)
{
kiwix::MimeCounterType counters;
std::istringstream ss(counterData);
while (ss)
{
const std::string mtcStr = readFullMimetypeAndCounterString(ss);
const MimetypeAndCounter mtc = parseASingleMimetypeCounter(mtcStr);
if ( !mtc.first.empty() )
counters.insert(mtc);
}
return counters;
}
std::string kiwix::gen_date_str()
{
auto now = std::time(0);
@@ -329,72 +387,3 @@ std::string kiwix::render_template(const std::string& template_str, kainjow::mus
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
return ss.str();
}
namespace
{
std::string escapeBackslashes(const std::string& s)
{
std::string es;
es.reserve(s.size());
for (char c : s) {
if ( c == '\\' ) {
es.push_back('\\');
}
es.push_back(c);
}
return es;
}
std::string makeFulltextSearchSuggestion(const std::string& lang,
const std::string& queryString)
{
return kiwix::i18n::expandParameterizedString(lang, "suggest-full-text-search",
{
{"SEARCH_TERMS", queryString}
}
);
}
} // unnamed namespace
kiwix::Suggestions::Suggestions()
: m_data(kainjow::mustache::data::type::list)
{
}
void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
{
kainjow::mustache::data result;
const std::string label = suggestion.hasSnippet()
? suggestion.getSnippet()
: suggestion.getTitle();
result.set("label", escapeBackslashes(label));
result.set("value", escapeBackslashes(suggestion.getTitle()));
result.set("kind", "path");
result.set("path", escapeBackslashes(suggestion.getPath()));
result.set("first", m_data.is_empty_list());
m_data.push_back(result);
}
void kiwix::Suggestions::addFTSearchSuggestion(const std::string& uiLang,
const std::string& queryString)
{
kainjow::mustache::data result;
const std::string label = makeFulltextSearchSuggestion(uiLang, queryString);
result.set("label", escapeBackslashes(label));
result.set("value", escapeBackslashes(queryString + " "));
result.set("kind", "pattern");
result.set("first", m_data.is_empty_list());
m_data.push_back(result);
}
std::string kiwix::Suggestions::getJSON() const
{
kainjow::mustache::data data;
data.set("suggestions", m_data);
return render_template(RESOURCE::templates::suggestion_json, data);
}

View File

@@ -33,10 +33,6 @@ namespace pugi {
class xml_node;
}
namespace zim {
class SuggestionItem;
}
namespace kiwix
{
std::string nodeToString(const pugi::xml_node& node);
@@ -49,6 +45,9 @@ namespace kiwix
const std::string& tagName);
bool convertStrToBool(const std::string& value);
using MimeCounterType = std::map<const std::string, zim::entry_index_type>;
MimeCounterType parseMimetypeCounter(const std::string& counterData);
std::string gen_date_str();
std::string gen_uuid(const std::string& s);
@@ -71,22 +70,6 @@ namespace kiwix
return defaultValue;
}
class Suggestions
{
public:
Suggestions();
void add(const zim::SuggestionItem& suggestion);
void addFTSearchSuggestion(const std::string& uiLang,
const std::string& query);
std::string getJSON() const;
private:
kainjow::mustache::data m_data;
};
}
#endif

View File

@@ -1,20 +0,0 @@
{
"@metadata": {
"authors": [
"Lucas Werkmeister",
"ThisCarthing"
]
},
"name": "Deutsch",
"random-article-failure": "Hoppla! Konnte keinen zufälligen Artikel auswählen :(",
"400-page-title": "Ungültige Anfrage",
"400-page-heading": "Ungültige Anfrage",
"404-page-title": "Inhalt nicht gefunden",
"404-page-heading": "Nicht gefunden",
"500-page-title": "Interner Server-Fehler",
"500-page-heading": "Interner Server-Fehler",
"library-button-text": "Zur Willkommensseite gehen",
"home-button-text": "Zur Hauptseite von '{{BOOK_TITLE}}' gehen",
"random-page-button-text": "Zu einer zufällig ausgewählten Seite gehen",
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen"
}

View File

@@ -27,5 +27,4 @@
, "home-button-text": "Go to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "Go to a randomly selected page"
, "searchbox-tooltip": "Search '{{BOOK_TITLE}}'"
, "confusion-of-tongues": "Two or more books in different languages would participate in search, which may lead to confusing results."
}

View File

@@ -29,6 +29,5 @@
"library-button-text": "Aller à la page de bienvenue",
"home-button-text": "Aller à la page principale de « {{BOOK_TITLE}} »",
"random-page-button-text": "Aller à une page sélectionnée aléatoirement",
"searchbox-tooltip": "Rechercher « {{BOOK_TITLE}} »",
"confusion-of-tongues": "Deux livres ou plus dans des langues différentes participeraient à la recherche, ce qui pourrait conduire à des résultats confus."
"searchbox-tooltip": "Rechercher « {{BOOK_TITLE}} »"
}

View File

@@ -1,8 +1,7 @@
{
"@metadata": {
"authors": [
"Amire80",
"YaronSh"
"Amire80"
]
},
"name": "עברית",
@@ -28,6 +27,5 @@
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
"home-button-text": "מעבר לדף הראשי של \"{{BOOK_TITLE}}\"",
"random-page-button-text": "מעבר לדף שנבחר אקראית",
"searchbox-tooltip": "חיפוש \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "שני ספרים או יותר בשפות שונות ישתתפו בחיפוש, מה שעלול להוביל לתוצאות מבלבלות."
"searchbox-tooltip": "חיפוש \"{{BOOK_TITLE}}\""
}

View File

@@ -1,16 +1,12 @@
{
"@metadata": {
"authors": [
"Kareyac"
]
"authors": []
},
"name": "Հայերեն",
"suggest-full-text-search": "որոնել '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Գիրքը բացակայում է՝ {{BOOK_NAME}}",
"url-not-found": "Սխալ հասցե՝ {{url}}",
"suggest-search": "Որոնել <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"400-page-title": "Անվավեր հարցում",
"400-page-heading": "Անվավեր հարցում",
"404-page-title": "Սխալ հասցե",
"404-page-heading": "Սխալ հասցե",
"library-button-text": "Գրադարանի էջ",

View File

@@ -27,6 +27,5 @@
"library-button-text": "Here rûpela xêrhatinê",
"home-button-text": "Here rûpela destpêkê yê {{BOOK_TITLE}}",
"random-page-button-text": "Here rûpeleke ketober bijartî",
"searchbox-tooltip": "Li {{BOOK_TITLE}} bigere",
"confusion-of-tongues": "Du an zêdetir kitêbên bi zimanên cihê wê beşdarî lêgerînê bibin, ev jî dibe ku bibe sedema tevliheviya encaman."
"searchbox-tooltip": "Li {{BOOK_TITLE}} bigere"
}

View File

@@ -27,6 +27,5 @@
"library-button-text": "Оди на воведната страница",
"home-button-text": "Оди на главната страница на „{{BOOK_TITLE}}“",
"random-page-button-text": "Оди на случајно избрана страница",
"searchbox-tooltip": "Пребарај го „{{BOOK_TITLE}}“",
"confusion-of-tongues": "Во пребарувањето ќе учествуваат две или повеќе книги на различни јазици, што може да довете до збунувачки исход."
"searchbox-tooltip": "Пребарај го „{{BOOK_TITLE}}“"
}

View File

@@ -30,6 +30,5 @@
"library-button-text": "Перейти на страницу-приветствие",
"home-button-text": "Перейти на главную страницу '{{BOOK_TITLE}}'",
"random-page-button-text": "Перейти на случайно выбранную страницу",
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'",
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам."
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'"
}

View File

@@ -27,6 +27,5 @@
"library-button-text": "Bae a sa pàgina de bene bènnidu",
"home-button-text": "Bae a sa pàgina printzipale de '{{BOOK_TITLE}}'",
"random-page-button-text": "Bae a una pàgina seletzionada a manera casuale",
"searchbox-tooltip": "Chirca '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Duos o prus libros in limbas diferentes diant pigare parte a sa chirca, cosa chi diat pòdere causare resurtados confusionosos."
"searchbox-tooltip": "Chirca '{{BOOK_TITLE}}'"
}

View File

@@ -1,7 +1,6 @@
{
"@metadata": {
"authors": [
"Jopparn",
"Sabelöga",
"WikiPhoenix"
]
@@ -10,13 +9,10 @@
"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}}",
"no-book-found": "Ingen bok matchar urvalskriterierna",
"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 :(",
"invalid-raw-data-type": "{{DATATYPE}} är ingen giltig begäran för oformaterat innehåll.",
"no-value-for-arg": "Inget värde angett för argumentet {{ARGUMENT}}",
"no-query": "Ingen fråga tillhandahålls.",
"raw-entry-not-found": "Kunde inte hitta {{DATATYPE}}-inlägget {{ENTRY}}",
"400-page-title": "Ogiltig begäran",
"400-page-heading": "Ogiltig begäran",
@@ -29,6 +25,5 @@
"library-button-text": "Gå till hemsidan",
"home-button-text": "Gå till huvudsidan för \"{{BOOK_TITLE}}\"",
"random-page-button-text": "Gå till en slumpmässigt utvald sida",
"searchbox-tooltip": "Sök efter \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "Två eller fler böcker på olika språk skulle delta i sökningen, vilket kan ge förvirrande resultat."
"searchbox-tooltip": "Sök efter \"{{BOOK_TITLE}}\""
}

View File

@@ -1,20 +0,0 @@
{
"@metadata": {
"authors": [
"Kareyac"
]
},
"name": "Fake language for i18n testing"
, "suggest-full-text-search": "[I18N TESTING] cOnTaInInG '{{{SEARCH_TERMS}}}'..."
, "no-such-book": "[I18N TESTING] No such book: {{BOOK_NAME}}. Sorry."
, "url-not-found": "[I18N TESTING] URL not found: {{url}}"
, "suggest-search": "[I18N TESTING] Make a full text search for <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>"
, "400-page-title": "[I18N TESTING] Invalid request ($400 fine must be paid)"
, "400-page-heading": "[I18N TESTING] -400 karma for an invalid request"
, "404-page-title": "[I18N TESTING] Not Found - Try Again"
, "404-page-heading": "[I18N TESTING] Content not found, but at least the server is alive"
, "library-button-text": "[I18N TESTING] Navigate to the welcome page"
, "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "[I18N TESTING] I am tired of determinism"
, "searchbox-tooltip": "[I18N TESTING] Let's search in '{{BOOK_TITLE}}'"
}

View File

@@ -1,6 +1,5 @@
i18n/bn.json
i18n/cs.json
i18n/de.json
i18n/en.json
i18n/fr.json
i18n/he.json
@@ -16,7 +15,6 @@ i18n/ru.json
i18n/sc.json
i18n/sk.json
i18n/sv.json
i18n/test.json
i18n/tr.json
i18n/zh-hans.json
i18n/zh-hant.json

View File

@@ -27,7 +27,6 @@ templates/catalog_entries.xml
templates/catalog_v2_root.xml
templates/catalog_v2_entries.xml
templates/catalog_v2_entry.xml
templates/catalog_v2_partial_entry.xml
templates/catalog_v2_categories.xml
templates/catalog_v2_languages.xml
templates/url_of_search_results_css
@@ -36,6 +35,7 @@ opensearchdescription.xml
ft_opensearchdescription.xml
catalog_v2_searchdescription.xml
skin/css/autoComplete.css
skin/css/images/search.svg
skin/favicon/android-chrome-192x192.png
skin/favicon/android-chrome-512x512.png
skin/favicon/apple-touch-icon.png

View File

@@ -1,639 +0,0 @@
// https://github.com/TarekRaafat/autoComplete.js 10.2.7
// Copyright (C) 2022 Tarek Raafat
// SPDX-License-Identifier: Apache-2.0
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.autoComplete = factory());
})(this, (function () { 'use strict';
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
enumerableOnly && (symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
})), keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = null != arguments[i] ? arguments[i] : {};
i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
_defineProperty(target, key, source[key]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
return target;
}
function _typeof(obj) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
}, _typeof(obj);
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _createForOfIteratorHelper(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (!it) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
var F = function () {};
return {
s: F,
n: function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
},
e: function (e) {
throw e;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var normalCompletion = true,
didErr = false,
err;
return {
s: function () {
it = it.call(o);
},
n: function () {
var step = it.next();
normalCompletion = step.done;
return step;
},
e: function (e) {
didErr = true;
err = e;
},
f: function () {
try {
if (!normalCompletion && it.return != null) it.return();
} finally {
if (didErr) throw err;
}
}
};
}
var select$1 = function select(element) {
return typeof element === "string" ? document.querySelector(element) : element();
};
var create = function create(tag, options) {
var el = typeof tag === "string" ? document.createElement(tag) : tag;
for (var key in options) {
var val = options[key];
if (key === "inside") {
val.append(el);
} else if (key === "dest") {
select$1(val[0]).insertAdjacentElement(val[1], el);
} else if (key === "around") {
var ref = val;
ref.parentNode.insertBefore(el, ref);
el.append(ref);
if (ref.getAttribute("autofocus") != null) ref.focus();
} else if (key in el) {
el[key] = val;
} else {
el.setAttribute(key, val);
}
}
return el;
};
var getQuery = function getQuery(field) {
return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement ? field.value : field.innerHTML;
};
var format = function format(value, diacritics) {
value = String(value).toLowerCase();
return diacritics ? value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC") : value;
};
var debounce = function debounce(callback, duration) {
var timer;
return function () {
clearTimeout(timer);
timer = setTimeout(function () {
return callback();
}, duration);
};
};
var checkTrigger = function checkTrigger(query, condition, threshold) {
return condition ? condition(query) : query.length >= threshold;
};
var mark = function mark(value, cls) {
return create("mark", _objectSpread2({
innerHTML: value
}, typeof cls === "string" && {
"class": cls
})).outerHTML;
};
var configure = (function (ctx) {
var name = ctx.name,
options = ctx.options,
resultsList = ctx.resultsList,
resultItem = ctx.resultItem;
for (var option in options) {
if (_typeof(options[option]) === "object") {
if (!ctx[option]) ctx[option] = {};
for (var subOption in options[option]) {
ctx[option][subOption] = options[option][subOption];
}
} else {
ctx[option] = options[option];
}
}
ctx.selector = ctx.selector || "#" + name;
resultsList.destination = resultsList.destination || ctx.selector;
resultsList.id = resultsList.id || name + "_list_" + ctx.id;
resultItem.id = resultItem.id || name + "_result";
ctx.input = select$1(ctx.selector);
});
var eventEmitter = (function (name, ctx) {
ctx.input.dispatchEvent(new CustomEvent(name, {
bubbles: true,
detail: ctx.feedback,
cancelable: true
}));
});
var search = (function (query, record, options) {
var _ref = options || {},
mode = _ref.mode,
diacritics = _ref.diacritics,
highlight = _ref.highlight;
var nRecord = format(record, diacritics);
record = String(record);
query = format(query, diacritics);
if (mode === "loose") {
query = query.replace(/ /g, "");
var qLength = query.length;
var cursor = 0;
var match = Array.from(record).map(function (character, index) {
if (cursor < qLength && nRecord[index] === query[cursor]) {
character = highlight ? mark(character, highlight) : character;
cursor++;
}
return character;
}).join("");
if (cursor === qLength) return match;
} else {
var _match = nRecord.indexOf(query);
if (~_match) {
query = record.substring(_match, _match + query.length);
_match = highlight ? record.replace(query, mark(query, highlight)) : record;
return _match;
}
}
});
var getData = function getData(ctx, query) {
return new Promise(function ($return, $error) {
var data;
data = ctx.data;
if (data.cache && data.store) return $return();
return new Promise(function ($return, $error) {
if (typeof data.src === "function") {
return data.src(query).then($return, $error);
}
return $return(data.src);
}).then(function ($await_4) {
try {
ctx.feedback = data.store = $await_4;
eventEmitter("response", ctx);
return $return();
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
});
};
var findMatches = function findMatches(query, ctx) {
var data = ctx.data,
searchEngine = ctx.searchEngine;
var matches = [];
data.store.forEach(function (value, index) {
var find = function find(key) {
var record = key ? value[key] : value;
var match = typeof searchEngine === "function" ? searchEngine(query, record) : search(query, record, {
mode: searchEngine,
diacritics: ctx.diacritics,
highlight: ctx.resultItem.highlight
});
if (!match) return;
var result = {
match: match,
value: value
};
if (key) result.key = key;
matches.push(result);
};
if (data.keys) {
var _iterator = _createForOfIteratorHelper(data.keys),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var key = _step.value;
find(key);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
} else {
find();
}
});
if (data.filter) matches = data.filter(matches);
var results = matches.slice(0, ctx.resultsList.maxResults);
ctx.feedback = {
query: query,
matches: matches,
results: results
};
eventEmitter("results", ctx);
};
var Expand = "aria-expanded";
var Active = "aria-activedescendant";
var Selected = "aria-selected";
var feedback = function feedback(ctx, index) {
ctx.feedback.selection = _objectSpread2({
index: index
}, ctx.feedback.results[index]);
};
var render = function render(ctx) {
var resultsList = ctx.resultsList,
list = ctx.list,
resultItem = ctx.resultItem,
feedback = ctx.feedback;
var matches = feedback.matches,
results = feedback.results;
ctx.cursor = -1;
list.innerHTML = "";
if (matches.length || resultsList.noResults) {
var fragment = new DocumentFragment();
results.forEach(function (result, index) {
var element = create(resultItem.tag, _objectSpread2({
id: "".concat(resultItem.id, "_").concat(index),
role: "option",
innerHTML: result.match,
inside: fragment
}, resultItem["class"] && {
"class": resultItem["class"]
}));
if (resultItem.element) resultItem.element(element, result);
});
list.append(fragment);
if (resultsList.element) resultsList.element(list, feedback);
open(ctx);
} else {
close(ctx);
}
};
var open = function open(ctx) {
if (ctx.isOpen) return;
(ctx.wrapper || ctx.input).setAttribute(Expand, true);
ctx.list.removeAttribute("hidden");
ctx.isOpen = true;
eventEmitter("open", ctx);
};
var close = function close(ctx) {
if (!ctx.isOpen) return;
(ctx.wrapper || ctx.input).setAttribute(Expand, false);
ctx.input.setAttribute(Active, "");
ctx.list.setAttribute("hidden", "");
ctx.isOpen = false;
eventEmitter("close", ctx);
};
var goTo = function goTo(index, ctx) {
var resultItem = ctx.resultItem;
var results = ctx.list.getElementsByTagName(resultItem.tag);
var cls = resultItem.selected ? resultItem.selected.split(" ") : false;
if (ctx.isOpen && results.length) {
var _results$index$classL;
var state = ctx.cursor;
if (index >= results.length) index = 0;
if (index < 0) index = results.length - 1;
ctx.cursor = index;
if (state > -1) {
var _results$state$classL;
results[state].removeAttribute(Selected);
if (cls) (_results$state$classL = results[state].classList).remove.apply(_results$state$classL, _toConsumableArray(cls));
}
results[index].setAttribute(Selected, true);
if (cls) (_results$index$classL = results[index].classList).add.apply(_results$index$classL, _toConsumableArray(cls));
ctx.input.setAttribute(Active, results[ctx.cursor].id);
ctx.list.scrollTop = results[index].offsetTop - ctx.list.clientHeight + results[index].clientHeight + 5;
ctx.feedback.cursor = ctx.cursor;
feedback(ctx, index);
eventEmitter("navigate", ctx);
}
};
var next = function next(ctx) {
goTo(ctx.cursor + 1, ctx);
};
var previous = function previous(ctx) {
goTo(ctx.cursor - 1, ctx);
};
var select = function select(ctx, event, index) {
index = index >= 0 ? index : ctx.cursor;
if (index < 0) return;
ctx.feedback.event = event;
feedback(ctx, index);
eventEmitter("selection", ctx);
close(ctx);
};
var click = function click(event, ctx) {
var itemTag = ctx.resultItem.tag.toUpperCase();
var items = Array.from(ctx.list.querySelectorAll(itemTag));
var item = event.target.closest(itemTag);
if (item && item.nodeName === itemTag) {
select(ctx, event, items.indexOf(item));
}
};
var navigate = function navigate(event, ctx) {
switch (event.keyCode) {
case 40:
case 38:
event.preventDefault();
event.keyCode === 40 ? next(ctx) : previous(ctx);
break;
case 13:
if (!ctx.submit) event.preventDefault();
if (ctx.cursor >= 0) select(ctx, event);
break;
case 9:
if (ctx.resultsList.tabSelect && ctx.cursor >= 0) select(ctx, event);
break;
case 27:
ctx.input.value = "";
close(ctx);
break;
}
};
function start (ctx, q) {
var _this = this;
return new Promise(function ($return, $error) {
var queryVal, condition;
queryVal = q || getQuery(ctx.input);
queryVal = ctx.query ? ctx.query(queryVal) : queryVal;
condition = checkTrigger(queryVal, ctx.trigger, ctx.threshold);
if (condition) {
return getData(ctx, queryVal).then(function ($await_2) {
try {
if (ctx.feedback instanceof Error) return $return();
findMatches(queryVal, ctx);
if (ctx.resultsList) render(ctx);
return $If_1.call(_this);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} else {
close(ctx);
return $If_1.call(_this);
}
function $If_1() {
return $return();
}
});
}
var eventsManager = function eventsManager(events, callback) {
for (var element in events) {
for (var event in events[element]) {
callback(element, event);
}
}
};
var addEvents = function addEvents(ctx) {
var events = ctx.events;
var run = debounce(function () {
return start(ctx);
}, ctx.debounce);
var publicEvents = ctx.events = _objectSpread2({
input: _objectSpread2({}, events && events.input)
}, ctx.resultsList && {
list: events ? _objectSpread2({}, events.list) : {}
});
var privateEvents = {
input: {
input: function input() {
run();
},
keydown: function keydown(event) {
navigate(event, ctx);
},
blur: function blur() {
close(ctx);
}
},
list: {
mousedown: function mousedown(event) {
event.preventDefault();
},
click: function click$1(event) {
click(event, ctx);
}
}
};
eventsManager(privateEvents, function (element, event) {
if (!ctx.resultsList && event !== "input") return;
if (publicEvents[element][event]) return;
publicEvents[element][event] = privateEvents[element][event];
});
eventsManager(publicEvents, function (element, event) {
ctx[element].addEventListener(event, publicEvents[element][event]);
});
};
var removeEvents = function removeEvents(ctx) {
eventsManager(ctx.events, function (element, event) {
ctx[element].removeEventListener(event, ctx.events[element][event]);
});
};
function init (ctx) {
var _this = this;
return new Promise(function ($return, $error) {
var placeHolder, resultsList, parentAttrs;
placeHolder = ctx.placeHolder;
resultsList = ctx.resultsList;
parentAttrs = {
role: "combobox",
"aria-owns": resultsList.id,
"aria-haspopup": true,
"aria-expanded": false
};
create(ctx.input, _objectSpread2(_objectSpread2({
"aria-controls": resultsList.id,
"aria-autocomplete": "both"
}, placeHolder && {
placeholder: placeHolder
}), !ctx.wrapper && _objectSpread2({}, parentAttrs)));
if (ctx.wrapper) ctx.wrapper = create("div", _objectSpread2({
around: ctx.input,
"class": ctx.name + "_wrapper"
}, parentAttrs));
if (resultsList) ctx.list = create(resultsList.tag, _objectSpread2({
dest: [resultsList.destination, resultsList.position],
id: resultsList.id,
role: "listbox",
hidden: "hidden"
}, resultsList["class"] && {
"class": resultsList["class"]
}));
addEvents(ctx);
if (ctx.data.cache) {
return getData(ctx).then(function ($await_2) {
try {
return $If_1.call(_this);
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
}
function $If_1() {
eventEmitter("init", ctx);
return $return();
}
return $If_1.call(_this);
});
}
function extend (autoComplete) {
var prototype = autoComplete.prototype;
prototype.init = function () {
init(this);
};
prototype.start = function (query) {
start(this, query);
};
prototype.unInit = function () {
if (this.wrapper) {
var parentNode = this.wrapper.parentNode;
parentNode.insertBefore(this.input, this.wrapper);
parentNode.removeChild(this.wrapper);
}
removeEvents(this);
};
prototype.open = function () {
open(this);
};
prototype.close = function () {
close(this);
};
prototype.goTo = function (index) {
goTo(index, this);
};
prototype.next = function () {
next(this);
};
prototype.previous = function () {
previous(this);
};
prototype.select = function (index) {
select(this, null, index);
};
prototype.search = function (query, record, options) {
return search(query, record, options);
};
}
function autoComplete(config) {
this.options = config;
this.id = autoComplete.instances = (autoComplete.instances || 0) + 1;
this.name = "autoComplete";
this.wrapper = 1;
this.threshold = 1;
this.debounce = 0;
this.resultsList = {
position: "afterend",
tag: "ul",
maxResults: 5
};
this.resultItem = {
tag: "li"
};
configure(this);
extend.call(this, autoComplete);
init(this);
}
return autoComplete;
}));

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
/* Modified from https://github.com/TarekRaafat/autoComplete.js */
.autoComplete_wrapper {
display: inline-block;
position: relative;

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" x="0px" y="0px" width="30" height="30" viewBox="0 0 171 171" style=" fill:#000000;">
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal">
<path d="M0,171.99609v-171.99609h171.99609v171.99609z" fill="none"></path>
<g fill="#ff7a7a">
<path d="M74.1,17.1c-31.41272,0 -57,25.58728 -57,57c0,31.41272 25.58728,57 57,57c13.6601,0 26.20509,-4.85078 36.03692,-12.90293l34.03301,34.03301c1.42965,1.48907 3.55262,2.08891 5.55014,1.56818c1.99752,-0.52073 3.55746,-2.08067 4.07819,-4.07819c0.52073,-1.99752 -0.0791,-4.12049 -1.56818,-5.55014l-34.03301,-34.03301c8.05215,-9.83182 12.90293,-22.37682 12.90293,-36.03692c0,-31.41272 -25.58728,-57 -57,-57zM74.1,28.5c25.2517,0 45.6,20.3483 45.6,45.6c0,25.2517 -20.3483,45.6 -45.6,45.6c-25.2517,0 -45.6,-20.3483 -45.6,-45.6c0,-25.2517 20.3483,-45.6 45.6,-45.6z"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -105,7 +105,7 @@ body {
border-radius: 10px;
border: solid 1px #b5b2b2;
padding: 10px;
background-image: url('../skin/search-icon.svg?KIWIXCACHEID');
background-image: url('./search-icon.svg');
background-repeat: no-repeat;
background-position: right center;
background-origin: content-box;

View File

@@ -152,27 +152,6 @@ function updateSearchBoxForBookChange() {
}
}
let previousScrollTop = Infinity;
function updateToolbarVisibilityState() {
const iframeDoc = contentIframe.contentDocument;
const st = iframeDoc.documentElement.scrollTop || iframeDoc.body.scrollTop;
if ( Math.abs(previousScrollTop - st) <= 5 )
return;
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
if (st > previousScrollTop) {
kiwixToolBar.style.position = 'fixed';
kiwixToolBar.style.top = '-100%';
} else {
kiwixToolBar.style.position = 'static';
kiwixToolBar.style.top = '0';
}
previousScrollTop = st;
}
function handle_visual_viewport_change() {
contentIframe.height = window.visualViewport.height - contentIframe.offsetTop - 4;
}
@@ -186,7 +165,6 @@ function handle_location_hash_change() {
contentIframe.contentWindow.location.replace(iframeContentUrl);
}
updateSearchBoxForLocationChange();
previousScrollTop = Infinity;
}
function handle_content_url_change() {
@@ -298,7 +276,39 @@ function htmlDecode(input) {
}
function setupAutoHidingOfTheToolbar() {
setInterval(updateToolbarVisibilityState, 250);
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() {
@@ -374,10 +384,7 @@ function setupSuggestions() {
}
function setupViewer() {
// Defer the call of handle_visual_viewport_change() until after the
// presence or absence of the taskbar as determined by this function
// has been settled.
setTimeout(handle_visual_viewport_change, 0);
setInterval(handle_visual_viewport_change, 0);
const kiwixToolBarWrapper = document.getElementById('kiwixtoolbarwrapper');
if ( ! viewerSettings.toolbarEnabled ) {

View File

@@ -1,8 +1,13 @@
<entry>
{{#with_xml_header}}<?xml version="1.0" encoding="UTF-8"?>
{{/with_xml_header}} <entry>
<id>urn:uuid:{{id}}</id>
<title>{{title}}</title>
<updated>{{updated}}</updated>
<summary>{{description}}</summary>
{{#dump_partial_entries}}
<link rel="alternate"
href="{{endpoint_root}}/entry/{{{id}}}"
type="application/atom+xml;type=entry;profile=opds-catalog"/>
{{/dump_partial_entries}}{{^dump_partial_entries}} <summary>{{description}}</summary>
<language>{{language}}</language>
<name>{{name}}</name>
<flavour>{{flavour}}</flavour>
@@ -24,4 +29,5 @@
{{#url}}
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
{{/url}}
{{/dump_partial_entries}}
</entry>

View File

@@ -1,8 +0,0 @@
<entry>
<id>urn:uuid:{{id}}</id>
<title>{{title}}</title>
<updated>{{updated}}</updated>
<link rel="alternate"
href="{{endpoint_root}}/entry/{{{id}}}"
type="application/atom+xml;type=entry;profile=opds-catalog"/>
</entry>

View File

@@ -13,7 +13,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="{{root}}/skin/favicon/apple-touch-icon.png?KIWIXCACHEID">
<link rel="icon" type="image/png" sizes="32x32" href="{{root}}/skin/favicon/favicon-32x32.png?KIWIXCACHEID">
<link rel="icon" type="image/png" sizes="16x16" href="{{root}}/skin/favicon/favicon-16x16.png?KIWIXCACHEID">
<link rel="manifest" href="{{root}}/skin/favicon/site.webmanifest?KIWIXCACHEID">
<link rel="manifest" href="{{root}}/skin/favicon/site.webmanifest">
<link rel="mask-icon" href="{{root}}/skin/favicon/safari-pinned-tab.svg?KIWIXCACHEID" color="#5bbad5">
<link rel="shortcut icon" href="{{root}}/skin/favicon/favicon.ico?KIWIXCACHEID">
<meta name="msapplication-TileColor" content="#da532c">

View File

@@ -9,7 +9,7 @@
<opensearch:totalResults>{{results.count}}</opensearch:totalResults>
<opensearch:startIndex>{{results.start}}</opensearch:startIndex>
<opensearch:itemsPerPage>{{pagination.itemsPerPage}}</opensearch:itemsPerPage>
<atom:link rel="search" type="application/opensearchdescription+xml" href="{{searchProtocolPrefix}}/searchdescription.xml"/>
<atom:link rel="search" type="application/opensearchdescription+xml" href="{{protocolPrefix}}search/searchdescription.xml"/>
<opensearch:Query role="request"
searchTerms="{{query.pattern}}"{{#query.lang}}
language="{{query.lang}}"{{/query.lang}}

View File

@@ -16,7 +16,7 @@
}
const root = getRootLocation();
const blankPageUrl = root + "/skin/blank.html?KIWIXCACHEID";
const blankPageUrl = `${root}/skin/blank.html`;
if ( location.hash == '' ) {
location.href = root + '/';
@@ -58,7 +58,7 @@
<iframe id="content_iframe"
referrerpolicy="same-origin"
onload="on_content_load()"
src="./skin/blank.html?KIWIXCACHEID" title="ZIM content" width="100%"
src="skin/blank.html" title="ZIM content" width="100%"
style="border:0px">
</iframe>

143
test/counterParsing.cpp Normal file
View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2019 Matthieu Gautier
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
* NON-INFRINGEMENT. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "gtest/gtest.h"
#include <string>
#include <vector>
#include <map>
#include <zim/zim.h>
namespace kiwix {
using CounterType = std::map<const std::string, zim::entry_index_type>;
CounterType parseMimetypeCounter(const std::string& counterData);
};
using namespace kiwix;
#define parse parseMimetypeCounter
namespace
{
TEST(ParseCounterTest, simpleMimeType)
{
{
std::string counterStr = "";
CounterType counterMap = {};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
{
std::string counterStr = "foo=1";
CounterType counterMap = {{"foo", 1}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
{
std::string counterStr = "foo=1;text/html=50;";
CounterType counterMap = {{"foo", 1}, {"text/html", 50}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
}
TEST(ParseCounterTest, paramMimeType)
{
{
std::string counterStr = "text/html;raw=true=1";
CounterType counterMap = {{"text/html;raw=true", 1}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
{
std::string counterStr = "foo=1;text/html;raw=true=50;bar=2";
CounterType counterMap = {{"foo", 1}, {"text/html;raw=true", 50}, {"bar", 2}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
{
std::string counterStr = "foo=1;text/html;raw=true;param=value=50;bar=2";
CounterType counterMap = {{"foo", 1}, {"text/html;raw=true;param=value", 50}, {"bar", 2}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
{
std::string counterStr = "foo=1;text/html;raw=true=50;bar=2";
CounterType counterMap = {{"foo", 1}, {"text/html;raw=true", 50}, {"bar", 2}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
{
std::string counterStr = "application/javascript=8;text/html=3;application/warc-headers=28364;text/html;raw=true=6336;text/css=47;text/javascript=98;image/png=968;image/webp=24;application/json=3694;image/gif=10274;image/jpeg=1582;font/woff2=25;text/plain=284;application/atom+xml=247;application/x-www-form-urlencoded=9;video/mp4=9;application/x-javascript=7;application/xml=1;image/svg+xml=5";
CounterType counterMap = {
{"application/javascript", 8},
{"text/html", 3},
{"application/warc-headers", 28364},
{"text/html;raw=true", 6336},
{"text/css", 47},
{"text/javascript", 98},
{"image/png", 968},
{"image/webp", 24},
{"application/json", 3694},
{"image/gif", 10274},
{"image/jpeg", 1582},
{"font/woff2", 25},
{"text/plain", 284},
{"application/atom+xml", 247},
{"application/x-www-form-urlencoded", 9},
{"video/mp4", 9},
{"application/x-javascript", 7},
{"application/xml", 1},
{"image/svg+xml", 5}
};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
}
TEST(ParseCounterTest, wrongType)
{
CounterType empty = {};
{
std::string counterStr = "text/html";
ASSERT_EQ(parse(counterStr), empty) << counterStr;
}
{
std::string counterStr = "text/html=";
ASSERT_EQ(parse(counterStr), empty) << counterStr;
}
{
std::string counterStr = "text/html=foo";
ASSERT_EQ(parse(counterStr), empty) << counterStr;
}
{
std::string counterStr = "text/html=123foo";
ASSERT_EQ(parse(counterStr), empty) << counterStr;
}
{
std::string counterStr = "text/html=50;foo";
CounterType counterMap = {{"text/html", 50}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
{
std::string counterStr = "text/html;foo=20";
ASSERT_EQ(parse(counterStr), empty) << counterStr;
}
{
std::string counterStr = "text/html;foo=20;";
ASSERT_EQ(parse(counterStr), empty) << counterStr;
}
{
std::string counterStr = "text/html=50;;foo";
CounterType counterMap = {{"text/html", 50}};
ASSERT_EQ(parse(counterStr), counterMap) << counterStr;
}
}
};

View File

@@ -1,4 +0,0 @@
<library version="20110515">
<book id="5dc0b3af-5df2-0925-f0ca-d2bf75e78af6" path="example.zim" title="Wikibooks" description="testZim" language="eng" creator="test" publisher="test" tags="_ftindex:yes;_ftindex:yes;_pictures:yes;_videos:yes;_details:yes" date="2021-04-17" mediaCount="22" size="253" />
<book id="6f1d19d0-633f-087b-fb55-7ac324ff9baf" path="zimfile.zim" title="Ray Charles" description="Wikipedia articles about Ray Charles" language="eng" creator="Wikipedia" publisher="Kiwix" name="wikipedia_en_ray_charles" flavour="_mini" tags="wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes" date="2020-03-31" articleCount="129" mediaCount="45" size="555" />
</library>

View File

@@ -500,6 +500,24 @@ 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)
{
@@ -801,14 +819,8 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
lib.addBook(lib.getBookByIdThreadSafe(id));
}
EXPECT_GT(lib.getRevision(), rev);
const uint64_t rev2 = lib.getRevision();
EXPECT_EQ(9u, lib.removeBooksNotUpdatedSince(rev));
EXPECT_GT(lib.getRevision(), rev2);
EXPECT_FILTER_RESULTS(kiwix::Filter(),
"Islam Stack Exchange",
"Movies & TV Stack Exchange",

View File

@@ -18,13 +18,8 @@ protected:
const int PORT = 8002;
protected:
void resetServer(ZimFileServer::Options options) {
zfs1_.reset();
zfs1_.reset(new ZimFileServer(PORT, options, "./test/library.xml"));
}
void SetUp() override {
zfs1_.reset(new ZimFileServer(PORT, ZimFileServer::DEFAULT_OPTIONS, "./test/library.xml"));
zfs1_.reset(new ZimFileServer(PORT, "./test/library.xml"));
}
void TearDown() override {
@@ -75,20 +70,20 @@ std::string maskVariableOPDSFeedData(std::string s)
" type=\"application/opensearchdescription+xml\"" \
" href=\"/ROOT/catalog/searchdescription.xml\" />\n"
#define CATALOG_ENTRY(UUID, TITLE, SUMMARY, LANG, NAME, CATEGORY, TAGS, EXTRA_LINK, CONTENT_NAME, FILE_NAME, LENGTH) \
#define CHARLES_RAY_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:" UUID "</id>\n" \
" <title>" TITLE "</title>\n" \
" <id>urn:uuid:charlesray</id>\n" \
" <title>Charles, Ray</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>" SUMMARY "</summary>\n" \
" <language>" LANG "</language>\n" \
" <name>" NAME "</name>\n" \
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
" <language>fra</language>\n" \
" <name>wikipedia_fr_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category>" CATEGORY "</category>\n" \
" <tags>" TAGS "</tags>\n" \
" <category>jazz</category>\n" \
" <tags>unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" " EXTRA_LINK "<link type=\"text/html\" href=\"/ROOT/content/" CONTENT_NAME "\" />\n" \
" <link type=\"text/html\" href=\"/ROOT/content/zimfile%26other\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
" </author>\n" \
@@ -96,59 +91,59 @@ std::string maskVariableOPDSFeedData(std::string s)
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/" FILE_NAME ".zim\" length=\"" LENGTH "\" />\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile%26other.zim\" length=\"569344\" />\n" \
" </entry>\n"
#define RAY_CHARLES_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:raycharles</id>\n" \
" <title>Ray Charles</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
" <language>eng</language>\n" \
" <name>wikipedia_en_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category>wikipedia</category>\n" \
" <tags>public_tag_without_a_value;_private_tag_without_a_value;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/ROOT/catalog/v2/illustration/raycharles/?size=48\"\n" \
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
" <link type=\"text/html\" href=\"/ROOT/content/zimfile\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
" </author>\n" \
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"569344\" />\n" \
" </entry>\n"
#define _CHARLES_RAY_CATALOG_ENTRY(CONTENT_NAME) CATALOG_ENTRY( \
"charlesray", \
"Charles, Ray", \
"Wikipedia articles about Ray Charles", \
"fra", \
"wikipedia_fr_ray_charles",\
"jazz",\
"unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes",\
"", \
CONTENT_NAME, \
"zimfile%26other", \
"569344" \
)
#define CHARLES_RAY_CATALOG_ENTRY _CHARLES_RAY_CATALOG_ENTRY("zimfile%26other")
#define CHARLES_RAY_CATALOG_ENTRY_NO_MAPPER _CHARLES_RAY_CATALOG_ENTRY("charlesray")
#define _RAY_CHARLES_CATALOG_ENTRY(CONTENT_NAME) CATALOG_ENTRY(\
"raycharles",\
"Ray Charles",\
"Wikipedia articles about Ray Charles",\
"eng",\
"wikipedia_en_ray_charles",\
"wikipedia",\
"public_tag_without_a_value;_private_tag_without_a_value;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes",\
"<link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/ROOT/catalog/v2/illustration/raycharles/?size=48\"\n" \
" type=\"image/png;width=48;height=48;scale=1\"/>\n ", \
CONTENT_NAME, \
"zimfile", \
"569344"\
)
#define RAY_CHARLES_CATALOG_ENTRY _RAY_CHARLES_CATALOG_ENTRY("zimfile")
#define RAY_CHARLES_CATALOG_ENTRY_NO_MAPPER _RAY_CHARLES_CATALOG_ENTRY("raycharles")
#define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY CATALOG_ENTRY(\
"raycharles_uncategorized",\
"Ray (uncategorized) Charles",\
"No category is assigned to this library entry.",\
"rus",\
"wikipedia_ru_ray_charles",\
"",\
"public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no",\
"",\
"zimfile", \
"zimfile", \
"125952"\
)
#define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:raycharles_uncategorized</id>\n" \
" <title>Ray (uncategorized) Charles</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>No category is assigned to this library entry.</summary>\n" \
" <language>rus</language>\n" \
" <name>wikipedia_ru_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category></category>\n" \
" <tags>public_tag_with_a_value:value_of_a_public_tag;_private_tag_with_a_value:value_of_a_private_tag;wikipedia;_pictures:no;_videos:no;_details:no</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <link type=\"text/html\" href=\"/ROOT/content/zimfile\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
" </author>\n" \
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <dc:issued>2020-03-31T00:00:00Z</dc:issued>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"125952\" />\n" \
" </entry>\n"
TEST_F(LibraryServerTest, catalog_root_xml)
{
@@ -317,44 +312,6 @@ TEST_F(LibraryServerTest, catalog_search_by_category)
);
}
TEST_F(LibraryServerTest, catalog_search_by_language)
{
{
const auto r = zfs1_->GET("/ROOT/catalog/search?lang=eng");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (lang=eng)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/search?lang=eng,fra");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (lang=eng,fra)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_search_results_pagination)
{
{
@@ -397,7 +354,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (start=1&amp;count=1)</title>\n"
" <title>Filtered zims (count=1&amp;start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
@@ -413,7 +370,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (start=100&amp;count=10)</title>\n"
" <title>Filtered zims (count=10&amp;start=100)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>100</startIndex>\n"
@@ -676,8 +633,8 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range)
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?start=1&count=1");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?start=1&count=1")
" <title>Filtered Entries (start=1&amp;count=1)</title>\n"
CATALOG_V2_ENTRIES_PREAMBLE("?count=1&start=1")
" <title>Filtered Entries (count=1&amp;start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
@@ -705,40 +662,6 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
);
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_language)
{
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?lang=eng");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?lang=eng")
" <title>Filtered Entries (lang=eng)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?lang=eng,fra");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?lang=eng,fra")
" <title>Filtered Entries (lang=eng,fra)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_v2_individual_entry_access)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entry/raycharles");
@@ -857,40 +780,4 @@ TEST_F(LibraryServerTest, catalog_search_excludes_hidden_tags)
#undef EXPECT_ZERO_RESULTS
}
TEST_F(LibraryServerTest, no_name_mapper_returned_catalog_use_uuid_in_link)
{
resetServer(ZimFileServer::NO_NAME_MAPPER);
const auto r = zfs1_->GET("/ROOT/catalog/search?tag=_category:jazz");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (tag=_category:jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY_NO_MAPPER
"</feed>\n"
);
}
TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
{
resetServer(ZimFileServer::NO_NAME_MAPPER);
const auto r = zfs1_->GET("/ROOT/catalog/v2/entry/raycharles");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
RAY_CHARLES_CATALOG_ENTRY_NO_MAPPER
);
const auto r1 = zfs1_->GET("/ROOT/catalog/v2/entry/non-existent-entry");
EXPECT_EQ(r1->status, 404);
}
#undef EXPECT_SEARCH_RESULTS

View File

@@ -2,9 +2,9 @@ tests = [
'library',
'regex',
'tagParsing',
'counterParsing',
'stringTools',
'pathTools',
'otherTools',
'kiwixserve',
'book',
'manager',
@@ -37,7 +37,6 @@ if gtest_dep.found() and not meson.is_cross_build()
'corner_cases.zim',
'poor.zim',
'library.xml',
'lib_for_server_search_test.xml',
'customized_resources.txt',
'helloworld.txt',
'welcome.html',

View File

@@ -1,174 +0,0 @@
/*
* Copyright (C) 2022 Veloman Yunkan
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
* NON-INFRINGEMENT. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "gtest/gtest.h"
#include "../src/tools/otherTools.h"
#include "zim/suggestion_iterator.h"
#include <regex>
namespace
{
// Output generated via mustache templates sometimes contains end-of-line
// whitespace. This complicates representing the expected output of a unit-test
// as C++ raw strings in editors that are configured to delete EOL whitespace.
// A workaround is to put special markers (//EOLWHITESPACEMARKER) at the end
// of such lines in the expected output string and remove them at runtime.
// This is exactly what this function is for.
std::string removeEOLWhitespaceMarkers(const std::string& s)
{
const std::regex pattern("//EOLWHITESPACEMARKER");
return std::regex_replace(s, pattern, "");
}
} // unnamed namespace
#define CHECK_SUGGESTIONS(actual, expected) \
EXPECT_EQ(actual, removeEOLWhitespaceMarkers(expected))
TEST(Suggestions, basicTest)
{
kiwix::Suggestions s;
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
//EOLWHITESPACEMARKER
]
)EXPECTEDJSON"
);
s.add(zim::SuggestionItem("Title", "/PATH", "Snippet"));
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "Title",
"label" : "Snippet",
"kind" : "path"
, "path" : "/PATH"
}
]
)EXPECTEDJSON"
);
s.add(zim::SuggestionItem("Title Without Snippet", "/P/a/t/h"));
s.addFTSearchSuggestion("en", "kiwi");
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "Title",
"label" : "Snippet",
"kind" : "path"
, "path" : "/PATH"
},
{
"value" : "Title Without Snippet",
"label" : "Title Without Snippet",
"kind" : "path"
, "path" : "/P/a/t/h"
},
{
"value" : "kiwi ",
"label" : "containing &apos;kiwi&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}
]
)EXPECTEDJSON"
);
}
TEST(Suggestions, specialCharHandling)
{
// HTML special symbols (<, >, &, ", and ') must be HTML-escaped
// Backslash symbols (\) must be duplicated.
const std::string SYMBOLS(R"(\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?)");
{
kiwix::Suggestions s;
s.add(zim::SuggestionItem("Title with " + SYMBOLS,
"Path with " + SYMBOLS,
"Snippet with " + SYMBOLS));
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "Title with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippet with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"kind" : "path"
, "path" : "Path with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
}
]
)EXPECTEDJSON"
);
}
{
kiwix::Suggestions s;
s.add(zim::SuggestionItem("Snippetless title with " + SYMBOLS,
"Path with " + SYMBOLS));
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "Snippetless title with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippetless title with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"kind" : "path"
, "path" : "Path with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
}
]
)EXPECTEDJSON"
);
}
{
kiwix::Suggestions s;
s.addFTSearchSuggestion("eng", "text with " + SYMBOLS);
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "text with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.? ",
"label" : "containing &apos;text with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}
]
)EXPECTEDJSON"
);
}
}
TEST(Suggestions, fulltextSearchSuggestionIsTranslated)
{
kiwix::Suggestions s;
s.addFTSearchSuggestion("it", "kiwi");
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "kiwi ",
"label" : "contenente &apos;kiwi&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}
]
)EXPECTEDJSON"
);
}

View File

@@ -23,19 +23,13 @@ T1 concat(T1 a, const T2& b)
return a;
}
enum ResourceKind
{
ZIM_CONTENT,
STATIC_CONTENT,
DYNAMIC_CONTENT,
};
const bool WITH_ETAG = true;
const bool NO_ETAG = false;
struct Resource
{
ResourceKind kind;
bool etag_expected;
const char* url;
bool etag_expected() const { return kind != STATIC_CONTENT; }
};
std::ostream& operator<<(std::ostream& out, const Resource& r)
@@ -47,127 +41,55 @@ std::ostream& operator<<(std::ostream& out, const Resource& r)
typedef std::vector<Resource> ResourceCollection;
const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT/" },
{ WITH_ETAG, "/ROOT/" },
{ DYNAMIC_CONTENT, "/ROOT/viewer" },
{ DYNAMIC_CONTENT, "/ROOT/viewer?cacheid=whatever" },
{ WITH_ETAG, "/ROOT/skin/autoComplete.min.js" },
{ WITH_ETAG, "/ROOT/skin/css/autoComplete.css" },
{ WITH_ETAG, "/ROOT/skin/taskbar.css" },
{ DYNAMIC_CONTENT, "/ROOT/skin/autoComplete.min.js" },
{ STATIC_CONTENT, "/ROOT/skin/autoComplete.min.js?cacheid=1191aaaf" },
{ DYNAMIC_CONTENT, "/ROOT/skin/css/autoComplete.css" },
{ STATIC_CONTENT, "/ROOT/skin/css/autoComplete.css?cacheid=08951e06" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/favicon.ico" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/favicon.ico?cacheid=fba03a27" },
{ DYNAMIC_CONTENT, "/ROOT/skin/index.css" },
{ STATIC_CONTENT, "/ROOT/skin/index.css?cacheid=0f9ba34e" },
{ DYNAMIC_CONTENT, "/ROOT/skin/index.js" },
{ STATIC_CONTENT, "/ROOT/skin/index.js?cacheid=2f5a81ac" },
{ DYNAMIC_CONTENT, "/ROOT/skin/iso6391To3.js" },
{ STATIC_CONTENT, "/ROOT/skin/iso6391To3.js?cacheid=ecde2bb3" },
{ DYNAMIC_CONTENT, "/ROOT/skin/isotope.pkgd.min.js" },
{ STATIC_CONTENT, "/ROOT/skin/isotope.pkgd.min.js?cacheid=2e48d392" },
{ DYNAMIC_CONTENT, "/ROOT/skin/taskbar.css" },
{ STATIC_CONTENT, "/ROOT/skin/taskbar.css?cacheid=216d6b5d" },
{ DYNAMIC_CONTENT, "/ROOT/skin/viewer.js" },
{ STATIC_CONTENT, "/ROOT/skin/viewer.js?cacheid=51e745c2" },
{ NO_ETAG, "/ROOT/catalog/search" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/search" },
{ NO_ETAG, "/ROOT/search?content=zimfile&pattern=a" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/v2/root.xml" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/v2/languages" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/v2/entries" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/v2/partial_entries" },
{ NO_ETAG, "/ROOT/suggest?content=zimfile&term=ray" },
{ DYNAMIC_CONTENT, "/ROOT/search?content=zimfile&pattern=a" },
{ WITH_ETAG, "/ROOT/content/zimfile/A/index" },
{ WITH_ETAG, "/ROOT/content/zimfile/A/Ray_Charles" },
{ DYNAMIC_CONTENT, "/ROOT/suggest?content=zimfile&term=ray" },
{ ZIM_CONTENT, "/ROOT/content/zimfile/A/index" },
{ ZIM_CONTENT, "/ROOT/content/zimfile/A/Ray_Charles" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/content/A/index" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/content/A/Ray_Charles" },
{ WITH_ETAG, "/ROOT/raw/zimfile/content/A/index" },
{ WITH_ETAG, "/ROOT/raw/zimfile/content/A/Ray_Charles" },
};
const ResourceCollection resources200Uncompressible{
{ DYNAMIC_CONTENT, "/ROOT/skin/bittorrent.png" },
{ STATIC_CONTENT, "/ROOT/skin/bittorrent.png?cacheid=4f5c6882" },
{ DYNAMIC_CONTENT, "/ROOT/skin/blank.html" },
{ STATIC_CONTENT, "/ROOT/skin/blank.html?cacheid=6b1fa032" },
{ DYNAMIC_CONTENT, "/ROOT/skin/caret.png" },
{ STATIC_CONTENT, "/ROOT/skin/caret.png?cacheid=22b942b4" },
{ DYNAMIC_CONTENT, "/ROOT/skin/download.png" },
{ STATIC_CONTENT, "/ROOT/skin/download.png?cacheid=a39aa502" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/android-chrome-192x192.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/android-chrome-192x192.png?cacheid=bfac158b" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/android-chrome-512x512.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/android-chrome-512x512.png?cacheid=380c3653" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/apple-touch-icon.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/browserconfig.xml" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/browserconfig.xml?cacheid=f29a7c4a" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/favicon-16x16.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/favicon-16x16.png?cacheid=a986fedc" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/favicon-32x32.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/favicon-32x32.png?cacheid=79ded625" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/mstile-144x144.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/mstile-144x144.png?cacheid=c25a7641" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/mstile-150x150.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/mstile-150x150.png?cacheid=6fa6f467" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/mstile-310x150.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/mstile-310x150.png?cacheid=e0ed9032" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/mstile-310x310.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/mstile-310x310.png?cacheid=26b20530" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/mstile-70x70.png" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/mstile-70x70.png?cacheid=64ffd9dc" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/safari-pinned-tab.svg" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95" },
{ DYNAMIC_CONTENT, "/ROOT/skin/favicon/site.webmanifest" },
{ STATIC_CONTENT, "/ROOT/skin/favicon/site.webmanifest?cacheid=bc396efb" },
{ DYNAMIC_CONTENT, "/ROOT/skin/fonts/Poppins.ttf" },
{ STATIC_CONTENT, "/ROOT/skin/fonts/Poppins.ttf?cacheid=af705837" },
{ DYNAMIC_CONTENT, "/ROOT/skin/fonts/Roboto.ttf" },
{ STATIC_CONTENT, "/ROOT/skin/fonts/Roboto.ttf?cacheid=84d10248" },
{ DYNAMIC_CONTENT, "/ROOT/skin/hash.png" },
{ STATIC_CONTENT, "/ROOT/skin/hash.png?cacheid=f836e872" },
{ DYNAMIC_CONTENT, "/ROOT/skin/magnet.png" },
{ STATIC_CONTENT, "/ROOT/skin/magnet.png?cacheid=73b6bddf" },
{ DYNAMIC_CONTENT, "/ROOT/skin/search-icon.svg" },
{ STATIC_CONTENT, "/ROOT/skin/search-icon.svg?cacheid=b10ae7ed" },
{ DYNAMIC_CONTENT, "/ROOT/skin/search_results.css" },
{ STATIC_CONTENT, "/ROOT/skin/search_results.css?cacheid=76d39c84" },
{ WITH_ETAG, "/ROOT/skin/caret.png" },
{ WITH_ETAG, "/ROOT/skin/css/images/search.svg" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Title" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Description" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Language" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Name" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Tags" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Date" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Creator" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Publisher" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Title" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Description" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Language" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Name" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Tags" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Date" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Creator" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Publisher" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/root.xml" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/searchdescription.xml" },
{ NO_ETAG, "/ROOT/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/v2/categories" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/v2/searchdescription.xml" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" },
{ NO_ETAG, "/ROOT/catch/external?source=www.example.com" },
{ DYNAMIC_CONTENT, "/ROOT/catch/external?source=www.example.com" },
{ WITH_ETAG, "/ROOT/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
{ ZIM_CONTENT, "/ROOT/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
{ ZIM_CONTENT, "/ROOT/content/corner_cases/A/empty.html" },
{ ZIM_CONTENT, "/ROOT/content/corner_cases/-/empty.css" },
{ ZIM_CONTENT, "/ROOT/content/corner_cases/-/empty.js" },
{ WITH_ETAG, "/ROOT/content/corner_cases/A/empty.html" },
{ WITH_ETAG, "/ROOT/content/corner_cases/-/empty.css" },
{ WITH_ETAG, "/ROOT/content/corner_cases/-/empty.js" },
// The following url's responses are too small to be compressed
{ DYNAMIC_CONTENT, "/ROOT/catalog/root.xml" },
{ DYNAMIC_CONTENT, "/ROOT/catalog/searchdescription.xml" },
{ DYNAMIC_CONTENT, "/ROOT/suggest?content=zimfile" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Creator" },
{ ZIM_CONTENT, "/ROOT/raw/zimfile/meta/Title" },
{ NO_ETAG, "/ROOT/catalog/root.xml" },
{ NO_ETAG, "/ROOT/catalog/searchdescription.xml" },
{ NO_ETAG, "/ROOT/suggest?content=zimfile" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Creator" },
{ WITH_ETAG, "/ROOT/raw/zimfile/meta/Title" },
};
ResourceCollection all200Resources()
@@ -209,15 +131,6 @@ TEST_F(ServerTest, 200)
EXPECT_EQ(200, zfs1_->GET(res.url)->status) << "res.url: " << res.url;
}
TEST_F(ServerTest, 200_IdNameMapper)
{
EXPECT_EQ(404, zfs1_->GET("/ROOT/content/6f1d19d0-633f-087b-fb55-7ac324ff9baf/A/index")->status);
EXPECT_EQ(200, zfs1_->GET("/ROOT/content/zimfile/A/index")->status);
resetServer(ZimFileServer::NO_NAME_MAPPER);
EXPECT_EQ(200, zfs1_->GET("/ROOT/content/6f1d19d0-633f-087b-fb55-7ac324ff9baf/A/index")->status);
EXPECT_EQ(404, zfs1_->GET("/ROOT/content/zimfile/A/index")->status);
}
TEST_F(ServerTest, CompressibleContentIsCompressedIfAcceptable)
{
for ( const Resource& res : resources200Compressible ) {
@@ -259,11 +172,11 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
const std::vector<UrlAndExpectedResult> testData{
{
/* url */ "/ROOT/",
R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=0f9ba34e"
R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=3b470cee"
<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">
<link rel="manifest" href="/ROOT/skin/favicon/site.webmanifest?cacheid=bc396efb">
<link rel="manifest" href="/ROOT/skin/favicon/site.webmanifest">
<link rel="mask-icon" href="/ROOT/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95" color="#5bbad5">
<link rel="shortcut icon" href="/ROOT/skin/favicon/favicon.ico?cacheid=fba03a27">
<meta name="msapplication-config" content="/ROOT/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
@@ -272,11 +185,6 @@ R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=0f9ba34e"
<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>
)EXPECTEDRESULT"
},
{
/* url */ "/ROOT/skin/index.css",
R"EXPECTEDRESULT( background-image: url('../skin/search-icon.svg?cacheid=b10ae7ed');
)EXPECTEDRESULT"
},
{
@@ -291,11 +199,10 @@ R"EXPECTEDRESULT( <img src="../skin/download.png?
/* 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=51e745c2" defer></script>
<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?cacheid=6b1fa032";
const blankPageUrl = `${root}/skin/blank.html`;
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
src="./skin/blank.html?cacheid=6b1fa032" title="ZIM content" width="100%"
)EXPECTEDRESULT"
},
{
@@ -345,7 +252,6 @@ const char* urls404[] = {
"/",
"/zimfile",
"/ROOT/skin/non-existent-skin-resource",
"/ROOT/skin/autoComplete.min.js?cacheid=wrongcacheid",
"/ROOT/catalog",
"/ROOT/catalog/",
"/ROOT/catalog/non-existent-item",
@@ -404,11 +310,6 @@ std::string getHeaderValue(const Headers& headers, const std::string& name)
return er.first->second;
}
std::string getCacheControlHeader(const httplib::Response& r)
{
return getHeaderValue(r.headers, "Cache-Control");
}
TEST_F(CustomizedServerTest, NewResourcesCanBeAdded)
{
// ServerTest.404 verifies that "/ROOT/non-existent-item" doesn't exist
@@ -611,12 +512,12 @@ TEST_F(ServerTest, Http404HtmlError)
</p>
)" },
{ /* url */ "/ROOT/random?content=non-existent-book&userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
{ /* url */ "/ROOT/random?content=non-existent-book&userlang=hy",
expected_page_title=="Սխալ հասցե" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<h1>Սխալ հասցե</h1>
<p>
[I18N TESTING] No such book: non-existent-book. Sorry.
Գիրքը բացակայում է՝ non-existent-book
</p>
)" },
@@ -636,12 +537,12 @@ TEST_F(ServerTest, Http404HtmlError)
</p>
)" },
{ /* url */ "/ROOT/catalog/?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
{ /* url */ "/ROOT/catalog/?userlang=hy",
expected_page_title=="Սխալ հասցե" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<h1>Սխալ հասցե</h1>
<p>
[I18N TESTING] URL not found: /ROOT/catalog/
Սխալ հասցե՝ /ROOT/catalog/
</p>
)" },
@@ -653,12 +554,12 @@ TEST_F(ServerTest, Http404HtmlError)
</p>
)" },
{ /* url */ "/ROOT/catalog/invalid_endpoint?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
{ /* url */ "/ROOT/catalog/invalid_endpoint?userlang=hy",
expected_page_title=="Սխալ հասցե" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<h1>Սխալ հասցե</h1>
<p>
[I18N TESTING] URL not found: /ROOT/catalog/invalid_endpoint
Սխալ հասցե՝ /ROOT/catalog/invalid_endpoint
</p>
)" },
@@ -710,17 +611,17 @@ TEST_F(ServerTest, Http404HtmlError)
</p>
)" },
{ /* url */ "/ROOT/content/zimfile/invalid-article?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
{ /* url */ "/ROOT/content/zimfile/invalid-article?userlang=hy",
expected_page_title=="Սխալ հասցե" &&
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<h1>Սխալ հասցե</h1>
<p>
[I18N TESTING] URL not found: /ROOT/content/zimfile/invalid-article
Սխալ հասցե՝ /ROOT/content/zimfile/invalid-article
</p>
<p>
[I18N TESTING] Make a full text search for <a href="/ROOT/search?content=zimfile&pattern=invalid-article">invalid-article</a>
Որոնել <a href="/ROOT/search?content=zimfile&pattern=invalid-article">invalid-article</a>
</p>
)" },
@@ -839,7 +740,7 @@ TEST_F(ServerTest, Http400HtmlError)
expected_body==R"(
<h1>Invalid request</h1>
<p>
The requested URL "/ROOT/search?books.filter.lang=eng&pattern" is not a valid request.
The requested URL "/ROOT/search?books.filter.lang=eng&pattern=" is not a valid request.
</p>
<p>
No query provided.
@@ -896,21 +797,21 @@ TEST_F(ServerTest, HttpXmlError)
/* HTTP status code */ 400,
/* expected response XML */ R"(
<error>Invalid request</error>
<detail>The requested URL "/ROOT/search?format=xml&content=zimfile" is not a valid request.</detail>
<detail>The requested URL "/ROOT/search?content=zimfile&format=xml" is not a valid request.</detail>
<detail>No query provided.</detail>
)" },
{ /* url */ "/ROOT/search?format=xml&content=non-existing-book&pattern=asdfqwerty",
/* HTTP status code */ 400,
/* expected response XML */ R"(
<error>Invalid request</error>
<detail>The requested URL "/ROOT/search?format=xml&content=non-existing-book&pattern=asdfqwerty" is not a valid request.</detail>
<detail>The requested URL "/ROOT/search?content=non-existing-book&format=xml&pattern=asdfqwerty" is not a valid request.</detail>
<detail>No such book: non-existing-book</detail>
)" },
{ /* url */ "/ROOT/search?format=xml&content=non-existing-book&pattern=a\"<script foo>",
/* HTTP status code */ 400,
/* expected response XML */ R"(
<error>Invalid request</error>
<detail>The requested URL "/ROOT/search?format=xml&content=non-existing-book&pattern=a"&lt;script foo&gt;" is not a valid request.</detail>
<detail>The requested URL "/ROOT/search?content=non-existing-book&format=xml&pattern=a"&lt;script foo&gt;" is not a valid request.</detail>
<detail>No such book: non-existing-book</detail>
)" },
// There is a flaw in our way to handle query string, we cannot differenciate
@@ -919,7 +820,7 @@ TEST_F(ServerTest, HttpXmlError)
/* HTTP status code */ 400,
/* expected response XML */ R"(
<error>Invalid request</error>
<detail>The requested URL "/ROOT/search?format=xml&books.filter.lang=eng&pattern" is not a valid request.</detail>
<detail>The requested URL "/ROOT/search?books.filter.lang=eng&format=xml&pattern=" is not a valid request.</detail>
<detail>No query provided.</detail>
)" },
{ /* url */ "/ROOT/search?format=xml&pattern=foo",
@@ -1001,9 +902,9 @@ TEST_F(ServerTest, UserLanguageControl)
/* expected <h1> */ "Not Found"
},
{
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=test",
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=hy",
/*Accept-Language:*/ "",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
/* expected <h1> */ "Սխալ հասցե"
},
{
/*url*/ "/ROOT/content/zimfile/invalid-article",
@@ -1012,13 +913,13 @@ TEST_F(ServerTest, UserLanguageControl)
},
{
/*url*/ "/ROOT/content/zimfile/invalid-article",
/*Accept-Language:*/ "test",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
/*Accept-Language:*/ "hy",
/* expected <h1> */ "Սխալ հասցե"
},
{
// userlang query parameter takes precedence over Accept-Language
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "test",
/*Accept-Language:*/ "hy",
/* expected <h1> */ "Not Found"
},
{
@@ -1026,7 +927,7 @@ TEST_F(ServerTest, UserLanguageControl)
// In case of a comma separated list of languages (optionally weighted
// with quality values) the default (en) language is used instead.
/*url*/ "/ROOT/content/zimfile/invalid-article",
/*Accept-Language:*/ "test;q=0.9, en;q=0.2",
/*Accept-Language:*/ "hy;q=0.9, en;q=0.2",
/* expected <h1> */ "Not Found"
},
};
@@ -1051,8 +952,6 @@ TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle)
ASSERT_EQ(302, g->status);
ASSERT_TRUE(g->has_header("Location"));
ASSERT_TRUE(kiwix::startsWith(g->get_header_value("Location"), "/ROOT/content/zimfile/A/"));
ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate");
ASSERT_FALSE(g->has_header("ETag"));
}
TEST_F(ServerTest, NonEndpointUrlsAreRedirectedToContentUrls)
@@ -1096,8 +995,6 @@ TEST_F(ServerTest, NonEndpointUrlsAreRedirectedToContentUrls)
ASSERT_EQ(302, g->status) << ctx;
ASSERT_TRUE(g->has_header("Location")) << ctx;
ASSERT_EQ("/ROOT/content" + p, g->get_header_value("Location")) << ctx;
ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate");
ASSERT_FALSE(g->has_header("ETag"));
}
}
@@ -1162,45 +1059,12 @@ TEST_F(ServerTest, HeadersAreTheSameInResponsesToHeadAndGetRequests)
}
}
TEST_F(ServerTest, CacheControlOfZimContent)
{
for ( const Resource& res : all200Resources() ) {
if ( res.kind == ZIM_CONTENT ) {
const auto g = zfs1_->GET(res.url);
EXPECT_EQ(getCacheControlHeader(*g), "max-age=3600, must-revalidate") << res;
EXPECT_TRUE(g->has_header("ETag")) << res;
}
}
}
TEST_F(ServerTest, CacheControlOfStaticContent)
{
for ( const Resource& res : all200Resources() ) {
if ( res.kind == STATIC_CONTENT ) {
const auto g = zfs1_->GET(res.url);
EXPECT_EQ(getCacheControlHeader(*g), "max-age=31536000, immutable") << res;
EXPECT_FALSE(g->has_header("ETag")) << res;
}
}
}
TEST_F(ServerTest, CacheControlOfDynamicContent)
{
for ( const Resource& res : all200Resources() ) {
if ( res.kind == DYNAMIC_CONTENT ) {
const auto g = zfs1_->GET(res.url);
EXPECT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate") << res;
EXPECT_TRUE(g->has_header("ETag")) << res;
}
}
}
TEST_F(ServerTest, ETagHeaderIsSetAsNeeded)
{
for ( const Resource& res : all200Resources() ) {
const auto responseToGet = zfs1_->GET(res.url);
EXPECT_EQ(res.etag_expected(), responseToGet->has_header("ETag")) << res;
if ( res.etag_expected() ) {
EXPECT_EQ(res.etag_expected, responseToGet->has_header("ETag")) << res;
if ( res.etag_expected ) {
EXPECT_TRUE(is_valid_etag(responseToGet->get_header_value("ETag")));
}
}
@@ -1224,32 +1088,21 @@ TEST_F(ServerTest, ETagIsTheSameAcrossHeadAndGet)
}
}
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETagsForDynamicContent)
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETags)
{
ZimFileServer zfs2(SERVER_PORT + 1, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES);
for ( const Resource& res : all200Resources() ) {
if ( res.kind != DYNAMIC_CONTENT ) continue;
if ( !res.etag_expected ) continue;
const auto h1 = zfs1_->HEAD(res.url);
const auto h2 = zfs2.HEAD(res.url);
EXPECT_NE(h1->get_header_value("ETag"), h2->get_header_value("ETag"));
}
}
TEST_F(ServerTest, DifferentServerInstancesProduceIdenticalETagsForZimContent)
{
ZimFileServer zfs2(SERVER_PORT + 1, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES);
for ( const Resource& res : all200Resources() ) {
if ( res.kind != ZIM_CONTENT ) continue;
const auto h1 = zfs1_->HEAD(res.url);
const auto h2 = zfs2.HEAD(res.url);
EXPECT_EQ(h1->get_header_value("ETag"), h2->get_header_value("ETag"));
}
}
TEST_F(ServerTest, CompressionInfluencesETag)
{
for ( const Resource& res : resources200Compressible ) {
if ( ! res.etag_expected() ) continue;
if ( ! res.etag_expected ) continue;
const auto g1 = zfs1_->GET(res.url);
const auto g2 = zfs1_->GET(res.url, { {"Accept-Encoding", ""} } );
const auto g3 = zfs1_->GET(res.url, { {"Accept-Encoding", "gzip"} } );
@@ -1262,7 +1115,7 @@ TEST_F(ServerTest, CompressionInfluencesETag)
TEST_F(ServerTest, ETagOfUncompressibleContentIsNotAffectedByAcceptEncoding)
{
for ( const Resource& res : resources200Uncompressible ) {
if ( ! res.etag_expected() ) continue;
if ( ! res.etag_expected ) continue;
const auto g1 = zfs1_->GET(res.url);
const auto g2 = zfs1_->GET(res.url, { {"Accept-Encoding", ""} } );
const auto g3 = zfs1_->GET(res.url, { {"Accept-Encoding", "gzip"} } );
@@ -1307,7 +1160,7 @@ TEST_F(ServerTest, IfNoneMatchRequestsWithMatchingETagResultIn304Responses)
const char* const encodings[] = { "", "gzip" };
for ( const Resource& res : all200Resources() ) {
for ( const char* enc: encodings ) {
if ( ! res.etag_expected() ) continue;
if ( ! res.etag_expected ) continue;
const TestContext ctx{ {"url", res.url}, {"encoding", enc} };
const auto g = zfs1_->GET(res.url, { {"Accept-Encoding", enc} });
@@ -1334,8 +1187,8 @@ TEST_F(ServerTest, IfNoneMatchRequestsWithMismatchingETagResultIn200Responses)
const auto etag2 = etag.substr(0, etag.size() - 1) + "x\"";
const auto h = zfs1_->HEAD(res.url, { {"If-None-Match", etag2} } );
const auto g2 = zfs1_->GET(res.url, { {"If-None-Match", etag2} } );
EXPECT_EQ(200, h->status) << res;
EXPECT_EQ(200, g2->status) << res;
EXPECT_EQ(200, h->status);
EXPECT_EQ(200, g2->status);
}
}
@@ -1542,11 +1395,11 @@ R"EXPECTEDRESPONSE([
]
)EXPECTEDRESPONSE"
},
{ /* url: */ "/ROOT/suggest?content=zimfile&term=abracadabra&userlang=test",
{ /* url: */ "/ROOT/suggest?content=zimfile&term=abracadabra&userlang=hy",
R"EXPECTEDRESPONSE([
{
"value" : "abracadabra ",
"label" : "[I18N TESTING] cOnTaInInG &apos;abracadabra&apos;...",
"label" : "որոնել &apos;abracadabra&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}

View File

@@ -6,7 +6,6 @@
#define SERVER_PORT 8101
#include "server_testing_tools.h"
std::string makeSearchResultsHtml(const std::string& pattern,
const std::string& header,
const std::string& results,
@@ -556,7 +555,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
//
// In order to be able to share the same expected output data
// LARGE_SEARCH_RESULTS between multiple build platforms and test-points
// of the ServerSearchTest.searchResults test-case
// of the ServerTest.searchResults test-case
//
// 1. Snippets are excluded from the plain-text comparison of actual and
// expected HTML strings. This is done with the help of the
@@ -917,7 +916,7 @@ struct TestData
}
};
TEST(ServerSearchTest, searchResults)
TEST_F(ServerTest, searchResults)
{
const TestData testData[] = {
{
@@ -1341,12 +1340,14 @@ TEST(ServerSearchTest, searchResults)
/* pagination */ {}
},
// Only RayCharles is in English.
// [TODO] We should extend our test data to have another zim file in english returning results.
{
/* query */ "pattern=travel"
"&books.filter.lang=eng",
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 2,
/* totalResultCount */ 1,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
@@ -1356,14 +1357,6 @@ TEST(ServerSearchTest, searchResults)
/*bookTitle*/ "Ray Charles",
/*wordCount*/ "204"
),
SEARCH_RESULT(
/*link*/ "/ROOT/content/example/Wikibooks.html",
/*title*/ "Wikibooks",
/*snippet*/ R"SNIPPET(...<b>Travel</b> guide Wikidata Knowledge database Commons Media repository Meta Coordination MediaWiki MediaWiki software Phabricator MediaWiki bug tracker Wikimedia Labs MediaWiki development The Wikimedia Foundation is a non-profit organization that depends on your voluntarism and donations to operate. If you find Wikibooks or other projects hosted by the Wikimedia Foundation useful, please volunteer or make a donation. Your donations primarily helps to purchase server equipment, launch new projects......)SNIPPET",
/*bookTitle*/ "Wikibooks",
/*wordCount*/ "538"
)
},
/* pagination */ {}
},
@@ -1458,86 +1451,15 @@ TEST(ServerSearchTest, searchResults)
},
};
ZimFileServer zfs(SERVER_PORT, ZimFileServer::DEFAULT_OPTIONS,
"./test/lib_for_server_search_test.xml");
for ( const auto& t : testData ) {
const std::string htmlSearchUrl = t.url();
const auto htmlRes = zfs.GET(htmlSearchUrl.c_str());
const auto htmlRes = zfs1_->GET(htmlSearchUrl.c_str());
EXPECT_EQ(htmlRes->status, 200);
t.checkHtml(htmlRes->body);
const std::string xmlSearchUrl = t.xmlSearchUrl();
const auto xmlRes = zfs.GET(xmlSearchUrl.c_str());
const auto xmlRes = zfs1_->GET(xmlSearchUrl.c_str());
EXPECT_EQ(xmlRes->status, 200);
t.checkXml(xmlRes->body);
}
}
std::string expectedConfusionOfTonguesErrorHtml(std::string url)
{
return R"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Invalid request</title>
</head>
<body>
<h1>Invalid request</h1>
<p>
The requested URL ")" + url + R"(" is not a valid request.
</p>
<p>
Two or more books in different languages would participate in search, which may lead to confusing results.
</p>
</body>
</html>
)";
}
std::string expectedConfusionOfTonguesErrorXml(std::string url)
{
return R"(<?xml version="1.0" encoding="UTF-8">
<error>Invalid request</error>
<detail>The requested URL ")" + url + R"(" is not a valid request.</detail>
<detail>Two or more books in different languages would participate in search, which may lead to confusing results.</detail>
)";
}
TEST(ServerSearchTest, searchInMultilanguageBookSetIsDenied)
{
const std::string testQueries[] = {
"pattern=towerofbabel",
"pattern=babylon&books.filter.maxsize=1000000",
"pattern=baby&books.id=" RAYCHARLESZIMID "&books.id=" EXAMPLEZIMID,
};
// The default limit on the number of books in a multi-zim search is 3
const ZimFileServer::FilePathCollection ZIMFILES{
"./test/zimfile.zim", // eng
"./test/example.zim", // en
"./test/corner_cases.zim" // =en
};
ZimFileServer zfs(SERVER_PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES);
for ( const auto& q : testQueries ) {
{
// HTML mode
const std::string url = "/ROOT/search?" + q;
const auto r = zfs.GET(url.c_str());
const TestContext ctx{ {"url", url} };
EXPECT_EQ(r->status, 400) << ctx;
EXPECT_EQ(r->body, expectedConfusionOfTonguesErrorHtml(url)) << ctx;
}
{
// XML mode
const std::string url = "/ROOT/search?" + q + "&format=xml";
const auto r = zfs.GET(url.c_str());
const TestContext ctx{ {"url", url} };
EXPECT_EQ(r->status, 400) << ctx;
EXPECT_EQ(r->body, expectedConfusionOfTonguesErrorXml(url)) << ctx;
}
}
}

View File

@@ -61,7 +61,6 @@ public: // types
WITH_TASKBAR = 1 << 1,
WITH_LIBRARY_BUTTON = 1 << 2,
BLOCK_EXTERNAL_LINKS = 1 << 3,
NO_NAME_MAPPER = 1 << 4,
WITH_TASKBAR_AND_LIBRARY_BUTTON = WITH_TASKBAR | WITH_LIBRARY_BUTTON,
@@ -69,7 +68,7 @@ public: // types
};
public: // functions
ZimFileServer(int serverPort, Options options, std::string libraryFilePath);
ZimFileServer(int serverPort, std::string libraryFilePath);
ZimFileServer(int serverPort,
Options options,
const FilePathCollection& zimpaths,
@@ -92,15 +91,14 @@ private:
private: // data
kiwix::Library library;
kiwix::Manager manager;
std::unique_ptr<kiwix::NameMapper> nameMapper;
std::unique_ptr<kiwix::HumanReadableNameMapper> nameMapper;
std::unique_ptr<kiwix::Server> server;
std::unique_ptr<httplib::Client> client;
const Options options = DEFAULT_OPTIONS;
};
ZimFileServer::ZimFileServer(int serverPort, Options _options, std::string libraryFilePath)
ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
: manager(&this->library)
, options(_options)
{
if ( kiwix::isRelativePath(libraryFilePath) )
libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath);
@@ -125,11 +123,7 @@ ZimFileServer::ZimFileServer(int serverPort,
void ZimFileServer::run(int serverPort, std::string indexTemplateString)
{
const std::string address = "127.0.0.1";
if (options & NO_NAME_MAPPER) {
nameMapper.reset(new kiwix::IdNameMapper());
} else {
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
}
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
server.reset(new kiwix::Server(&library, nameMapper.get()));
server->setRoot("ROOT");
server->setAddress(address);