mirror of
https://github.com/kiwix/libkiwix.git
synced 2025-12-25 07:28:00 -05:00
Compare commits
135 Commits
widgetEndp
...
autocomple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23a0a57d96 | ||
|
|
e65c9c41d8 | ||
|
|
0ae31bd181 | ||
|
|
0d8971ef88 | ||
|
|
2812b5ca5c | ||
|
|
4dc8973cdc | ||
|
|
160c95e317 | ||
|
|
956289d9f8 | ||
|
|
3568ccd511 | ||
|
|
d66cc6286c | ||
|
|
7743e73ede | ||
|
|
4966f4155d | ||
|
|
c727de6591 | ||
|
|
0f0ae1cfed | ||
|
|
da78aae62b | ||
|
|
abcd4ade99 | ||
|
|
7a9780eb90 | ||
|
|
51bd881211 | ||
|
|
f36f1661d5 | ||
|
|
18f4a58237 | ||
|
|
6285599b7c | ||
|
|
764f68f7d8 | ||
|
|
777c5e1f7a | ||
|
|
8031ffa447 | ||
|
|
0c8ceac117 | ||
|
|
ec31882e94 | ||
|
|
8cec014691 | ||
|
|
bf9aeffbfa | ||
|
|
7765769e6f | ||
|
|
7d69ece27d | ||
|
|
c0d027e8a4 | ||
|
|
c87add1419 | ||
|
|
a52138e5ba | ||
|
|
d1b85192c0 | ||
|
|
cb02dbd92a | ||
|
|
9409e8bd91 | ||
|
|
cd62b5dd91 | ||
|
|
414d7ae4fe | ||
|
|
9d2cc35447 | ||
|
|
7167ca1e6a | ||
|
|
8cc1c47133 | ||
|
|
e5b94fa1bb | ||
|
|
b0d719431d | ||
|
|
0e20f50443 | ||
|
|
18a18c17a9 | ||
|
|
602c20f160 | ||
|
|
415ec41099 | ||
|
|
b9f60ecfe9 | ||
|
|
12a638750e | ||
|
|
b62486c2f9 | ||
|
|
6bc7e0178d | ||
|
|
ce8b2bf9d9 | ||
|
|
9fd1423100 | ||
|
|
6b8d6232f0 | ||
|
|
c91df1cb26 | ||
|
|
b249edee60 | ||
|
|
a31ccb6588 | ||
|
|
43c8da9b04 | ||
|
|
190156e095 | ||
|
|
5471819021 | ||
|
|
7feef320d9 | ||
|
|
73191fb8f8 | ||
|
|
a844bc4000 | ||
|
|
f13ca55ef6 | ||
|
|
dc194683bb | ||
|
|
0841472004 | ||
|
|
ebb713cb85 | ||
|
|
cd6cbe3655 | ||
|
|
582c8d868a | ||
|
|
f6ae75e41d | ||
|
|
ffbda34b75 | ||
|
|
f61fc07121 | ||
|
|
de7fa771fc | ||
|
|
24c1ca5a4a | ||
|
|
15f5abad3c | ||
|
|
0a866fa914 | ||
|
|
ff192cba49 | ||
|
|
0dd638f261 | ||
|
|
229c0ceaf9 | ||
|
|
70f7be4202 | ||
|
|
60148717e1 | ||
|
|
266e29dff2 | ||
|
|
11051b4eed | ||
|
|
86eacea74e | ||
|
|
3a75facfdc | ||
|
|
0a0f52f1e2 | ||
|
|
0994a8f1b0 | ||
|
|
fa67b45f50 | ||
|
|
defa38719d | ||
|
|
cac2d212c6 | ||
|
|
4e06bb6a08 | ||
|
|
796e729f52 | ||
|
|
ae01790375 | ||
|
|
da23e4eca4 | ||
|
|
2be9ac342f | ||
|
|
369406fb5d | ||
|
|
b81cb3a8e9 | ||
|
|
6cc677b8ad | ||
|
|
a674561110 | ||
|
|
685e7f8ad4 | ||
|
|
0ce36e6246 | ||
|
|
eb0a45b13e | ||
|
|
c988511561 | ||
|
|
c73e6f9a81 | ||
|
|
0cf4850a9b | ||
|
|
40c496d401 | ||
|
|
9a193735fb | ||
|
|
2083c390b5 | ||
|
|
29efb88d48 | ||
|
|
948435794f | ||
|
|
7ed01e7678 | ||
|
|
eadc0ac72b | ||
|
|
77d9777208 | ||
|
|
4a55b136f6 | ||
|
|
a9446714ea | ||
|
|
17ff2a094d | ||
|
|
0c4d9e8730 | ||
|
|
7be7a8ed5f | ||
|
|
f41e71b2d7 | ||
|
|
58e45711ff | ||
|
|
5b545d81bd | ||
|
|
7c6c315ead | ||
|
|
228e31cddd | ||
|
|
4105be9bd2 | ||
|
|
e5f97d95b1 | ||
|
|
4db443eca6 | ||
|
|
dea674ef38 | ||
|
|
4b6c6452c0 | ||
|
|
5130bf9774 | ||
|
|
ee3514d2d6 | ||
|
|
e1847cb058 | ||
|
|
dd2b82a6be | ||
|
|
1062bd73a3 | ||
|
|
cd56277123 | ||
|
|
5e8b977bec |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -12,14 +12,14 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
- name: Setup python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: '3.9'
|
||||
- name: Install packages
|
||||
run: |
|
||||
brew update
|
||||
brew install gcovr pkg-config ninja
|
||||
brew install gcovr pkg-config ninja || brew link --overwrite python
|
||||
- name: Install python modules
|
||||
run: pip3 install meson==0.49.2 pytest
|
||||
- name: Install deps
|
||||
|
||||
18
.github/workflows/package.yml
vendored
18
.github/workflows/package.yml
vendored
@@ -8,8 +8,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
distro:
|
||||
- ubuntu-kinetic
|
||||
- ubuntu-jammy
|
||||
- ubuntu-impish
|
||||
- ubuntu-focal
|
||||
- ubuntu-bionic
|
||||
steps:
|
||||
@@ -34,18 +34,18 @@ jobs:
|
||||
email: release+launchpad@kiwix.org
|
||||
distro: ${{ matrix.distro }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
|
||||
if: matrix.distro == 'ubuntu-jammy'
|
||||
name: Build package for ubuntu-jammy
|
||||
id: build-ubuntu-jammy
|
||||
- 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-impish
|
||||
if: matrix.distro == 'ubuntu-impish'
|
||||
name: Build package for ubuntu-impish
|
||||
id: build-ubuntu-impish
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
|
||||
if: matrix.distro == 'ubuntu-jammy'
|
||||
name: Build package for ubuntu-jammy
|
||||
id: build-ubuntu-jammy
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
42
ChangeLog
42
ChangeLog
@@ -1,3 +1,43 @@
|
||||
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
|
||||
===============
|
||||
|
||||
@@ -5,7 +45,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 (@juuz #772)
|
||||
* [server|front] Use integer to query the host for page length (@juuz0 #772)
|
||||
* [server] Improve multizim search API:
|
||||
- Improvement of the cache system
|
||||
- Better API to select on which books to search in.
|
||||
|
||||
73
README.md
73
README.md
@@ -101,6 +101,33 @@ meson . build -Dwrapper=android -Dwerror=false
|
||||
ninja -C build
|
||||
```
|
||||
|
||||
Static files compilation
|
||||
------------------------
|
||||
|
||||
Libkiwix has a few static files 'compiled' within the binary
|
||||
code. This is mostly Javascript/HTML/pictures necessary for the HTTP
|
||||
daemon.
|
||||
|
||||
These static files are available in the `static` directory and are
|
||||
compiled by custom Python code available in this repository `scripts`
|
||||
directory. This happens automatically at compilation time without any
|
||||
additional command to run.
|
||||
|
||||
To avoid HTTP caching issues, the URLs (to the static content) are
|
||||
appended with a `cacheid` parameter (this is called "cache
|
||||
busting"). This `cacheid` value derived from the
|
||||
[sha1sum](https://en.wikipedia.org/wiki/Sha1sum) of each targeted
|
||||
static file. As a consequence, each time you change a static file, the
|
||||
corresponding `cacheid` value will change.
|
||||
|
||||
To properly test this feature, this `cacheid` needs to be added
|
||||
manually to the automated tests and has to be commited. After
|
||||
modifying the needed static file, [run the automated
|
||||
tests](#Testing). They will fail, but the inspection of the testing
|
||||
log will give you the new `cacheid` value(s). Finally update
|
||||
`test/server.cpp` with the appropriate `cacheid` value(s) which have
|
||||
changed.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
@@ -124,7 +151,7 @@ where you want to install the libraries. After the installation
|
||||
succeeded, you may need to run `ldconfig` (as `root`).
|
||||
|
||||
Uninstallation
|
||||
------------
|
||||
--------------
|
||||
|
||||
If you want to uninstall the Kiwix library:
|
||||
```bash
|
||||
@@ -134,28 +161,6 @@ ninja -C build uninstall
|
||||
Like for the installation, you might need to run the command as `root`
|
||||
(or using `sudo`).
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If you need to install Meson "manually":
|
||||
```bash
|
||||
virtualenv -p python3 ./ # Create virtualenv
|
||||
source bin/activate # Activate the virtualenv
|
||||
pip3 install meson # Install Meson
|
||||
hash -r # Refresh bash paths
|
||||
```
|
||||
|
||||
If you need to install Ninja "manually":
|
||||
```bash
|
||||
git clone git://github.com/ninja-build/ninja.git
|
||||
cd ninja
|
||||
git checkout release
|
||||
./configure.py --bootstrap
|
||||
mkdir ../bin
|
||||
cp ninja ../bin
|
||||
cd ..
|
||||
```
|
||||
|
||||
Custom Index Page
|
||||
-----------------
|
||||
|
||||
@@ -205,6 +210,28 @@ distribution. Try then with a source tarball distributed by the
|
||||
problematic upstream project or even directly from the source code
|
||||
repository.
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
If you need to install Meson "manually":
|
||||
```bash
|
||||
virtualenv -p python3 ./ # Create virtualenv
|
||||
source bin/activate # Activate the virtualenv
|
||||
pip3 install meson # Install Meson
|
||||
hash -r # Refresh bash paths
|
||||
```
|
||||
|
||||
If you need to install Ninja "manually":
|
||||
```bash
|
||||
git clone git://github.com/ninja-build/ninja.git
|
||||
cd ninja
|
||||
git checkout release
|
||||
./configure.py --bootstrap
|
||||
mkdir ../bin
|
||||
cp ninja ../bin
|
||||
cd ..
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/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
|
||||
@@ -106,7 +106,15 @@ 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);
|
||||
@@ -332,8 +340,8 @@ class Library
|
||||
/**
|
||||
* Return the current revision of the library.
|
||||
*
|
||||
* The revision of the library is updated (incremented by one) only by
|
||||
* the addBook() operation.
|
||||
* The revision of the library is updated (incremented by one) by
|
||||
* the addBook() and removeBookById() operations.
|
||||
*
|
||||
* @return Current revision of the library.
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include "library.h"
|
||||
#include "name_mapper.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -41,7 +42,7 @@ class OPDSDumper
|
||||
{
|
||||
public:
|
||||
OPDSDumper() = default;
|
||||
OPDSDumper(Library* library);
|
||||
OPDSDumper(Library* library, NameMapper* NameMapper);
|
||||
~OPDSDumper();
|
||||
|
||||
/**
|
||||
@@ -110,6 +111,7 @@ class OPDSDumper
|
||||
|
||||
protected:
|
||||
kiwix::Library* library;
|
||||
kiwix::NameMapper* nameMapper;
|
||||
std::string libraryId;
|
||||
std::string rootLocation;
|
||||
int m_totalResults;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('libkiwix', 'cpp',
|
||||
version : '11.0.0',
|
||||
version : '12.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 : '>=7.2.0', static:static_deps)
|
||||
libzim_dep = dependency('libzim', version : '>=8.1.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
|
||||
|
||||
14
scripts/format_code.sh
Executable file
14
scripts/format_code.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/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
|
||||
@@ -52,15 +52,21 @@ 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):
|
||||
filename = filename.strip()
|
||||
def __init__(self, base_dirs, filename, cacheid=None):
|
||||
filename = filename
|
||||
self.filename = filename
|
||||
self.identifier = full_identifier(filename)
|
||||
self.cacheid = cacheid
|
||||
found = False
|
||||
for base_dir in base_dirs:
|
||||
try:
|
||||
@@ -71,7 +77,7 @@ class Resource:
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if not found:
|
||||
raise Exception("Impossible to found {}".format(filename))
|
||||
raise Exception("Resource not found: {}".format(filename))
|
||||
|
||||
def dump_impl(self):
|
||||
nb_row = len(self.data)//16 + (1 if len(self.data) % 16 else 0)
|
||||
@@ -93,6 +99,12 @@ 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]),
|
||||
@@ -123,7 +135,12 @@ 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.");
|
||||
throw ResourceNotFound("Resource not found: " + name);
|
||||
}}
|
||||
|
||||
const char* getResourceCacheId_{basename}(const std::string& name) {{
|
||||
{RESOURCE_CACHEID_GETTER}
|
||||
return nullptr;
|
||||
}}
|
||||
|
||||
{RESOURCES}
|
||||
@@ -134,6 +151,7 @@ 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)
|
||||
)
|
||||
@@ -159,8 +177,10 @@ 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}
|
||||
|
||||
@@ -189,8 +209,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, filename)
|
||||
for filename in f.readlines()]
|
||||
resources = [Resource([base_dir]+source_dir, *line.strip().split())
|
||||
for line in f.readlines()]
|
||||
|
||||
h_identifier = to_identifier(os.path.basename(args.hfile))
|
||||
with open(args.hfile, 'w') as f:
|
||||
|
||||
@@ -99,16 +99,24 @@ def preprocess_resource(resource_path):
|
||||
print(preprocessed_content, end='', file=target)
|
||||
|
||||
|
||||
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 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 preprocess_resources(resource_file_path):
|
||||
resource_filename = os.path.basename(resource_file_path)
|
||||
for resource in read_resource_file(resource_file_path):
|
||||
preprocess_resource(resource)
|
||||
copy_file(resource_file_path, os.path.join(OUT_DIR, resource_filename))
|
||||
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))
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
@@ -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 = getArchiveId(archive);
|
||||
m_id = std::string(archive.getUuid());
|
||||
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 = getArchiveArticleCount(archive);
|
||||
m_mediaCount = getArchiveMediaCount(archive);
|
||||
m_articleCount = archive.getArticleCount();
|
||||
m_mediaCount = archive.getMediaCount();
|
||||
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
|
||||
|
||||
m_illustrations.clear();
|
||||
|
||||
@@ -221,7 +221,11 @@ 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).
|
||||
return mp_impl->m_books.erase(id) == 1;
|
||||
const bool bookWasRemoved = mp_impl->m_books.erase(id) == 1;
|
||||
if ( bookWasRemoved ) {
|
||||
++mp_impl->m_revision;
|
||||
}
|
||||
return bookWasRemoved;
|
||||
}
|
||||
|
||||
Library::Revision Library::getRevision() const
|
||||
@@ -531,9 +535,20 @@ Xapian::Query categoryQuery(const std::string& category)
|
||||
return Xapian::Query("XC" + normalizeText(category));
|
||||
}
|
||||
|
||||
Xapian::Query langQuery(const std::string& lang)
|
||||
Xapian::Query langQuery(const std::string& commaSeparatedLanguageList)
|
||||
{
|
||||
return Xapian::Query("L" + normalizeText(lang));
|
||||
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);
|
||||
}
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
Xapian::Query publisherQuery(const std::string& publisher)
|
||||
|
||||
@@ -45,7 +45,7 @@ config_h = configure_file(output : 'kiwix_config.h',
|
||||
input : 'config.h.in')
|
||||
install_headers(config_h, subdir:'kiwix')
|
||||
|
||||
kiwixlib = library('kiwix',
|
||||
libkiwix = library('kiwix',
|
||||
kiwix_sources,
|
||||
include_directories : inc,
|
||||
dependencies : all_deps,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "opds_dumper.h"
|
||||
#include "book.h"
|
||||
|
||||
#include "kiwixlib-resources.h"
|
||||
#include "libkiwix-resources.h"
|
||||
#include <mustache.hpp>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
@@ -30,8 +30,9 @@ namespace kiwix
|
||||
{
|
||||
|
||||
/* Constructor */
|
||||
OPDSDumper::OPDSDumper(Library* library)
|
||||
: library(library)
|
||||
OPDSDumper::OPDSDumper(Library* library, NameMapper* nameMapper)
|
||||
: library(library),
|
||||
nameMapper(nameMapper)
|
||||
{
|
||||
}
|
||||
/* Destructor */
|
||||
@@ -49,6 +50,8 @@ 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;
|
||||
@@ -69,16 +72,17 @@ IllustrationInfo getBookIllustrationInfo(const Book& book)
|
||||
return illustrations;
|
||||
}
|
||||
|
||||
kainjow::mustache::object getSingleBookData(const Book& book)
|
||||
std::string fullEntryXML(const Book& book, const std::string& rootLocation, const std::string& contentId)
|
||||
{
|
||||
const auto bookDate = book.getDate() + "T00:00:00Z";
|
||||
return kainjow::mustache::object{
|
||||
const kainjow::mustache::object data{
|
||||
{"root", rootLocation},
|
||||
{"id", book.getId()},
|
||||
{"name", book.getName()},
|
||||
{"title", book.getTitle()},
|
||||
{"description", book.getDescription()},
|
||||
{"language", book.getLanguage()},
|
||||
{"content_id", urlEncode(book.getHumanReadableIdFromPath(), true)},
|
||||
{"content_id", urlEncode(contentId, true)},
|
||||
{"updated", bookDate}, // XXX: this should be the entry update datetime
|
||||
{"book_date", bookDate},
|
||||
{"category", book.getCategory()},
|
||||
@@ -92,27 +96,34 @@ kainjow::mustache::object getSingleBookData(const Book& book)
|
||||
{"size", to_string(book.getSize())},
|
||||
{"icons", getBookIllustrationInfo(book)},
|
||||
};
|
||||
return render_template(RESOURCE::templates::catalog_v2_entry_xml, data);
|
||||
}
|
||||
|
||||
std::string getSingleBookEntryXML(const Book& book, bool withXMLHeader, const std::string& rootLocation, const std::string& endpointRoot, bool partial)
|
||||
std::string partialEntryXML(const Book& book, const std::string& rootLocation)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
BooksData getBooksData(const Library* library, const std::vector<std::string>& bookIds, const std::string& rootLocation, const std::string& endpointRoot, bool partial)
|
||||
BooksData getBooksData(const Library* library, const NameMapper* nameMapper, const std::vector<std::string>& bookIds, const std::string& rootLocation, bool partial)
|
||||
{
|
||||
BooksData booksData;
|
||||
for ( const auto& bookId : bookIds ) {
|
||||
try {
|
||||
const Book book = library->getBookByIdThreadSafe(bookId);
|
||||
booksData.push_back(kainjow::mustache::object{
|
||||
{"entry", getSingleBookEntryXML(book, false, rootLocation, endpointRoot, partial)}
|
||||
});
|
||||
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} });
|
||||
} catch ( const std::out_of_range& ) {
|
||||
// the book was removed from the library since its id was obtained
|
||||
// ignore it
|
||||
@@ -179,7 +190,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, bookIds, rootLocation, "", false);
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, false);
|
||||
const kainjow::mustache::object template_data{
|
||||
{"date", gen_date_str()},
|
||||
{"root", rootLocation},
|
||||
@@ -197,7 +208,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, bookIds, rootLocation, endpointRoot, partial);
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, partial);
|
||||
|
||||
const char* const endpoint = partial ? "/partial_entries" : "/entries";
|
||||
const kainjow::mustache::object template_data{
|
||||
@@ -218,7 +229,11 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const
|
||||
|
||||
std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
|
||||
{
|
||||
return getSingleBookEntryXML(library->getBookById(bookId), true, rootLocation, "", false);
|
||||
const auto book = library->getBookById(bookId);
|
||||
const std::string contentId = nameMapper->getNameForId(bookId);
|
||||
return XML_HEADER
|
||||
+ "\n"
|
||||
+ fullEntryXML(book, rootLocation, contentId);
|
||||
}
|
||||
|
||||
std::string OPDSDumper::categoriesOPDSFeed() const
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include <zim/search.h>
|
||||
|
||||
#include <mustache.hpp>
|
||||
#include "kiwixlib-resources.h"
|
||||
#include "libkiwix-resources.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
namespace kiwix
|
||||
@@ -166,7 +166,7 @@ kainjow::mustache::data buildPagination(
|
||||
|
||||
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
{
|
||||
const std::string absPathPrefix = protocolPrefix + "content/";
|
||||
const std::string absPathPrefix = protocolPrefix;
|
||||
// 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("protocolPrefix", protocolPrefix);
|
||||
allData.set("searchProtocolPrefix", searchProtocolPrefix);
|
||||
allData.set("results", results);
|
||||
allData.set("pagination", pagination);
|
||||
allData.set("query", query);
|
||||
|
||||
@@ -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[] = "cz";
|
||||
const char all_options[] = "Zz";
|
||||
|
||||
static_assert(ETag::OPTION_COUNT == sizeof(all_options) - 1, "");
|
||||
|
||||
bool isValidServerId(const std::string& s)
|
||||
bool isValidETagBody(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_serverId.empty() )
|
||||
if ( m_body.empty() )
|
||||
return std::string();
|
||||
|
||||
return "\"" + m_serverId + "/" + m_options + "\"";
|
||||
return "\"" + m_body + "/" + m_options + "\"";
|
||||
}
|
||||
|
||||
ETag::ETag(const std::string& serverId, const std::string& options)
|
||||
ETag::ETag(const std::string& body, const std::string& options)
|
||||
{
|
||||
if ( isValidServerId(serverId) && isValidOptionsString(options) )
|
||||
if ( isValidETagBody(body) && isValidOptionsString(options) )
|
||||
{
|
||||
m_serverId = serverId;
|
||||
m_body = body;
|
||||
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& server_id)
|
||||
ETag ETag::match(const std::string& etags, const std::string& body)
|
||||
{
|
||||
std::istringstream ss(etags);
|
||||
std::string etag_str;
|
||||
@@ -125,7 +125,7 @@ ETag ETag::match(const std::string& etags, const std::string& server_id)
|
||||
etag_str.pop_back();
|
||||
|
||||
const ETag etag = parse(etag_str);
|
||||
if ( etag && etag.m_serverId == server_id )
|
||||
if ( etag && etag.m_body == body )
|
||||
return etag;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,11 @@ namespace kiwix {
|
||||
// The ETag string used by Kiwix server (more precisely, its value inside the
|
||||
// double quotes) consists of two parts:
|
||||
//
|
||||
// 1. ServerId - The string obtained on server start up
|
||||
// 1. Body - A string uniquely identifying the object or state from which
|
||||
// the resource has been obtained.
|
||||
//
|
||||
// 2. Options - Zero or more characters encoding the values of some of the
|
||||
// headers of the response
|
||||
// 2. Options - Zero or more characters encoding the type of the ETag and/or
|
||||
// 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
|
||||
@@ -40,7 +41,7 @@ namespace kiwix {
|
||||
//
|
||||
// "abcdefghijklmn/"
|
||||
// "1234567890/z"
|
||||
// "1234567890/cz"
|
||||
// "6f1d19d0-633f-087b-fb55-7ac324ff9baf/Zz"
|
||||
//
|
||||
// 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
|
||||
@@ -51,7 +52,7 @@ class ETag
|
||||
{
|
||||
public: // types
|
||||
enum Option {
|
||||
CACHEABLE_ENTITY,
|
||||
ZIM_CONTENT,
|
||||
COMPRESSED_CONTENT,
|
||||
OPTION_COUNT
|
||||
};
|
||||
@@ -59,10 +60,10 @@ class ETag
|
||||
public: // functions
|
||||
ETag() {}
|
||||
|
||||
void set_server_id(const std::string& id) { m_serverId = id; }
|
||||
void set_body(const std::string& s) { m_body = s; }
|
||||
void set_option(Option opt);
|
||||
|
||||
explicit operator bool() const { return !m_serverId.empty(); }
|
||||
explicit operator bool() const { return !m_body.empty(); }
|
||||
|
||||
bool get_option(Option opt) const;
|
||||
std::string get_etag() const;
|
||||
@@ -76,7 +77,7 @@ class ETag
|
||||
static ETag parse(std::string s);
|
||||
|
||||
private: // data
|
||||
std::string m_serverId;
|
||||
std::string m_body;
|
||||
std::string m_options;
|
||||
};
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ extern "C" {
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include "kiwixlib-resources.h"
|
||||
#include "libkiwix-resources.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
# include <arpa/inet.h>
|
||||
@@ -77,7 +77,6 @@ extern "C" {
|
||||
#include "request_context.h"
|
||||
#include "response.h"
|
||||
|
||||
#define MAX_SEARCH_LEN 140
|
||||
#define DEFAULT_CACHE_SIZE 2
|
||||
|
||||
namespace kiwix {
|
||||
@@ -139,15 +138,6 @@ 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} });
|
||||
@@ -212,12 +202,40 @@ 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
|
||||
@@ -289,6 +307,10 @@ 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;
|
||||
|
||||
@@ -443,7 +465,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -511,8 +532,9 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
}
|
||||
}
|
||||
|
||||
if (response->getReturnCode() == MHD_HTTP_OK && !etag_not_needed(request))
|
||||
response->set_server_id(m_server_id);
|
||||
if ( responseMustBeETaggedWithLibraryId(*response, request) ) {
|
||||
response->set_etag_body(getLibraryId());
|
||||
}
|
||||
|
||||
auto ret = response->send(request, connection);
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
@@ -534,6 +556,11 @@ 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 {
|
||||
@@ -542,7 +569,7 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
const ETag etag = get_matching_if_none_match_etag(request);
|
||||
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
|
||||
@@ -553,9 +580,12 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
if (url == "/" )
|
||||
return build_homepage(request);
|
||||
|
||||
if (isEndpointUrl(url, "skin"))
|
||||
if (isEndpointUrl(url, "viewer") || isEndpointUrl(url, "skin"))
|
||||
return handle_skin(request);
|
||||
|
||||
if (url == "/viewer_settings.js")
|
||||
return handle_viewer_settings(request);
|
||||
|
||||
if (isEndpointUrl(url, "content"))
|
||||
return handle_content(request);
|
||||
|
||||
@@ -600,36 +630,30 @@ 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", true);
|
||||
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
@@ -653,8 +677,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ noSuchBookErrorMsg(bookName)
|
||||
+ TaskbarInfo(bookName);
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
const auto queryString = request.get_optional_param("term", std::string());
|
||||
@@ -668,65 +691,82 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
printf("Searching suggestions for: \"%s\"\n", queryString.c_str());
|
||||
}
|
||||
|
||||
MustacheData results{MustacheData::type::list};
|
||||
|
||||
bool first = true;
|
||||
Suggestions results;
|
||||
|
||||
/* Get the suggestions */
|
||||
auto searcher = suggestionSearcherCache.getOrPut(bookId,
|
||||
[=](){ return make_shared<zim::SuggestionSearcher>(*archive); }
|
||||
[=](){ return make_shared<LockableSuggestionSearcher>(*archive); }
|
||||
);
|
||||
const auto lock(searcher->getLock());
|
||||
auto search = searcher->suggest(queryString);
|
||||
auto srs = search.getResults(start, count);
|
||||
|
||||
for(auto& suggestion: srs) {
|
||||
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);
|
||||
results.add(suggestion);
|
||||
}
|
||||
|
||||
|
||||
/* Propose the fulltext search if possible */
|
||||
if (archive->hasFulltextIndex()) {
|
||||
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);
|
||||
results.addFTSearchSuggestion(request.get_user_language(), queryString);
|
||||
}
|
||||
|
||||
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);
|
||||
return ContentResponse::build(*this, results.getJSON(), "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
printf("** running handle_viewer_settings\n");
|
||||
}
|
||||
|
||||
const kainjow::mustache::object data{
|
||||
{"enable_toolbar", m_withTaskbar ? "true" : "false" },
|
||||
{"enable_link_blocking", m_blockExternalLinks ? "true" : "false" },
|
||||
{"enable_library_button", m_withLibraryButton ? "true" : "false" }
|
||||
};
|
||||
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
|
||||
}
|
||||
|
||||
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()) {
|
||||
printf("** running handle_skin\n");
|
||||
}
|
||||
|
||||
auto resourceName = request.get_url().substr(1);
|
||||
const bool isRequestForViewer = request.get_url() == "/viewer";
|
||||
auto resourceName = isRequestForViewer
|
||||
? "viewer.html"
|
||||
: request.get_url().substr(1);
|
||||
|
||||
const char* const resourceCacheId = getResourceCacheId(resourceName);
|
||||
|
||||
try {
|
||||
const auto accessType = staticResourceAccessType(request, resourceCacheId);
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
getResource(resourceName),
|
||||
getMimeTypeForFile(resourceName));
|
||||
response->set_cacheable();
|
||||
response->set_kind(accessType);
|
||||
return std::move(response);
|
||||
} catch (const ResourceNotFound& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
@@ -753,75 +793,7 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
}
|
||||
|
||||
try {
|
||||
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())));
|
||||
}
|
||||
);
|
||||
} 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");
|
||||
if(bookIds.size() == 1) {
|
||||
auto bookId = *bookIds.begin();
|
||||
auto bookName = mp_nameMapper->getNameForId(bookId);
|
||||
response += TaskbarInfo(bookName, mp_library->getArchiveById(bookId).get());
|
||||
}
|
||||
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",
|
||||
/*isHomePage =*/false,
|
||||
/*raw =*/true);
|
||||
}
|
||||
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
|
||||
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);
|
||||
return handle_search_request(request);
|
||||
} catch (const Error& e) {
|
||||
return HTTP400Response(*this, request)
|
||||
+ invalidUrlMsg
|
||||
@@ -829,6 +801,87 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
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())));
|
||||
}
|
||||
);
|
||||
} 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());
|
||||
}
|
||||
*/
|
||||
return response;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
@@ -852,8 +905,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ noSuchBookErrorMsg(bookName)
|
||||
+ TaskbarInfo(bookName);
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -861,8 +913,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ nonParameterizedMessage("random-article-failure")
|
||||
+ TaskbarInfo(bookName, archive.get());
|
||||
+ nonParameterizedMessage("random-article-failure");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,9 +979,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper(mp_library);
|
||||
kiwix::OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
if (url == "root.xml") {
|
||||
uuid = zim::Uuid::generate(host);
|
||||
@@ -952,9 +1003,6 @@ 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);
|
||||
@@ -1010,10 +1058,14 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true);
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
|
||||
+ TaskbarInfo(bookName);
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1021,15 +1073,18 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
|
||||
try {
|
||||
auto entry = getEntryFromPath(*archive, urlStr);
|
||||
if (entry.isRedirect() || urlStr.empty()) {
|
||||
// If urlStr is empty, we want to mainPage.
|
||||
// We must do a redirection to the real page.
|
||||
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.
|
||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||
}
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
try {
|
||||
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, archive.get());
|
||||
} catch (std::bad_cast& e) {}
|
||||
response->set_etag_body(archiveUuid);
|
||||
|
||||
if (m_verbose.load()) {
|
||||
printf("Found %s\n", entry.getPath().c_str());
|
||||
@@ -1044,8 +1099,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true);
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
|
||||
+ TaskbarInfo(bookName, archive.get());
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1084,6 +1138,11 @@ 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
|
||||
// ^^^^^ ^ ^
|
||||
@@ -1093,13 +1152,17 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
try {
|
||||
if (kind == "meta") {
|
||||
auto item = archive->getMetadataItem(itemPath);
|
||||
return ItemResponse::build(*this, request, item, /*raw=*/true);
|
||||
auto response = ItemResponse::build(*this, request, item);
|
||||
response->set_etag_body(archiveUuid);
|
||||
return response;
|
||||
} else {
|
||||
auto entry = archive->getEntryByPath(itemPath);
|
||||
if (entry.isRedirect()) {
|
||||
return build_redirect(bookName, entry.getItem(true));
|
||||
}
|
||||
return ItemResponse::build(*this, request, entry.getItem(), /*raw=*/true);
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
response->set_etag_body(archiveUuid);
|
||||
return response;
|
||||
}
|
||||
} catch (zim::EntryNotFound& e ) {
|
||||
if (m_verbose.load()) {
|
||||
@@ -1136,9 +1199,7 @@ std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(con
|
||||
|
||||
return ContentResponse::build(*this,
|
||||
resourceData,
|
||||
crd.mimeType,
|
||||
/*isHomePage=*/false,
|
||||
/*raw=*/true);
|
||||
crd.mimeType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,9 +88,6 @@ 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 {
|
||||
@@ -126,6 +123,7 @@ class InternalServer {
|
||||
std::unique_ptr<Response> handle_request(const RequestContext& request);
|
||||
std::unique_ptr<Response> build_redirect(const std::string& bookName, const zim::Item& item) const;
|
||||
std::unique_ptr<Response> build_homepage(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_viewer_settings(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_skin(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_catalog_v2(const RequestContext& request);
|
||||
@@ -136,6 +134,7 @@ 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);
|
||||
@@ -149,13 +148,18 @@ 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;
|
||||
@@ -177,14 +181,13 @@ class InternalServer {
|
||||
SuggestionSearcherCache suggestionSearcherCache;
|
||||
|
||||
std::string m_server_id;
|
||||
std::string m_library_id;
|
||||
|
||||
class CustomizedResources;
|
||||
std::unique_ptr<CustomizedResources> m_customizedResources;
|
||||
|
||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage, bool raw);
|
||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw);
|
||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype);
|
||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "request_context.h"
|
||||
#include "response.h"
|
||||
#include "tools/otherTools.h"
|
||||
#include "kiwixlib-resources.h"
|
||||
#include "libkiwix-resources.h"
|
||||
|
||||
#include <mustache.hpp>
|
||||
|
||||
@@ -77,17 +77,18 @@ 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(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")}
|
||||
{"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")}
|
||||
},
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
);
|
||||
@@ -95,9 +96,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);
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
const auto bookIds = search_catalog(request, opdsDumper);
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
|
||||
return ContentResponse::build(
|
||||
@@ -116,9 +117,9 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
+ urlNotFoundMsg;
|
||||
}
|
||||
|
||||
OPDSDumper opdsDumper(mp_library);
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
@@ -129,9 +130,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);
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.categoriesOPDSFeed(),
|
||||
@@ -141,9 +142,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);
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(m_library_id);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.languagesOPDSFeed(),
|
||||
@@ -158,7 +159,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
|
||||
auto book = mp_library->getBookByIdThreadSafe(bookId);
|
||||
auto size = request.get_argument<unsigned int>("size");
|
||||
auto illustration = book.getIllustration(size);
|
||||
return ContentResponse::build(*this, illustration->getData(), illustration->mimeType);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
illustration->getData(),
|
||||
illustration->mimeType
|
||||
);
|
||||
} catch(...) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
|
||||
@@ -107,6 +107,14 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,9 +92,7 @@ class RequestContext {
|
||||
std::string get_url_part(int part) const;
|
||||
std::string get_full_url() const;
|
||||
|
||||
std::string get_query(bool mustEncode = false) const {
|
||||
return get_query([](const std::string& key) {return true;}, mustEncode);
|
||||
}
|
||||
std::string get_query() const { return queryString; }
|
||||
|
||||
template<class F>
|
||||
std::string get_query(F filter, bool mustEncode) const {
|
||||
@@ -132,6 +130,7 @@ 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*);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "response.h"
|
||||
#include "request_context.h"
|
||||
#include "internalServer.h"
|
||||
#include "kiwixlib-resources.h"
|
||||
#include "libkiwix-resources.h"
|
||||
|
||||
#include "tools/regexTools.h"
|
||||
#include "tools/stringTools.h"
|
||||
@@ -102,6 +102,14 @@ 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
|
||||
|
||||
@@ -112,6 +120,13 @@ 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()));
|
||||
@@ -122,6 +137,9 @@ 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");
|
||||
}
|
||||
@@ -140,9 +158,6 @@ std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObjec
|
||||
{
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
if ( m_taskbarInfo ) {
|
||||
r->set_taskbar(m_taskbarInfo->bookName, m_taskbarInfo->archive);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -236,29 +251,12 @@ HTTP500Response::HTTP500Response(const InternalServer& server,
|
||||
|
||||
std::unique_ptr<ContentResponse> HTTP500Response::generateResponseObject() const
|
||||
{
|
||||
// We want a 500 response to be a minimalistic one (so that the server doesn't
|
||||
// have to provide additional resources required for its proper rendering)
|
||||
// ";raw=true" in the MIME-type below disables response decoration
|
||||
// (see ContentResponse::contentDecorationAllowed())
|
||||
const std::string mimeType = "text/html;charset=utf-8;raw=true";
|
||||
const std::string mimeType = "text/html;charset=utf-8";
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
return r;
|
||||
}
|
||||
|
||||
ContentResponseBlueprint& ContentResponseBlueprint::operator+(const TaskbarInfo& taskbarInfo)
|
||||
{
|
||||
this->m_taskbarInfo.reset(new TaskbarInfo(taskbarInfo));
|
||||
return *this;
|
||||
}
|
||||
|
||||
ContentResponseBlueprint& ContentResponseBlueprint::operator+=(const TaskbarInfo& taskbarInfo)
|
||||
{
|
||||
// operator+() is already a state-modifying operator (akin to operator+=)
|
||||
return *this + taskbarInfo;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
@@ -337,52 +335,6 @@ void print_response_info(int retCode, MHD_Response* response)
|
||||
}
|
||||
|
||||
|
||||
void ContentResponse::introduce_taskbar(const std::string& lang)
|
||||
{
|
||||
i18n::GetTranslatedString t(lang);
|
||||
kainjow::mustache::object data{
|
||||
{"root", m_root},
|
||||
{"content", m_bookName},
|
||||
{"hascontent", (!m_bookName.empty() && !m_bookTitle.empty())},
|
||||
{"title", m_bookTitle},
|
||||
{"withlibrarybutton", m_withLibraryButton},
|
||||
{"LIBRARY_BUTTON_TEXT", t("library-button-text")},
|
||||
{"HOME_BUTTON_TEXT", t("home-button-text", {{"BOOK_TITLE", m_bookTitle}}) },
|
||||
{"RANDOM_PAGE_BUTTON_TEXT", t("random-page-button-text") },
|
||||
{"SEARCHBOX_TOOLTIP", t("searchbox-tooltip", {{"BOOK_TITLE", m_bookTitle}}) },
|
||||
};
|
||||
auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data);
|
||||
m_content = prependToFirstOccurence(
|
||||
m_content,
|
||||
"</head[ \\t]*>",
|
||||
head_content);
|
||||
|
||||
auto taskbar_part = render_template(RESOURCE::templates::taskbar_part_html, data);
|
||||
m_content = appendToFirstOccurence(
|
||||
m_content,
|
||||
"<body[^>]*>",
|
||||
taskbar_part);
|
||||
}
|
||||
|
||||
|
||||
void ContentResponse::inject_externallinks_blocker()
|
||||
{
|
||||
kainjow::mustache::data data;
|
||||
data.set("root", m_root);
|
||||
auto script_tag = render_template(RESOURCE::templates::external_blocker_part_html, data);
|
||||
m_content = prependToFirstOccurence(
|
||||
m_content,
|
||||
"</head[ \\t]*>",
|
||||
script_tag);
|
||||
}
|
||||
|
||||
void ContentResponse::inject_root_link(){
|
||||
m_content = prependToFirstOccurence(
|
||||
m_content,
|
||||
"</head[ \\t]*>",
|
||||
"<link type=\"root\" href=\"" + m_root + "\">");
|
||||
}
|
||||
|
||||
bool
|
||||
ContentResponse::can_compress(const RequestContext& request) const
|
||||
{
|
||||
@@ -391,16 +343,6 @@ ContentResponse::can_compress(const RequestContext& request) const
|
||||
&& (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_COMPRESS);
|
||||
}
|
||||
|
||||
bool
|
||||
ContentResponse::contentDecorationAllowed() const
|
||||
{
|
||||
if (m_raw) {
|
||||
return false;
|
||||
}
|
||||
return (startsWith(m_mimeType, "text/html")
|
||||
&& m_mimeType.find(";raw=true") == std::string::npos);
|
||||
}
|
||||
|
||||
MHD_Response*
|
||||
Response::create_mhd_response(const RequestContext& request)
|
||||
{
|
||||
@@ -411,17 +353,6 @@ Response::create_mhd_response(const RequestContext& request)
|
||||
MHD_Response*
|
||||
ContentResponse::create_mhd_response(const RequestContext& request)
|
||||
{
|
||||
if (contentDecorationAllowed()) {
|
||||
inject_root_link();
|
||||
|
||||
if (m_withTaskbar) {
|
||||
introduce_taskbar(request.get_user_language());
|
||||
}
|
||||
if (m_blockExternalLinks) {
|
||||
inject_externallinks_blocker();
|
||||
}
|
||||
}
|
||||
|
||||
const bool isCompressed = can_compress(request) && compress(m_content);
|
||||
|
||||
MHD_Response* response = MHD_create_response_from_buffer(
|
||||
@@ -442,7 +373,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,
|
||||
m_etag.get_option(ETag::CACHEABLE_ENTITY) ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
|
||||
getCacheControlHeader(m_kind));
|
||||
const std::string etag = m_etag.get_etag();
|
||||
if ( ! etag.empty() )
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etag.c_str());
|
||||
@@ -461,24 +392,11 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ContentResponse::set_taskbar(const std::string& bookName, const zim::Archive* archive)
|
||||
{
|
||||
m_bookName = bookName;
|
||||
m_bookTitle = archive ? getArchiveTitle(*archive) : "";
|
||||
}
|
||||
|
||||
|
||||
ContentResponse::ContentResponse(const std::string& root, bool verbose, bool raw, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype) :
|
||||
ContentResponse::ContentResponse(const std::string& root, bool verbose, const std::string& content, const std::string& mimetype) :
|
||||
Response(verbose),
|
||||
m_root(root),
|
||||
m_content(content),
|
||||
m_mimeType(mimetype),
|
||||
m_raw(raw),
|
||||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_blockExternalLinks(blockExternalLinks),
|
||||
m_bookName(""),
|
||||
m_bookTitle("")
|
||||
m_mimeType(mimetype)
|
||||
{
|
||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||
}
|
||||
@@ -486,17 +404,11 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, bool raw
|
||||
std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||
const InternalServer& server,
|
||||
const std::string& content,
|
||||
const std::string& mimetype,
|
||||
bool isHomePage,
|
||||
bool raw)
|
||||
const std::string& mimetype)
|
||||
{
|
||||
return std::unique_ptr<ContentResponse>(new ContentResponse(
|
||||
server.m_root,
|
||||
server.m_verbose.load(),
|
||||
raw,
|
||||
server.m_withTaskbar && !isHomePage,
|
||||
server.m_withLibraryButton,
|
||||
server.m_blockExternalLinks,
|
||||
content,
|
||||
mimetype));
|
||||
}
|
||||
@@ -505,11 +417,10 @@ std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||
const InternalServer& server,
|
||||
const std::string& template_str,
|
||||
kainjow::mustache::data data,
|
||||
const std::string& mimetype,
|
||||
bool isHomePage)
|
||||
const std::string& mimetype)
|
||||
{
|
||||
auto content = render_template(template_str, data);
|
||||
return ContentResponse::build(server, content, mimetype, isHomePage);
|
||||
return ContentResponse::build(server, content, mimetype);
|
||||
}
|
||||
|
||||
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
||||
@@ -518,26 +429,26 @@ ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::strin
|
||||
m_mimeType(mimetype)
|
||||
{
|
||||
m_byteRange = byterange;
|
||||
set_cacheable();
|
||||
set_kind(Response::ZIM_CONTENT);
|
||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw)
|
||||
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item)
|
||||
{
|
||||
const std::string mimetype = get_mime_type(item);
|
||||
auto byteRange = request.get_range().resolve(item.getSize());
|
||||
const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
|
||||
if (noRange && is_compressible_mime_type(mimetype)) {
|
||||
// Return a contentResponse
|
||||
auto response = ContentResponse::build(server, item.getData(), mimetype, /*isHomePage=*/false, raw);
|
||||
response->set_cacheable();
|
||||
auto response = ContentResponse::build(server, item.getData(), mimetype);
|
||||
response->set_kind(Response::ZIM_CONTENT);
|
||||
response->m_byteRange = byteRange;
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
|
||||
auto response = Response::build_416(server, item.getSize());
|
||||
response->set_cacheable();
|
||||
response->set_kind(Response::ZIM_CONTENT);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,14 @@ class InternalServer;
|
||||
class RequestContext;
|
||||
|
||||
class Response {
|
||||
public:
|
||||
enum Kind
|
||||
{
|
||||
STATIC_RESOURCE,
|
||||
ZIM_CONTENT,
|
||||
DYNAMIC_CONTENT
|
||||
};
|
||||
|
||||
public:
|
||||
Response(bool verbose);
|
||||
virtual ~Response() = default;
|
||||
@@ -57,8 +65,9 @@ class Response {
|
||||
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
|
||||
|
||||
void set_code(int code) { m_returnCode = code; }
|
||||
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 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 add_header(const std::string& name, const std::string& value) { m_customHeaders[name] = value; }
|
||||
|
||||
int getReturnCode() const { return m_returnCode; }
|
||||
@@ -68,6 +77,7 @@ 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;
|
||||
@@ -83,60 +93,32 @@ class ContentResponse : public Response {
|
||||
ContentResponse(
|
||||
const std::string& root,
|
||||
bool verbose,
|
||||
bool raw,
|
||||
bool withTaskbar,
|
||||
bool withLibraryButton,
|
||||
bool blockExternalLinks,
|
||||
const std::string& content,
|
||||
const std::string& mimetype);
|
||||
|
||||
static std::unique_ptr<ContentResponse> build(
|
||||
const InternalServer& server,
|
||||
const std::string& content,
|
||||
const std::string& mimetype,
|
||||
bool isHomePage = false,
|
||||
bool raw = false);
|
||||
const std::string& mimetype);
|
||||
|
||||
static std::unique_ptr<ContentResponse> build(
|
||||
const InternalServer& server,
|
||||
const std::string& template_str,
|
||||
kainjow::mustache::data data,
|
||||
const std::string& mimetype,
|
||||
bool isHomePage = false);
|
||||
|
||||
void set_taskbar(const std::string& bookName, const zim::Archive* archive);
|
||||
const std::string& mimetype);
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
|
||||
void introduce_taskbar(const std::string& lang);
|
||||
void inject_externallinks_blocker();
|
||||
void inject_root_link();
|
||||
bool can_compress(const RequestContext& request) const;
|
||||
bool contentDecorationAllowed() const;
|
||||
|
||||
|
||||
private:
|
||||
std::string m_root;
|
||||
std::string m_content;
|
||||
std::string m_mimeType;
|
||||
bool m_raw;
|
||||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
bool m_blockExternalLinks;
|
||||
std::string m_bookName;
|
||||
std::string m_bookTitle;
|
||||
};
|
||||
|
||||
struct TaskbarInfo
|
||||
{
|
||||
const std::string bookName;
|
||||
const zim::Archive* const archive;
|
||||
|
||||
TaskbarInfo(const std::string& bookName, const zim::Archive* a = nullptr)
|
||||
: bookName(bookName)
|
||||
, archive(a)
|
||||
{}
|
||||
};
|
||||
|
||||
class ContentResponseBlueprint
|
||||
{
|
||||
public: // functions
|
||||
@@ -165,9 +147,6 @@ public: // functions
|
||||
}
|
||||
|
||||
|
||||
ContentResponseBlueprint& operator+(const TaskbarInfo& taskbarInfo);
|
||||
ContentResponseBlueprint& operator+=(const TaskbarInfo& taskbarInfo);
|
||||
|
||||
protected: // functions
|
||||
std::string getMessage(const std::string& msgId) const;
|
||||
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
|
||||
@@ -179,7 +158,6 @@ public: //data
|
||||
const std::string m_mimeType;
|
||||
const std::string m_template;
|
||||
kainjow::mustache::data m_data;
|
||||
std::unique_ptr<TaskbarInfo> m_taskbarInfo;
|
||||
};
|
||||
|
||||
struct HTTPErrorResponse : ContentResponseBlueprint
|
||||
@@ -191,8 +169,6 @@ struct HTTPErrorResponse : ContentResponseBlueprint
|
||||
const std::string& headingMsgId,
|
||||
const std::string& cssUrl = "");
|
||||
|
||||
using ContentResponseBlueprint::operator+;
|
||||
using ContentResponseBlueprint::operator+=;
|
||||
HTTPErrorResponse& operator+(const std::string& msg);
|
||||
HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
|
||||
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
|
||||
@@ -238,7 +214,7 @@ private: // overrides
|
||||
class ItemResponse : public Response {
|
||||
public:
|
||||
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw = false);
|
||||
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
|
||||
@@ -93,10 +93,6 @@ 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 {
|
||||
@@ -109,46 +105,6 @@ 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;
|
||||
}
|
||||
@@ -169,14 +125,4 @@ 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
|
||||
|
||||
@@ -40,7 +40,6 @@ 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);
|
||||
@@ -52,9 +51,6 @@ 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
|
||||
|
||||
@@ -32,12 +32,15 @@
|
||||
#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 {
|
||||
@@ -288,67 +291,6 @@ 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);
|
||||
@@ -387,3 +329,72 @@ 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);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace pugi {
|
||||
class xml_node;
|
||||
}
|
||||
|
||||
namespace zim {
|
||||
class SuggestionItem;
|
||||
}
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
std::string nodeToString(const pugi::xml_node& node);
|
||||
@@ -45,9 +49,6 @@ 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);
|
||||
|
||||
@@ -70,6 +71,22 @@ 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
|
||||
|
||||
@@ -75,41 +75,3 @@ std::string replaceRegex(const std::string& content,
|
||||
uresult.toUTF8String(tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
std::string appendToFirstOccurence(const std::string& content,
|
||||
const std::string& regex,
|
||||
const std::string& replacement)
|
||||
{
|
||||
ucnv_setDefaultName("UTF-8");
|
||||
icu::UnicodeString ucontent(content.c_str());
|
||||
icu::UnicodeString ureplacement(replacement.c_str());
|
||||
auto matcher = buildMatcher(regex, ucontent);
|
||||
if (matcher->find()) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
ucontent.insert(matcher->end(status), ureplacement);
|
||||
std::string tmp;
|
||||
ucontent.toUTF8String(tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
std::string prependToFirstOccurence(const std::string& content,
|
||||
const std::string& regex,
|
||||
const std::string& replacement)
|
||||
{
|
||||
ucnv_setDefaultName("UTF-8");
|
||||
icu::UnicodeString ucontent(content.c_str());
|
||||
icu::UnicodeString ureplacement(replacement.c_str());
|
||||
auto matcher = buildMatcher(regex, ucontent);
|
||||
if (matcher->find()) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
ucontent.insert(matcher->start(status), ureplacement);
|
||||
std::string tmp;
|
||||
ucontent.toUTF8String(tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,5 @@ bool matchRegex(const std::string& content, const std::string& regex);
|
||||
std::string replaceRegex(const std::string& content,
|
||||
const std::string& replacement,
|
||||
const std::string& regex);
|
||||
std::string appendToFirstOccurence(const std::string& content,
|
||||
const std::string& regex,
|
||||
const std::string& replacement);
|
||||
std::string prependToFirstOccurence(const std::string& content,
|
||||
const std::string& regex,
|
||||
const std::string& replacement);
|
||||
|
||||
#endif
|
||||
|
||||
31
static/i18n/cs.json
Normal file
31
static/i18n/cs.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Spotter"
|
||||
]
|
||||
},
|
||||
"name": "Čeština",
|
||||
"suggest-full-text-search": "obsahující '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Žádná taková kniha: {{BOOK_NAME}}",
|
||||
"too-many-books": "Bylo požadováno příliš mnoho knih ({{NB_BOOKS}}), kde je limit {{LIMIT}}",
|
||||
"no-book-found": "Výběrovým kritériím nevyhovuje žádná kniha",
|
||||
"url-not-found": "Požadovaná adresa URL \"{{url}}\" nebyla na tomto serveru nalezena.",
|
||||
"suggest-search": "Proveďte fulltextové vyhledávání <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Jejda! Nepodařilo se vybrat náhodný článek :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} není platný požadavek na nezpracovaný obsah.",
|
||||
"no-value-for-arg": "Pro argument {{ARGUMENT}} nebyla zadána žádná hodnota",
|
||||
"no-query": "Nebyl poskytnut žádný dotaz.",
|
||||
"raw-entry-not-found": "Nelze najít položku {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "Neplatný požadavek",
|
||||
"400-page-heading": "Neplatný požadavek",
|
||||
"404-page-title": "Obsah nenalezen",
|
||||
"404-page-heading": "Nenalezeno",
|
||||
"500-page-title": "Interní chyba serveru",
|
||||
"500-page-heading": "Interní chyba serveru",
|
||||
"fulltext-search-unavailable": "Fulltextové vyhledávání není k dispozici",
|
||||
"no-search-results": "Fulltextový vyhledávač není pro tento obsah dostupný.",
|
||||
"library-button-text": "Přejít na uvítací stránku",
|
||||
"home-button-text": "Přejít na hlavní stránku '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Přejít na náhodně vybranou stránku",
|
||||
"searchbox-tooltip": "Hledat '{{BOOK_TITLE}}'"
|
||||
}
|
||||
20
static/i18n/de.json
Normal file
20
static/i18n/de.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"@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"
|
||||
}
|
||||
@@ -27,4 +27,5 @@
|
||||
, "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."
|
||||
}
|
||||
|
||||
@@ -29,5 +29,6 @@
|
||||
"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}} »"
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Amire80"
|
||||
"Amire80",
|
||||
"YaronSh"
|
||||
]
|
||||
},
|
||||
"name": "עברית",
|
||||
@@ -27,5 +28,6 @@
|
||||
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
|
||||
"home-button-text": "מעבר לדף הראשי של \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "מעבר לדף שנבחר אקראית",
|
||||
"searchbox-tooltip": "חיפוש \"{{BOOK_TITLE}}\""
|
||||
"searchbox-tooltip": "חיפוש \"{{BOOK_TITLE}}\"",
|
||||
"confusion-of-tongues": "שני ספרים או יותר בשפות שונות ישתתפו בחיפוש, מה שעלול להוביל לתוצאות מבלבלות."
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": []
|
||||
"authors": [
|
||||
"Kareyac"
|
||||
]
|
||||
},
|
||||
"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": "Գրադարանի էջ",
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Albano",
|
||||
"Beta16"
|
||||
]
|
||||
},
|
||||
"name": "italiano",
|
||||
"suggest-full-text-search": "contenente '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Nessun libro del genere: {{BOOK_NAME}}",
|
||||
"too-many-books": "Troppi libri richiesti ({{NB_BOOKS}}) dove il limite è {{LIMIT}}",
|
||||
"no-book-found": "Nessun libro corrisponde ai criteri di selezione",
|
||||
"url-not-found": "L'URL richiesto \"{{url}}\" non è stato trovato in questo server.",
|
||||
"suggest-search": "Effettua una ricerca di testo completo per <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ops! Impossibile selezionare un articolo casuale :(",
|
||||
"no-value-for-arg": "Nessun valore fornito per l'argomento {{ARGUMENT}}",
|
||||
"400-page-title": "Richiesta non valida",
|
||||
"400-page-heading": "Richiesta non valida",
|
||||
"404-page-title": "Contenuto non trovato",
|
||||
"404-page-heading": "Non trovato",
|
||||
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'"
|
||||
"500-page-title": "Errore interno del server",
|
||||
"500-page-heading": "Errore interno del server",
|
||||
"library-button-text": "Vai alla pagina di benvenuto",
|
||||
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Vai a una pagina selezionata casualmente",
|
||||
"searchbox-tooltip": "Cerca '{{BOOK_TITLE}}'"
|
||||
}
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
"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"
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
"library-button-text": "Оди на воведната страница",
|
||||
"home-button-text": "Оди на главната страница на „{{BOOK_TITLE}}“",
|
||||
"random-page-button-text": "Оди на случајно избрана страница",
|
||||
"searchbox-tooltip": "Пребарај го „{{BOOK_TITLE}}“"
|
||||
"searchbox-tooltip": "Пребарај го „{{BOOK_TITLE}}“",
|
||||
"confusion-of-tongues": "Во пребарувањето ќе учествуваат две или повеќе книги на различни јазици, што може да довете до збунувачки исход."
|
||||
}
|
||||
|
||||
31
static/i18n/nqo.json
Normal file
31
static/i18n/nqo.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Lancine.kounfantoh.fofana"
|
||||
]
|
||||
},
|
||||
"name": "ߒߞߏ",
|
||||
"suggest-full-text-search": "ߞߣߐߞߍߣߍ߲߫ ߦߋ߫ '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "ߞߊ߬ߝߊ߫ ߛߎ߮ ߏ߬ ߕߴߦߋ߲߬: {{BOOK_NAME}}",
|
||||
"too-many-books": "ߞߝߊ߬ ߛߌߦߊߡߊ߲߫ ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲߫ ߞߏߖߎ߰ {{NB_BOOKS}} ߡߍ߲ ߞߐߘߊ߲ ߦߋ߫ {{LIMIT}} ߘߌ߫",
|
||||
"no-book-found": "ߞߝߊ߬ ߛߌ߫ ߓߍ߬ߣߍ߲߬ ߕߍ߫ ߛߎߥߊ߲ߘߟߌ ߛߙߊߕߌ ߏ߬ ߡߊ߬",
|
||||
"url-not-found": "ߍ߲߬ߤߍ߲߫ ''URL ''{{url}} ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲ ߡߊ߫ ߛߐ߬ߘߐ߲߫ ߡߊ߬ߟߐ߬ߟߊ߲ ߣߌ߲߬ ߠߊ߫.",
|
||||
"suggest-search": "ߛߓߍߟߌ߫ ߘߝߊߣߍ߲ ߢߌߣߌ߲ߠߌ߲ ߞߍ߫ <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "ߏ߯߹ ߞߢߊ߬ ߓߘߊ߫ ߞߍ߫ ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߎߡߘߊ ߛߐ߬ߘߐ߲߬ ߠߊ߫:(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ߕߍ߫ ߡߞߊ߬ߣߌ߲߬ߣߍ߲߫ ߓߍ߲߬ߣߍ߲߫ ߘߌ߫ ߞߣߐߘߐ߫ ߘߝߊߣߍ߲ ߞߊ߲ߡߊ߬.",
|
||||
"no-value-for-arg": "ߡߐ߬ߟߐ߲߬ ߡߊ߫ ߡߊߛߐ߫ ߘߊߘߐߡߌߣߊߞߎ߲ߧߊ {{ARGUMENT}} ߞߊ߲ߡߊ߬",
|
||||
"no-query": "ߡߞߊ߬ߣߌ߲߬ߠߌ߲߫ ߛߌ߫ ߡߊ߫ ߡߊߛߐ߫.",
|
||||
"raw-entry-not-found": "ߟߊ߬ߘߏ߲߬ߣߍ߲ {{ENTRY}} {{DATATYPE}} ߕߍ߫ ߣߊ߬ ߡߊߛߐ߬ߘߐ߲߫ ߠߊ߫",
|
||||
"400-page-title": "ߡߞߊ߬ߣߌ߲߬ߠߌ߲߫ ߓߍ߲߬ߓߊߟߌ",
|
||||
"400-page-heading": "ߡߞߊ߬ߣߌ߲߬ߠߌ߲߫ ߓߍ߲߬ߓߊߟߌ",
|
||||
"404-page-title": "ߞߣߐߘߐ ߡߊ߫ ߛߐ߬ߘߐ߲߫",
|
||||
"404-page-heading": "ߡߊ߫ ߛߐ߬ߘߐ߲߫",
|
||||
"500-page-title": "ߞߣߐߟߊ ߡߊ߬ߛߐ߬ߟߊ߲ ߝߎ߬ߕߎ߲߬ߕߌ",
|
||||
"500-page-heading": "ߞߣߐߟߊ ߡߊ߬ߛߐ߬ߟߊ߲ ߝߎ߬ߕߎ߲߬ߕߌ",
|
||||
"fulltext-search-unavailable": "ߛߓߍߟߌ߫ ߘߝߊߣߍ߲ ߢߌߣߌ߲ߠߌ߲ ߕߍ߫ ߦߋ߲߬",
|
||||
"no-search-results": "ߛߓߍߟߌ߫ ߘߝߊߣߍ߲ ߢߌߣߌ߲ߠߌ߲߫ ߣߌߘߐ ߕߍ߫ ߦߋ߲߬ ߞߣߐߘߐ ߣߌ߲߬ ߢߍ߫.",
|
||||
"library-button-text": "ߕߊ߯ ߟߊ߬ߛߣߍ߬ߟߌ߫ ߞߐߜߍ ߞߊ߲߬",
|
||||
"home-button-text": "ߕߊ߯ {{BOOK_TITLE}} ߓߏ߬ߟߏ߲߬ߘߊ ߞߐߜߍ ߞߊ߲߬",
|
||||
"random-page-button-text": "ߕߊ߯ ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ߫ ߛߎߥߊ߲ߘߌߣߍ߲ ߠߎ߬ ߞߊ߲߬",
|
||||
"searchbox-tooltip": "ߕߌߙߌ߲ߠߌ߲ {{BOOK_TITLE}}"
|
||||
}
|
||||
@@ -3,15 +3,22 @@
|
||||
"authors": [
|
||||
"Fenixs-ru",
|
||||
"Kareyac",
|
||||
"Okras",
|
||||
"Pacha Tchernof"
|
||||
]
|
||||
},
|
||||
"name": "русский",
|
||||
"suggest-full-text-search": "содержащее '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Такой книги нет: {{BOOK_NAME}}",
|
||||
"too-many-books": "Запрошено слишком много книг ({{NB_BOOKS}}), максимальное количество — {{LIMIT}}.",
|
||||
"no-book-found": "Ни одна книга не соответствует критериям отбора",
|
||||
"url-not-found": "Запрошенный URL \"{{url}}\" не найден на этом сервере.",
|
||||
"suggest-search": "Выполните полнотекстовый поиск для <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ой! Не удалось выбрать случайную статью :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} не является допустимым запросом необработанного контента.",
|
||||
"no-value-for-arg": "Не указано значение для аргумента {{ARGUMENT}}",
|
||||
"no-query": "Не предоставлен запрос.",
|
||||
"raw-entry-not-found": "Не удаётся найти запись {{ENTRY}} типа {{DATATYPE}}",
|
||||
"400-page-title": "Недействительный запрос",
|
||||
"400-page-heading": "Недействительный запрос",
|
||||
"404-page-title": "Содержание не найдено",
|
||||
@@ -23,5 +30,6 @@
|
||||
"library-button-text": "Перейти на страницу-приветствие",
|
||||
"home-button-text": "Перейти на главную страницу '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Перейти на случайно выбранную страницу",
|
||||
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'"
|
||||
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам."
|
||||
}
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
"name": "Sardu",
|
||||
"suggest-full-text-search": "chi cuntenet '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Perunu libru cun custu nùmene: {{BOOK_NAME}}",
|
||||
"too-many-books": "Tropu libros pedidos, {{NB_BOOKS}} cando su lìmite est de {{LIMIT}}",
|
||||
"no-book-found": "Perunu libru currispondet a sos critèrios de seletzione",
|
||||
"url-not-found": "S'URL pedidu \"{{url}}\" non s'est atzapadu in custu serbidore.",
|
||||
"suggest-search": "Faghe una chirca de testu intreu pro <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Oops! Sa seletzione de un'artìculu a casu est fallida :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} no est una rechesta vàlida pro cuntenutu puru.",
|
||||
"no-value-for-arg": "Perunu valore frunidu pro s'argumentu {{ARGUMENT}}",
|
||||
"no-query": "Peruna chirca frunida.",
|
||||
"raw-entry-not-found": "Non faghet a atzapare s'elementu {{ENTRY}} de genia {{DATATYPE}}",
|
||||
"400-page-title": "Rechesta non vàlida",
|
||||
"400-page-heading": "Rechesta non vàlida",
|
||||
@@ -23,5 +27,6 @@
|
||||
"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}}'"
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
"name": "slovenčina",
|
||||
"suggest-full-text-search": "obsahuje '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Žiadna kniha ako: {{BOOK_NAME}}",
|
||||
"too-many-books": "Príliš veľa požadovaných kníh ({{NB_BOOKS}}), limit je {{LIMIT}}",
|
||||
"no-book-found": "Kritériám výberu nevyhovuje žiadna kniha",
|
||||
"url-not-found": "Požadovaná adresa URL \"{{url}}\" na tomto serveri nebola nájdená.",
|
||||
"suggest-search": "Spustite hľadanie celého textu <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Nepodarilo sa vybrať náhodný článok :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} nie je platná požiadavka pre surový obsah.",
|
||||
"no-value-for-arg": "Pre argument {{ARGUMENT}} nebola poskytnutá žiadna hodnota",
|
||||
"no-query": "Nebol poskytnutý žiadny dopyt.",
|
||||
"raw-entry-not-found": "Nepodarilo sa nájsť {{DATATYPE}} položka {{ENTRY}}",
|
||||
"400-page-title": "Neplatná požiadavka",
|
||||
"400-page-heading": "Neplatná požiadavka",
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Sabelöga"
|
||||
"Jopparn",
|
||||
"Sabelöga",
|
||||
"WikiPhoenix"
|
||||
]
|
||||
},
|
||||
"name": "Svenska",
|
||||
"suggest-full-text-search": "innehåller '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Ingen sådan bok: {{BOOK_NAME}}",
|
||||
"too-many-books": "För många böcker begärda ({{NB_BOOKS}}) där gränsen är {{LIMIT}}",
|
||||
"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",
|
||||
@@ -22,5 +29,6 @@
|
||||
"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}}\""
|
||||
"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."
|
||||
}
|
||||
|
||||
20
static/i18n/test.json
Normal file
20
static/i18n/test.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"@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}}'"
|
||||
}
|
||||
@@ -7,10 +7,14 @@
|
||||
"name": "Türkçe",
|
||||
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}' içeriyor...",
|
||||
"no-such-book": "Böyle bir kitap yok: {{BOOK_NAME}}",
|
||||
"too-many-books": "Sınır {{LIMIT}} olduğunda çok fazla ({{NB_BOOKS}}) kitap istendi",
|
||||
"no-book-found": "Seçim kriterleriyle eşleşen kitap yok",
|
||||
"url-not-found": "İstenen \"{{url}}\" URL'si bu sunucuda bulunamadı.",
|
||||
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> için tam metin araması yapın",
|
||||
"random-article-failure": "Hata! Rastgele bir madde seçilemedi :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}}, ham içerik için geçerli bir istek değil.",
|
||||
"no-value-for-arg": "{{ARGUMENT}} bağımsız değişkeni için değer sağlanmadı",
|
||||
"no-query": "Sorgu sağlanmadı.",
|
||||
"raw-entry-not-found": "{{DATATYPE}} {{ENTRY}} girişi bulunamadı",
|
||||
"400-page-title": "Geçersiz istek",
|
||||
"400-page-heading": "Geçersiz istek",
|
||||
|
||||
16
static/i18n/zh-hans.json
Normal file
16
static/i18n/zh-hans.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"GuoPC",
|
||||
"StarrySky"
|
||||
]
|
||||
},
|
||||
"name": "英语",
|
||||
"no-query": "未提供查询。",
|
||||
"400-page-title": "无效请求",
|
||||
"400-page-heading": "无效请求",
|
||||
"404-page-heading": "未找到",
|
||||
"500-page-title": "内部服务器错误",
|
||||
"500-page-heading": "内部服务器错误",
|
||||
"library-button-text": "前往欢迎页面"
|
||||
}
|
||||
@@ -1,15 +1,32 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kly",
|
||||
"Winston Sung"
|
||||
]
|
||||
},
|
||||
"name": "繁體中文",
|
||||
"suggest-full-text-search": "正在包含「{{{SEARCH_TERMS}}}」…",
|
||||
"no-such-book": "沒有這樣的書籍:{{BOOK_NAME}}",
|
||||
"too-many-books": "請求太多個書籍({{NB_BOOKS}}),上限是 {{LIMIT}} 個",
|
||||
"no-book-found": "沒有書籍符合選擇標準",
|
||||
"url-not-found": "在此伺服器上找不到請求的 URL「{{url}}」。",
|
||||
"suggest-search": "建立 <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> 使用的全文搜尋",
|
||||
"random-article-failure": "哎呀!隨機挑選條目失敗 :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}}不是原始內容的有效請求。",
|
||||
"no-value-for-arg": "沒有為引數 {{ARGUMENT}} 提供內容",
|
||||
"no-query": "未提供查詢。",
|
||||
"raw-entry-not-found": "找不到{{DATATYPE}}項目{{ENTRY}}",
|
||||
"400-page-title": "無效請求",
|
||||
"400-page-heading": "無效請求",
|
||||
"404-page-title": "查無內容",
|
||||
"404-page-heading": "查無頁面",
|
||||
"500-page-title": "內部伺服器錯誤",
|
||||
"500-page-heading": "內部伺服器錯誤",
|
||||
"fulltext-search-unavailable": "全文搜尋無效",
|
||||
"no-search-results": "全文搜尋引擎不適用此內容。",
|
||||
"library-button-text": "前往歡迎首頁",
|
||||
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
|
||||
"random-page-button-text": "前往隨機選取頁面",
|
||||
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
i18n/bn.json
|
||||
i18n/cs.json
|
||||
i18n/de.json
|
||||
i18n/en.json
|
||||
i18n/fr.json
|
||||
i18n/he.json
|
||||
@@ -8,10 +10,13 @@ i18n/ja.json
|
||||
i18n/ko.json
|
||||
i18n/ku-latn.json
|
||||
i18n/mk.json
|
||||
i18n/nqo.json
|
||||
i18n/pl.json
|
||||
i18n/ru.json
|
||||
i18n/sc.json
|
||||
i18n/sk.json
|
||||
i18n/sv.json
|
||||
i18n/test.json
|
||||
i18n/tr.json
|
||||
i18n/zh-hans.json
|
||||
i18n/zh-hant.json
|
||||
|
||||
@@ -15,7 +15,7 @@ preprocessed_resources = custom_target('preprocessed_resource_files',
|
||||
|
||||
lib_resources = custom_target('resources',
|
||||
input: preprocessed_resources,
|
||||
output: ['kiwixlib-resources.cpp', 'kiwixlib-resources.h'],
|
||||
output: ['libkiwix-resources.cpp', 'libkiwix-resources.h'],
|
||||
command:[res_compiler,
|
||||
'--cxxfile', '@OUTPUT0@',
|
||||
'--hfile', '@OUTPUT1@',
|
||||
|
||||
@@ -4,7 +4,6 @@ skin/magnet.png
|
||||
skin/download.png
|
||||
skin/hash.png
|
||||
skin/search-icon.svg
|
||||
skin/taskbar.js
|
||||
skin/iso6391To3.js
|
||||
skin/isotope.pkgd.min.js
|
||||
skin/index.js
|
||||
@@ -13,30 +12,30 @@ skin/taskbar.css
|
||||
skin/index.css
|
||||
skin/fonts/Poppins.ttf
|
||||
skin/fonts/Roboto.ttf
|
||||
skin/block_external.js
|
||||
skin/search_results.css
|
||||
skin/blank.html
|
||||
skin/viewer.js
|
||||
viewer.html
|
||||
templates/search_result.html
|
||||
templates/search_result.xml
|
||||
templates/error.html
|
||||
templates/error.xml
|
||||
templates/index.html
|
||||
templates/suggestion.json
|
||||
templates/head_taskbar.html
|
||||
templates/taskbar_part.html
|
||||
templates/external_blocker_part.html
|
||||
templates/captured_external.html
|
||||
templates/catalog_entries.xml
|
||||
templates/catalog_v2_root.xml
|
||||
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
|
||||
templates/viewer_settings.js
|
||||
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
|
||||
|
||||
639
static/skin/autoComplete.js
Normal file
639
static/skin/autoComplete.js
Normal file
@@ -0,0 +1,639 @@
|
||||
// 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;
|
||||
|
||||
}));
|
||||
2
static/skin/autoComplete.min.js
vendored
2
static/skin/autoComplete.min.js
vendored
File diff suppressed because one or more lines are too long
11
static/skin/blank.html
Normal file
11
static/skin/blank.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Blank page</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
const root = document.querySelector( `link[type='root']` ).getAttribute("href");
|
||||
// `block_path` variable used by openzim/warc2zim to detect whether URL blocking is enabled or not
|
||||
var block_path = `${root}/catch/external`;
|
||||
// called only on external links
|
||||
function capture_event(e, target) { target.setAttribute("href", encodeURI(block_path + "?source=" + target.href)); }
|
||||
|
||||
// called on all link clicks. filters external and call capture_event
|
||||
function on_click_event(e) {
|
||||
var target = findParent("a", e.target);
|
||||
if (target !== null && "href" in target) {
|
||||
var href = target.href;
|
||||
if (window.location.pathname.indexOf(block_path) == 0) // already in catch page
|
||||
return;
|
||||
if (href.indexOf(window.location.origin) == 0)
|
||||
return;
|
||||
if (href.substr(0, 2) == "//")
|
||||
return capture_event(e, target);
|
||||
if (href.substr(0, 5) == "http:")
|
||||
return capture_event(e, target);
|
||||
if (href.substr(0, 6) == "https:")
|
||||
return capture_event(e, target);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// script entrypoint (called on document ready)
|
||||
function run() { live('a', 'click', on_click_event); }
|
||||
|
||||
// find first parent with tagname
|
||||
function findParent(tagname, el) {
|
||||
while (el) {
|
||||
if ((el.nodeName || el.tagName).toLowerCase() === tagname.toLowerCase()) {
|
||||
return el;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// matches polyfill
|
||||
this.Element && function(ElementPrototype) {
|
||||
ElementPrototype.matches = ElementPrototype.matches ||
|
||||
ElementPrototype.matchesSelector ||
|
||||
ElementPrototype.webkitMatchesSelector ||
|
||||
ElementPrototype.msMatchesSelector ||
|
||||
function(selector) {
|
||||
var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
|
||||
while (nodes[++i] && nodes[i] != node);
|
||||
return !!nodes[i];
|
||||
}
|
||||
}(Element.prototype);
|
||||
|
||||
// helper for enabling IE 8 event bindings
|
||||
function addEvent(el, type, handler) {
|
||||
if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler);
|
||||
}
|
||||
|
||||
// live binding helper using matchesSelector
|
||||
function live(selector, event, callback, context) {
|
||||
addEvent(context || document, event, function(e) {
|
||||
var found, el = e.target || e.srcElement;
|
||||
while (el && el.matches && el !== context && !(found = el.matches(selector))) el = el.parentElement;
|
||||
if (found) callback.call(el, e);
|
||||
});
|
||||
}
|
||||
|
||||
// in case the document is already rendered
|
||||
if (document.readyState!='loading') run();
|
||||
// modern browsers
|
||||
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run);
|
||||
// IE <= 8
|
||||
else document.attachEvent('onreadystatechange', function(){
|
||||
if (document.readyState=='complete') run();
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
/* Modified from https://github.com/TarekRaafat/autoComplete.js */
|
||||
.autoComplete_wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -61,7 +61,7 @@ body {
|
||||
padding: 7px 10px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.kiwixNav__kiwixFilter:-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ body {
|
||||
border-radius: 10px;
|
||||
border: solid 1px #b5b2b2;
|
||||
padding: 10px;
|
||||
background-image: url('./search-icon.svg');
|
||||
background-image: url('../skin/search-icon.svg?KIWIXCACHEID');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
background-origin: content-box;
|
||||
@@ -213,7 +213,7 @@ body {
|
||||
display: grid;
|
||||
font-family: poppins;
|
||||
color: black;
|
||||
padding: 12px 10px 0 2px;
|
||||
padding: 5px 10px 0 7px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
@@ -482,4 +482,4 @@ body {
|
||||
.kiwixNav__filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,9 @@
|
||||
} catch {
|
||||
downloadLink = '';
|
||||
}
|
||||
const bookName = link.split('/').pop();
|
||||
const viewerLink = `${root}/viewer#${bookName}`;
|
||||
|
||||
const humanFriendlyZimSize = humanFriendlySize(zimSize);
|
||||
|
||||
const divTag = document.createElement('div');
|
||||
@@ -122,7 +125,7 @@
|
||||
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
|
||||
divTag.innerHTML = `
|
||||
<div class="book__wrapper">
|
||||
<a class="book__link" href="${link}" data-hover="Preview">
|
||||
<a class="book__link" href="${viewerLink}" data-hover="Preview">
|
||||
<div class="book__link__wrapper">
|
||||
<div class="book__icon" ${faviconAttr}></div>
|
||||
<div class="book__header">
|
||||
@@ -179,12 +182,12 @@
|
||||
<div onclick="closeModal()" class="modal-close-button">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
|
||||
0.683417 13.7071 0.292893C13.3166 -0.0976311 12.6834 -0.0976311 12.2929 0.292893L7 5.58579L1.70711
|
||||
0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417
|
||||
-0.0976311 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976311 12.6834
|
||||
-0.0976311 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7
|
||||
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
|
||||
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
|
||||
14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z" fill="black" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -203,7 +206,7 @@
|
||||
<div>Sha256 hash</div>
|
||||
</a>
|
||||
</div>
|
||||
${magnetLink ?
|
||||
${magnetLink ?
|
||||
`<div class="modal-regular-download">
|
||||
<a href="${magnetLink}" target="_blank">
|
||||
<img src="../skin/magnet.png?KIWIXCACHEID" alt="download magnet" />
|
||||
@@ -388,7 +391,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function addTagElement(tagValue, resetFilter) {
|
||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||
tagElement.style.display = 'inline-block';
|
||||
@@ -475,7 +478,7 @@
|
||||
const currentLink = window.location.search;
|
||||
const newLink = `?${params.toString()}`;
|
||||
if (currentLink != newLink) {
|
||||
window.history.pushState({}, null, newLink);
|
||||
window.history.pushState({}, null, newLink);
|
||||
}
|
||||
}
|
||||
updateVisibleParams();
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
#kiwixtoolbar {
|
||||
position: fixed;
|
||||
padding: .5em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background-position-y: 0;
|
||||
transition: 0.3s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@@ -135,10 +129,6 @@ a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
|
||||
column-count: 1 !important;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: calc(3em - 5px) !important;
|
||||
}
|
||||
|
||||
@media(min-width:420px) {
|
||||
.kiwix_button_cont {
|
||||
display: inline-block !important;
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
function htmlDecode(input) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
|
||||
function setupAutoHidingOfTheToolbar() {
|
||||
let lastScrollTop = 0;
|
||||
const delta = 5;
|
||||
let didScroll = false;
|
||||
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
didScroll = true;
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
if (didScroll) {
|
||||
hasScrolled();
|
||||
didScroll = false;
|
||||
}
|
||||
}, 250);
|
||||
|
||||
function hasScrolled() {
|
||||
const st = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
if (Math.abs(lastScrollTop - st) <= delta)
|
||||
return;
|
||||
|
||||
if (st > lastScrollTop) {
|
||||
kiwixToolBar.style.top = '-100%';
|
||||
} else {
|
||||
kiwixToolBar.style.top = '0';
|
||||
}
|
||||
|
||||
lastScrollTop = st;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const root = document.querySelector(`link[type='root']`).getAttribute("href");
|
||||
const bookName = (window.location.pathname == `${root}/search`)
|
||||
? (new URLSearchParams(window.location.search)).get('content')
|
||||
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
|
||||
|
||||
const autoCompleteJS = new autoComplete(
|
||||
{
|
||||
selector: "#kiwixsearchbox",
|
||||
placeHolder: document.querySelector("#kiwixsearchbox").title,
|
||||
threshold: 1,
|
||||
debounce: 300,
|
||||
data : {
|
||||
src: async (query) => {
|
||||
try {
|
||||
// Fetch Data from external Source
|
||||
const source = await fetch(`${root}/suggest?content=${encodeURIComponent(bookName)}&term=${encodeURIComponent(query)}`);
|
||||
const data = await source.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
},
|
||||
keys: ['label'],
|
||||
},
|
||||
submit: true,
|
||||
searchEngine: (query, record) => {
|
||||
// We accept all records
|
||||
return true;
|
||||
},
|
||||
resultsList: {
|
||||
noResults: true,
|
||||
/* We must display 10 results (requested) + 1 potential link to do a full text search. */
|
||||
maxResults: 11,
|
||||
},
|
||||
resultItem: {
|
||||
element: (item, data) => {
|
||||
let searchLink;
|
||||
if (data.value.kind == "path") {
|
||||
searchLink = `${root}/${bookName}/${htmlDecode(data.value.path)}`;
|
||||
} else {
|
||||
searchLink = `${root}/search?content=${encodeURIComponent(bookName)}&pattern=${encodeURIComponent(htmlDecode(data.value.value))}`;
|
||||
}
|
||||
item.innerHTML = `<a class="suggest" href="${searchLink}">${htmlDecode(data.value.label)}</a>`;
|
||||
},
|
||||
highlight: "autoComplete_highlight",
|
||||
selected: "autoComplete_selected"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
document.querySelector('#kiwixsearchform').addEventListener('submit', function(event) {
|
||||
try {
|
||||
const selectedElemLink = document.querySelector('.autoComplete_selected > a').href;
|
||||
if (selectedElemLink) {
|
||||
event.preventDefault();
|
||||
window.location = selectedElemLink;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
const kiwixSearchBox = document.querySelector('#kiwixsearchbox');
|
||||
const kiwixSearchForm = document.querySelector('.kiwix_searchform');
|
||||
kiwixSearchBox.addEventListener('focus', () => {
|
||||
kiwixSearchForm.classList.add('full_width');
|
||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.add('searching');
|
||||
document.querySelector('.kiwix_button_cont').classList.add('searching');
|
||||
});
|
||||
kiwixSearchBox.addEventListener('blur', () => {
|
||||
kiwixSearchForm.classList.remove('full_width');
|
||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.remove('searching');
|
||||
document.querySelector('.kiwix_button_cont').classList.remove('searching');
|
||||
});
|
||||
|
||||
// cybook hack
|
||||
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
|
||||
document.querySelector('html').classList.add('cybook');
|
||||
}
|
||||
|
||||
if (document.body.clientWidth < 520) {
|
||||
setupAutoHidingOfTheToolbar();
|
||||
}
|
||||
|
||||
});
|
||||
402
static/skin/viewer.js
Normal file
402
static/skin/viewer.js
Normal file
@@ -0,0 +1,402 @@
|
||||
// Terminology
|
||||
//
|
||||
// user url: identifier of the page that has to be displayed in the viewer
|
||||
// and that is used as the hash component of the viewer URL. For
|
||||
// book resources the address url is {book}/{resource} .
|
||||
//
|
||||
// iframe url: the URL to be loaded in the viewer iframe.
|
||||
|
||||
function userUrl2IframeUrl(url) {
|
||||
if ( url == '' ) {
|
||||
return blankPageUrl;
|
||||
}
|
||||
|
||||
if ( url.startsWith('search?') ) {
|
||||
return `${root}/${url}`;
|
||||
}
|
||||
|
||||
return `${root}/content/${url}`;
|
||||
}
|
||||
|
||||
function getBookFromUserUrl(url) {
|
||||
if ( url == '' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( url.startsWith('search?') ) {
|
||||
const p = new URLSearchParams(url.slice("search?".length));
|
||||
return p.get('books.name') || p.get('content');
|
||||
}
|
||||
return url.split('/')[0];
|
||||
}
|
||||
|
||||
let currentBook = getBookFromUserUrl(location.hash.slice(1));
|
||||
let currentBookTitle = null;
|
||||
|
||||
const bookUIGroup = document.getElementById('kiwix_serve_taskbar_book_ui_group');
|
||||
const homeButton = document.getElementById('kiwix_serve_taskbar_home_button');
|
||||
const contentIframe = document.getElementById('content_iframe');
|
||||
|
||||
|
||||
function gotoMainPageOfCurrentBook() {
|
||||
location.hash = currentBook + '/';
|
||||
}
|
||||
|
||||
function gotoUrl(url) {
|
||||
contentIframe.src = url;
|
||||
}
|
||||
|
||||
function gotoRandomPage() {
|
||||
gotoUrl(`${root}/random?content=${currentBook}`);
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
const searchbox = document.getElementById('kiwixsearchbox');
|
||||
const q = encodeURIComponent(searchbox.value);
|
||||
gotoUrl(`${root}/search?books.name=${currentBook}&pattern=${q}`);
|
||||
}
|
||||
|
||||
function suggestionsApiURL()
|
||||
{
|
||||
return `${root}/suggest?content=${encodeURIComponent(currentBook)}`;
|
||||
}
|
||||
|
||||
function setCurrentBook(book, title) {
|
||||
currentBook = book;
|
||||
currentBookTitle = title;
|
||||
homeButton.title = `Go to the main page of '${title}'`;
|
||||
homeButton.setAttribute("aria-label", homeButton.title);
|
||||
homeButton.innerHTML = `<button>${title}</button>`;
|
||||
bookUIGroup.style.display = 'inline';
|
||||
updateSearchBoxForBookChange();
|
||||
}
|
||||
|
||||
function noCurrentBook() {
|
||||
currentBook = null;
|
||||
currentBookTitle = null;
|
||||
bookUIGroup.style.display = 'none';
|
||||
updateSearchBoxForBookChange();
|
||||
}
|
||||
|
||||
function updateCurrentBookIfNeeded(userUrl) {
|
||||
const book = getBookFromUserUrl(userUrl);
|
||||
if ( currentBook != book ) {
|
||||
updateCurrentBook(book);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentBook(book) {
|
||||
if ( book == null ) {
|
||||
noCurrentBook();
|
||||
} else {
|
||||
fetch(`./raw/${book}/meta/Title`).then(async (resp) => {
|
||||
if ( resp.ok ) {
|
||||
setCurrentBook(book, await resp.text());
|
||||
} else {
|
||||
noCurrentBook();
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log("Error fetching book title: " + err);
|
||||
noCurrentBook();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function iframeUrl2UserUrl(url, query) {
|
||||
if ( url == blankPageUrl ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( url == `${root}/search` ) {
|
||||
return `search${query}`;
|
||||
}
|
||||
|
||||
url = url.slice(root.length);
|
||||
|
||||
return url.split('/').slice(2).join('/');
|
||||
}
|
||||
|
||||
function getSearchPattern() {
|
||||
const url = window.location.hash.slice(1);
|
||||
if ( url.startsWith('search?') ) {
|
||||
const p = new URLSearchParams(url.slice("search?".length));
|
||||
return p.get("pattern");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
let autoCompleteJS = null;
|
||||
|
||||
function closeSuggestions() {
|
||||
if ( autoCompleteJS ) {
|
||||
autoCompleteJS.close();
|
||||
}
|
||||
}
|
||||
|
||||
function updateSearchBoxForLocationChange() {
|
||||
closeSuggestions();
|
||||
document.getElementById("kiwixsearchbox").value = getSearchPattern();
|
||||
}
|
||||
|
||||
function updateSearchBoxForBookChange() {
|
||||
const searchbox = document.getElementById('kiwixsearchbox');
|
||||
const kiwixSearchFormWrapper = document.querySelector('.kiwix_searchform');
|
||||
if ( currentBookTitle ) {
|
||||
searchbox.title = `Search '${currentBookTitle}'`;
|
||||
searchbox.placeholder = searchbox.title;
|
||||
searchbox.setAttribute("aria-label", searchbox.title);
|
||||
kiwixSearchFormWrapper.style.display = 'inline';
|
||||
} else {
|
||||
kiwixSearchFormWrapper.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function handle_location_hash_change() {
|
||||
const hash = window.location.hash.slice(1);
|
||||
console.log("handle_location_hash_change: " + hash);
|
||||
updateCurrentBookIfNeeded(hash);
|
||||
const iframeContentUrl = userUrl2IframeUrl(hash);
|
||||
if ( iframeContentUrl != contentIframe.contentWindow.location.pathname ) {
|
||||
contentIframe.contentWindow.location.replace(iframeContentUrl);
|
||||
}
|
||||
updateSearchBoxForLocationChange();
|
||||
previousScrollTop = Infinity;
|
||||
}
|
||||
|
||||
function handle_content_url_change() {
|
||||
const iframeLocation = contentIframe.contentWindow.location;
|
||||
console.log('handle_content_url_change: ' + iframeLocation.href);
|
||||
document.title = contentIframe.contentDocument.title;
|
||||
const iframeContentUrl = iframeLocation.pathname;
|
||||
const iframeContentQuery = iframeLocation.search;
|
||||
const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery);
|
||||
const viewerURL = location.origin + location.pathname + location.search;
|
||||
window.location.replace(viewerURL + '#' + newHash);
|
||||
updateCurrentBookIfNeeded(newHash);
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// External link blocking
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function matchingAncestorElement(el, context, selector) {
|
||||
while (el && el.matches && el !== context) {
|
||||
if ( el.matches(selector) )
|
||||
return el;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const block_path = `${root}/catch/external`;
|
||||
|
||||
function blockLink(target) {
|
||||
const encodedHref = encodeURIComponent(target.href);
|
||||
target.setAttribute("href", block_path + "?source=" + encodedHref);
|
||||
}
|
||||
|
||||
function isExternalUrl(url) {
|
||||
if ( url.startsWith(window.location.origin) )
|
||||
return false;
|
||||
|
||||
return url.startsWith("//")
|
||||
|| url.startsWith("http:")
|
||||
|| url.startsWith("https:");
|
||||
}
|
||||
|
||||
function onClickEvent(e) {
|
||||
const iframeDocument = contentIframe.contentDocument;
|
||||
const target = matchingAncestorElement(e.target, iframeDocument, "a");
|
||||
if (target !== null && "href" in target) {
|
||||
if ( isExternalUrl(target.href) ) {
|
||||
target.setAttribute("target", "_top");
|
||||
if ( viewerSettings.linkBlockingEnabled ) {
|
||||
return blockLink(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper for enabling IE 8 event bindings
|
||||
function addEventHandler(el, eventType, handler) {
|
||||
if (el.attachEvent)
|
||||
el.attachEvent('on'+eventType, handler);
|
||||
else
|
||||
el.addEventListener(eventType, handler);
|
||||
}
|
||||
|
||||
function setupEventHandler(context, selector, eventType, callback) {
|
||||
addEventHandler(context, eventType, function(e) {
|
||||
const eventElement = e.target || e.srcElement;
|
||||
const el = matchingAncestorElement(eventElement, context, selector);
|
||||
if (el)
|
||||
callback.call(el, e);
|
||||
});
|
||||
}
|
||||
|
||||
// matches polyfill
|
||||
this.Element && function(ElementPrototype) {
|
||||
ElementPrototype.matches = ElementPrototype.matches ||
|
||||
ElementPrototype.matchesSelector ||
|
||||
ElementPrototype.webkitMatchesSelector ||
|
||||
ElementPrototype.msMatchesSelector ||
|
||||
function(selector) {
|
||||
var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
|
||||
while (nodes[++i] && nodes[i] != node);
|
||||
return !!nodes[i];
|
||||
}
|
||||
}(Element.prototype);
|
||||
|
||||
function setup_external_link_blocker() {
|
||||
setupEventHandler(contentIframe.contentDocument, 'a', 'click', onClickEvent);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// End of external link blocking
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function on_content_load() {
|
||||
handle_content_url_change();
|
||||
setup_external_link_blocker();
|
||||
}
|
||||
|
||||
window.onresize = handle_visual_viewport_change;
|
||||
window.onhashchange = handle_location_hash_change;
|
||||
|
||||
updateCurrentBook(currentBook);
|
||||
handle_location_hash_change();
|
||||
|
||||
function htmlDecode(input) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
|
||||
function setupAutoHidingOfTheToolbar() {
|
||||
setInterval(updateToolbarVisibilityState, 250);
|
||||
}
|
||||
|
||||
function setupSuggestions() {
|
||||
const kiwixSearchBox = document.querySelector('#kiwixsearchbox');
|
||||
const kiwixSearchFormWrapper = document.querySelector('.kiwix_searchform');
|
||||
|
||||
autoCompleteJS = new autoComplete(
|
||||
{
|
||||
selector: "#kiwixsearchbox",
|
||||
placeHolder: kiwixSearchBox.title,
|
||||
threshold: 1,
|
||||
debounce: 300,
|
||||
data : {
|
||||
src: async (query) => {
|
||||
try {
|
||||
// Fetch Data from external Source
|
||||
const source = await fetch(`${suggestionsApiURL()}&term=${encodeURIComponent(query)}`);
|
||||
const data = await source.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
},
|
||||
keys: ['label'],
|
||||
},
|
||||
submit: true,
|
||||
searchEngine: (query, record) => {
|
||||
// We accept all records
|
||||
return true;
|
||||
},
|
||||
resultsList: {
|
||||
noResults: true,
|
||||
// We must display 10 results (requested) + 1 potential link to do a full text search.
|
||||
maxResults: 11,
|
||||
},
|
||||
resultItem: {
|
||||
element: (item, data) => {
|
||||
let searchLink;
|
||||
if (data.value.kind == "path") {
|
||||
searchLink = `${root}/${currentBook}/${htmlDecode(data.value.path)}`;
|
||||
} else {
|
||||
searchLink = `${root}/search?content=${encodeURIComponent(currentBook)}&pattern=${encodeURIComponent(htmlDecode(data.value.value))}`;
|
||||
}
|
||||
item.innerHTML = `<a class="suggest" href="javascript:gotoUrl('${searchLink}')">${htmlDecode(data.value.label)}</a>`;
|
||||
},
|
||||
highlight: "autoComplete_highlight",
|
||||
selected: "autoComplete_selected"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
document.querySelector('#kiwixsearchform').addEventListener('submit', function(event) {
|
||||
closeSuggestions();
|
||||
try {
|
||||
const selectedElem = document.querySelector('.autoComplete_selected > a');
|
||||
if (selectedElem) {
|
||||
event.preventDefault();
|
||||
selectedElem.click();
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
|
||||
kiwixSearchBox.addEventListener('focus', () => {
|
||||
kiwixSearchFormWrapper.classList.add('full_width');
|
||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.add('searching');
|
||||
document.querySelector('.kiwix_button_cont').classList.add('searching');
|
||||
});
|
||||
kiwixSearchBox.addEventListener('blur', () => {
|
||||
kiwixSearchFormWrapper.classList.remove('full_width');
|
||||
document.querySelector('label[for="kiwix_button_show_toggle"]').classList.remove('searching');
|
||||
document.querySelector('.kiwix_button_cont').classList.remove('searching');
|
||||
});
|
||||
}
|
||||
|
||||
function setupViewer() {
|
||||
// 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);
|
||||
|
||||
const kiwixToolBarWrapper = document.getElementById('kiwixtoolbarwrapper');
|
||||
if ( ! viewerSettings.toolbarEnabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
kiwixToolBarWrapper.style.display = 'block';
|
||||
if ( ! viewerSettings.libraryButtonEnabled ) {
|
||||
document.getElementById("kiwix_serve_taskbar_library_button").remove();
|
||||
}
|
||||
|
||||
setupSuggestions();
|
||||
|
||||
// cybook hack
|
||||
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
|
||||
document.querySelector('html').classList.add('cybook');
|
||||
}
|
||||
|
||||
if (document.body.clientWidth < 520) {
|
||||
setupAutoHidingOfTheToolbar();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
{{#with_xml_header}}<?xml version="1.0" encoding="UTF-8"?>
|
||||
{{/with_xml_header}} <entry>
|
||||
<entry>
|
||||
<id>urn:uuid:{{id}}</id>
|
||||
<title>{{title}}</title>
|
||||
<updated>{{updated}}</updated>
|
||||
{{#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>
|
||||
<summary>{{description}}</summary>
|
||||
<language>{{language}}</language>
|
||||
<name>{{name}}</name>
|
||||
<flavour>{{flavour}}</flavour>
|
||||
@@ -29,5 +24,4 @@
|
||||
{{#url}}
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
|
||||
{{/url}}
|
||||
{{/dump_partial_entries}}
|
||||
</entry>
|
||||
|
||||
8
static/templates/catalog_v2_partial_entry.xml
Normal file
8
static/templates/catalog_v2_partial_entry.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
@@ -1 +0,0 @@
|
||||
<script type="text/javascript" src="{{root}}/skin/block_external.js"></script>
|
||||
@@ -1,4 +0,0 @@
|
||||
<link type="text/css" href="{{root}}/skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<link type="text/css" href="{{root}}/skin/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="{{root}}/skin/taskbar.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/autoComplete.min.js?KIWIXCACHEID"></script>
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link type="root" href="{{root}}">
|
||||
<title>Welcome to Kiwix Server</title>
|
||||
<link
|
||||
type="text/css"
|
||||
@@ -12,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">
|
||||
<link rel="manifest" href="{{root}}/skin/favicon/site.webmanifest?KIWIXCACHEID">
|
||||
<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">
|
||||
|
||||
@@ -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="{{protocolPrefix}}search/searchdescription.xml"/>
|
||||
<atom:link rel="search" type="application/opensearchdescription+xml" href="{{searchProtocolPrefix}}/searchdescription.xml"/>
|
||||
<opensearch:Query role="request"
|
||||
searchTerms="{{query.pattern}}"{{#query.lang}}
|
||||
language="{{query.lang}}"{{/query.lang}}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<span class="kiwix">
|
||||
<span id="kiwixtoolbar" class="ui-widget-header">
|
||||
<div class="kiwix_centered">
|
||||
<div class="kiwix_searchform">
|
||||
<form class="kiwixsearch" method="GET" action="{{root}}/search" id="kiwixsearchform">
|
||||
{{#hascontent}}<input type="hidden" name="content" value="{{content}}" />{{/hascontent}}
|
||||
<label for="kiwixsearchbox">🔍</label>
|
||||
<input autocomplete="off" id="kiwixsearchbox" name="pattern" type="text" size="50" title="{{{SEARCHBOX_TOOLTIP}}}" aria-label="{{{SEARCHBOX_TOOLTIP}}}">
|
||||
</form>
|
||||
</div>
|
||||
<input type="checkbox" id="kiwix_button_show_toggle">
|
||||
<label for="kiwix_button_show_toggle"><img src="{{root}}/skin/caret.png?KIWIXCACHEID" alt=""></label>
|
||||
<div class="kiwix_button_cont">
|
||||
{{#withlibrarybutton}}
|
||||
<a id="kiwix_serve_taskbar_library_button" title="{{{LIBRARY_BUTTON_TEXT}}}" aria-label="{{{LIBRARY_BUTTON_TEXT}}}" href="{{root}}/"><button>🏠</button></a>
|
||||
{{/withlibrarybutton}}
|
||||
{{#hascontent}}
|
||||
<a id="kiwix_serve_taskbar_home_button" title="{{{HOME_BUTTON_TEXT}}}" aria-label="{{{HOME_BUTTON_TEXT}}}" href="{{root}}/{{content}}/"><button>{{title}}</button></a>
|
||||
<a id="kiwix_serve_taskbar_random_button" title="{{{RANDOM_PAGE_BUTTON_TEXT}}}" aria-label="{{{RANDOM_PAGE_BUTTON_TEXT}}}"
|
||||
href="{{root}}/random?content={{#urlencoded}}{{{content}}}{{/urlencoded}}"><button>🎲</button></a>
|
||||
{{/hascontent}}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
5
static/templates/viewer_settings.js
Normal file
5
static/templates/viewer_settings.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const viewerSettings = {
|
||||
toolbarEnabled: {{enable_toolbar}},
|
||||
linkBlockingEnabled: {{enable_link_blocking}},
|
||||
libraryButtonEnabled: {{enable_library_button}}
|
||||
}
|
||||
68
static/viewer.html
Normal file
68
static/viewer.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ZIM Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link type="text/css" href="./skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="./viewer_settings.js"></script>
|
||||
<script type="text/javascript" src="./skin/viewer.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="./skin/autoComplete.min.js?KIWIXCACHEID"></script>
|
||||
<script>
|
||||
function getRootLocation() {
|
||||
const p = location.pathname;
|
||||
return p.slice(0, p.length - '/viewer'.length);
|
||||
}
|
||||
|
||||
const root = getRootLocation();
|
||||
const blankPageUrl = root + "/skin/blank.html?KIWIXCACHEID";
|
||||
|
||||
if ( location.hash == '' ) {
|
||||
location.href = root + '/';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body style="margin:0" onload="setupViewer()">
|
||||
<div class="kiwix" style="display:none" id="kiwixtoolbarwrapper">
|
||||
<div id="kiwixtoolbar" class="ui-widget-header">
|
||||
<div class="kiwix_centered">
|
||||
<div class="kiwix_searchform">
|
||||
<form class="kiwixsearch" method="GET" action="javascript:performSearch()" id="kiwixsearchform">
|
||||
<label for="kiwixsearchbox">🔍</label>
|
||||
<input autocomplete="off" class="ui-autocomplete-input" id="kiwixsearchbox" name="pattern" type="text" title="Search '{{title}}'" aria-label="Search '{{title}}'">
|
||||
</form>
|
||||
</div>
|
||||
<input type="checkbox" id="kiwix_button_show_toggle">
|
||||
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?KIWIXCACHEID" alt=""></label>
|
||||
<div class="kiwix_button_cont">
|
||||
<a id="kiwix_serve_taskbar_library_button" title="Go to welcome page" aria-label="Go to welcome page" href="./"><button>🏠</button></a>
|
||||
<span id="kiwix_serve_taskbar_book_ui_group">
|
||||
<a id="kiwix_serve_taskbar_home_button"
|
||||
title="Go to the main page of the current book"
|
||||
aria-label="Go to the main page of the current book"
|
||||
onclick="gotoMainPageOfCurrentBook()"></a>
|
||||
<a id="kiwix_serve_taskbar_random_button"
|
||||
title="Go to a randomly selected page"
|
||||
aria-label="Go to a randomly selected page"
|
||||
onclick="gotoRandomPage()">
|
||||
<button>🎲</button>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iframe id="content_iframe"
|
||||
referrerpolicy="same-origin"
|
||||
onload="on_content_load()"
|
||||
src="./skin/blank.html?KIWIXCACHEID" title="ZIM content" width="100%"
|
||||
style="border:0px">
|
||||
</iframe>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
4
test/data/lib_for_server_search_test.xml
Normal file
4
test/data/lib_for_server_search_test.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
@@ -801,8 +801,14 @@ 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",
|
||||
|
||||
@@ -18,8 +18,13 @@ 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, "./test/library.xml"));
|
||||
zfs1_.reset(new ZimFileServer(PORT, ZimFileServer::DEFAULT_OPTIONS, "./test/library.xml"));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
@@ -70,20 +75,20 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
" type=\"application/opensearchdescription+xml\"" \
|
||||
" href=\"/ROOT/catalog/searchdescription.xml\" />\n"
|
||||
|
||||
#define CHARLES_RAY_CATALOG_ENTRY \
|
||||
#define CATALOG_ENTRY(UUID, TITLE, SUMMARY, LANG, NAME, CATEGORY, TAGS, EXTRA_LINK, CONTENT_NAME, FILE_NAME, LENGTH) \
|
||||
" <entry>\n" \
|
||||
" <id>urn:uuid:charlesray</id>\n" \
|
||||
" <title>Charles, Ray</title>\n" \
|
||||
" <id>urn:uuid:" UUID "</id>\n" \
|
||||
" <title>" TITLE "</title>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
||||
" <language>fra</language>\n" \
|
||||
" <name>wikipedia_fr_ray_charles</name>\n" \
|
||||
" <summary>" SUMMARY "</summary>\n" \
|
||||
" <language>" LANG "</language>\n" \
|
||||
" <name>" NAME "</name>\n" \
|
||||
" <flavour></flavour>\n" \
|
||||
" <category>jazz</category>\n" \
|
||||
" <tags>unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
|
||||
" <category>" CATEGORY "</category>\n" \
|
||||
" <tags>" TAGS "</tags>\n" \
|
||||
" <articleCount>284</articleCount>\n" \
|
||||
" <mediaCount>2</mediaCount>\n" \
|
||||
" <link type=\"text/html\" href=\"/ROOT/content/zimfile%26other\" />\n" \
|
||||
" " EXTRA_LINK "<link type=\"text/html\" href=\"/ROOT/content/" CONTENT_NAME "\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Wikipedia</name>\n" \
|
||||
" </author>\n" \
|
||||
@@ -91,59 +96,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/zimfile%26other.zim\" length=\"569344\" />\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" \
|
||||
" </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 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"
|
||||
#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"\
|
||||
)
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_root_xml)
|
||||
{
|
||||
@@ -312,6 +317,44 @@ 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)
|
||||
{
|
||||
{
|
||||
@@ -354,7 +397,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 (count=1&start=1)</title>\n"
|
||||
" <title>Filtered zims (start=1&count=1)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>1</startIndex>\n"
|
||||
@@ -370,7 +413,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 (count=10&start=100)</title>\n"
|
||||
" <title>Filtered zims (start=100&count=10)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>100</startIndex>\n"
|
||||
@@ -633,8 +676,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("?count=1&start=1")
|
||||
" <title>Filtered Entries (count=1&start=1)</title>\n"
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?start=1&count=1")
|
||||
" <title>Filtered Entries (start=1&count=1)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>1</startIndex>\n"
|
||||
@@ -662,6 +705,40 @@ 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");
|
||||
@@ -780,4 +857,40 @@ 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
|
||||
|
||||
@@ -2,9 +2,9 @@ tests = [
|
||||
'library',
|
||||
'regex',
|
||||
'tagParsing',
|
||||
'counterParsing',
|
||||
'stringTools',
|
||||
'pathTools',
|
||||
'otherTools',
|
||||
'kiwixserve',
|
||||
'book',
|
||||
'manager',
|
||||
@@ -37,6 +37,7 @@ 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',
|
||||
@@ -69,7 +70,7 @@ if gtest_dep.found() and not meson.is_cross_build()
|
||||
test_exe = executable(test_name, [test_name+'.cpp'],
|
||||
implicit_include_directories: false,
|
||||
include_directories : inc,
|
||||
link_with : kiwixlib,
|
||||
link_with : libkiwix,
|
||||
link_args: extra_link_args,
|
||||
dependencies : all_deps + [gtest_dep],
|
||||
build_rpath : '$ORIGIN')
|
||||
|
||||
174
test/otherTools.cpp
Normal file
174
test/otherTools.cpp
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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 'kiwi'...",
|
||||
"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 \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippet with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"kind" : "path"
|
||||
, "path" : "Path with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
}
|
||||
]
|
||||
)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 \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippetless title with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"kind" : "path"
|
||||
, "path" : "Path with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
}
|
||||
]
|
||||
)EXPECTEDJSON"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
kiwix::Suggestions s;
|
||||
s.addFTSearchSuggestion("eng", "text with " + SYMBOLS);
|
||||
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "text with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.? ",
|
||||
"label" : "containing 'text with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?'...",
|
||||
"kind" : "pattern"
|
||||
//EOLWHITESPACEMARKER
|
||||
}
|
||||
]
|
||||
)EXPECTEDJSON"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Suggestions, fulltextSearchSuggestionIsTranslated)
|
||||
{
|
||||
kiwix::Suggestions s;
|
||||
s.addFTSearchSuggestion("it", "kiwi");
|
||||
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "kiwi ",
|
||||
"label" : "contenente 'kiwi'...",
|
||||
"kind" : "pattern"
|
||||
//EOLWHITESPACEMARKER
|
||||
}
|
||||
]
|
||||
)EXPECTEDJSON"
|
||||
);
|
||||
}
|
||||
@@ -73,31 +73,4 @@ TEST(ReplaceRegex, middle)
|
||||
EXPECT_EQ(replaceRegex("abcdefghij", "----", "F"), "abcde----ghij");
|
||||
}
|
||||
|
||||
TEST(append, beggining)
|
||||
{
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "abcd", "----"), "abcd----efghij");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "abcde", "----"), "abcde----fghij");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "a.*i", "----"), "abcdefghi----j");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "AbCd", "----"), "abcd----efghij");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "A", "----"), "a----bcdefghij");
|
||||
}
|
||||
|
||||
TEST(append, end)
|
||||
{
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "ghij", "----"), "abcdefghij----");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "fghij", "----"), "abcdefghij----");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "c.*j", "----"), "abcdefghij----");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "GhIj", "----"), "abcdefghij----");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "J", "----"), "abcdefghij----");
|
||||
}
|
||||
|
||||
TEST(append, middle)
|
||||
{
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "cdef", "----"), "abcdef----ghij");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "cdefgh", "----"), "abcdefgh----ij");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "c.*f", "----"), "abcdef----ghij");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "DeFg", "----"), "abcdefg----hij");
|
||||
EXPECT_EQ(appendToFirstOccurence("abcdefghij", "F", "----"), "abcdef----ghij");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
502
test/server.cpp
502
test/server.cpp
@@ -23,13 +23,19 @@ T1 concat(T1 a, const T2& b)
|
||||
return a;
|
||||
}
|
||||
|
||||
const bool WITH_ETAG = true;
|
||||
const bool NO_ETAG = false;
|
||||
enum ResourceKind
|
||||
{
|
||||
ZIM_CONTENT,
|
||||
STATIC_CONTENT,
|
||||
DYNAMIC_CONTENT,
|
||||
};
|
||||
|
||||
struct Resource
|
||||
{
|
||||
bool etag_expected;
|
||||
ResourceKind kind;
|
||||
const char* url;
|
||||
|
||||
bool etag_expected() const { return kind != STATIC_CONTENT; }
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, const Resource& r)
|
||||
@@ -41,54 +47,127 @@ std::ostream& operator<<(std::ostream& out, const Resource& r)
|
||||
typedef std::vector<Resource> ResourceCollection;
|
||||
|
||||
const ResourceCollection resources200Compressible{
|
||||
{ WITH_ETAG, "/ROOT/" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT/" },
|
||||
|
||||
{ WITH_ETAG, "/ROOT/skin/taskbar.js" },
|
||||
{ WITH_ETAG, "/ROOT/skin/taskbar.css" },
|
||||
{ WITH_ETAG, "/ROOT/skin/block_external.js" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT/viewer" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT/viewer?cacheid=whatever" },
|
||||
|
||||
{ NO_ETAG, "/ROOT/catalog/search" },
|
||||
{ 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/search?content=zimfile&pattern=a" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT/catalog/search" },
|
||||
|
||||
{ NO_ETAG, "/ROOT/suggest?content=zimfile&term=ray" },
|
||||
{ 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/catch/external?source=www.example.com" },
|
||||
{ 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" },
|
||||
|
||||
{ WITH_ETAG, "/ROOT/raw/zimfile/content/A/index" },
|
||||
{ WITH_ETAG, "/ROOT/raw/zimfile/content/A/Ray_Charles" },
|
||||
{ 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" },
|
||||
};
|
||||
|
||||
const ResourceCollection resources200Uncompressible{
|
||||
{ WITH_ETAG, "/ROOT/skin/caret.png" },
|
||||
{ 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/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" },
|
||||
{ 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" },
|
||||
|
||||
{ NO_ETAG, "/ROOT/catalog/v2/illustration/6f1d19d0-633f-087b-fb55-7ac324ff9baf?size=48" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT/catalog/root.xml" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT/catalog/searchdescription.xml" },
|
||||
|
||||
{ WITH_ETAG, "/ROOT/content/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
|
||||
{ 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" },
|
||||
|
||||
{ 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" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT/catch/external?source=www.example.com" },
|
||||
|
||||
{ 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" },
|
||||
|
||||
|
||||
// The following url's responses are too small to be compressed
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
};
|
||||
|
||||
ResourceCollection all200Resources()
|
||||
@@ -103,7 +182,7 @@ TEST(indexTemplateStringTest, emptyIndexTemplate) {
|
||||
"./test/corner_cases.zim"
|
||||
};
|
||||
|
||||
ZimFileServer zfs(PORT, /*withTaskbar=*/true, ZIMFILES, "");
|
||||
ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, "");
|
||||
EXPECT_EQ(200, zfs.GET("/ROOT/")->status);
|
||||
}
|
||||
|
||||
@@ -114,13 +193,12 @@ TEST(indexTemplateStringTest, indexTemplateCheck) {
|
||||
"./test/corner_cases.zim"
|
||||
};
|
||||
|
||||
ZimFileServer zfs(PORT, /*withTaskbar=*/true, ZIMFILES, "<!DOCTYPE html><head>"
|
||||
ZimFileServer zfs(PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES, "<!DOCTYPE html><head>"
|
||||
"<title>Welcome to kiwix library</title>"
|
||||
"</head>"
|
||||
"</html>");
|
||||
EXPECT_EQ("<!DOCTYPE html><head>"
|
||||
"<title>Welcome to kiwix library</title>"
|
||||
"<link type=\"root\" href=\"/ROOT\">"
|
||||
"</head>"
|
||||
"</html>", zfs.GET("/ROOT/")->body);
|
||||
}
|
||||
@@ -131,6 +209,15 @@ 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 ) {
|
||||
@@ -172,11 +259,11 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
|
||||
const std::vector<UrlAndExpectedResult> testData{
|
||||
{
|
||||
/* url */ "/ROOT/",
|
||||
R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=56e818cd"
|
||||
R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=0f9ba34e"
|
||||
<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">
|
||||
<link rel="manifest" href="/ROOT/skin/favicon/site.webmanifest?cacheid=bc396efb">
|
||||
<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">
|
||||
@@ -184,7 +271,12 @@ R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=56e818cd"
|
||||
src: url("/ROOT/skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
|
||||
<script src="/ROOT/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
|
||||
<script src="/ROOT/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
|
||||
<script type="text/javascript" src="/ROOT/skin/index.js?cacheid=76440e7a" defer></script>
|
||||
<script type="text/javascript" src="/ROOT/skin/index.js?cacheid=2f5a81ac" defer></script>
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
{
|
||||
/* url */ "/ROOT/skin/index.css",
|
||||
R"EXPECTEDRESULT( background-image: url('../skin/search-icon.svg?cacheid=b10ae7ed');
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
{
|
||||
@@ -196,24 +288,25 @@ R"EXPECTEDRESULT( <img src="../skin/download.png?
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
{
|
||||
/* url */ "/ROOT/content/zimfile/A/index",
|
||||
R"EXPECTEDRESULT(<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/taskbar.css?cacheid=26082885" rel="Stylesheet" />
|
||||
<link type="text/css" href="/ROOT/skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="/ROOT/skin/taskbar.js?cacheid=1aec4a68" defer></script>
|
||||
<script type="text/javascript" src="/ROOT/skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||
<label for="kiwix_button_show_toggle"><img src="/ROOT/skin/caret.png?cacheid=22b942b4" alt=""></label>
|
||||
/* url */ "/ROOT/viewer",
|
||||
R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=216d6b5d" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="./skin/viewer.js?cacheid=51e745c2" defer></script>
|
||||
<script type="text/javascript" src="./skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||
const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032";
|
||||
<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"
|
||||
},
|
||||
{
|
||||
/* url */ "/ROOT/content/zimfile/A/index",
|
||||
""
|
||||
},
|
||||
{
|
||||
// Searching in a ZIM file without a full-text index returns
|
||||
// a page rendered from static/templates/no_search_result_html
|
||||
/* url */ "/ROOT/search?content=poor&pattern=whatever",
|
||||
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT/skin/search_results.css?cacheid=76d39c84" rel="Stylesheet" />
|
||||
<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/taskbar.css?cacheid=26082885" rel="Stylesheet" />
|
||||
<link type="text/css" href="/ROOT/skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="/ROOT/skin/taskbar.js?cacheid=1aec4a68" defer></script>
|
||||
<script type="text/javascript" src="/ROOT/skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||
<label for="kiwix_button_show_toggle"><img src="/ROOT/skin/caret.png?cacheid=22b942b4" alt=""></label>
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
};
|
||||
@@ -252,6 +345,7 @@ 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",
|
||||
@@ -310,6 +404,11 @@ 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
|
||||
@@ -429,13 +528,8 @@ public:
|
||||
std::string expectedResponse() const;
|
||||
|
||||
private:
|
||||
bool isTranslatedVersion() const;
|
||||
virtual std::string pageTitle() const;
|
||||
std::string pageCssLink() const;
|
||||
std::string hiddenBookNameInput() const;
|
||||
std::string searchPatternInput() const;
|
||||
std::string taskbarLinks() const;
|
||||
std::string goToWelcomePageText() const;
|
||||
};
|
||||
|
||||
std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||
@@ -451,40 +545,8 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||
)FRAG",
|
||||
|
||||
R"FRAG(
|
||||
<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/taskbar.css?cacheid=26082885" rel="Stylesheet" />
|
||||
<link type="text/css" href="/ROOT/skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="/ROOT/skin/taskbar.js?cacheid=1aec4a68" defer></script>
|
||||
<script type="text/javascript" src="/ROOT/skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||
</head>
|
||||
<body><span class="kiwix">
|
||||
<span id="kiwixtoolbar" class="ui-widget-header">
|
||||
<div class="kiwix_centered">
|
||||
<div class="kiwix_searchform">
|
||||
<form class="kiwixsearch" method="GET" action="/ROOT/search" id="kiwixsearchform">
|
||||
)FRAG",
|
||||
|
||||
R"FRAG(
|
||||
<label for="kiwixsearchbox">🔍</label>
|
||||
)FRAG",
|
||||
|
||||
R"FRAG( </form>
|
||||
</div>
|
||||
<input type="checkbox" id="kiwix_button_show_toggle">
|
||||
<label for="kiwix_button_show_toggle"><img src="/ROOT/skin/caret.png?cacheid=22b942b4" alt=""></label>
|
||||
<div class="kiwix_button_cont">
|
||||
<a id="kiwix_serve_taskbar_library_button" title=")FRAG",
|
||||
|
||||
R"FRAG(" aria-label=")FRAG",
|
||||
|
||||
R"FRAG(" href="/ROOT/"><button>🏠</button></a>
|
||||
)FRAG",
|
||||
|
||||
R"FRAG(
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
)FRAG",
|
||||
</head>
|
||||
<body>)FRAG",
|
||||
|
||||
R"FRAG( </body>
|
||||
</html>
|
||||
@@ -496,18 +558,8 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||
+ frag[1]
|
||||
+ pageCssLink()
|
||||
+ frag[2]
|
||||
+ hiddenBookNameInput()
|
||||
+ frag[3]
|
||||
+ searchPatternInput()
|
||||
+ frag[4]
|
||||
+ goToWelcomePageText()
|
||||
+ frag[5]
|
||||
+ goToWelcomePageText()
|
||||
+ frag[6]
|
||||
+ taskbarLinks()
|
||||
+ frag[7]
|
||||
+ expectedBody
|
||||
+ frag[8];
|
||||
+ frag[3];
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::pageTitle() const
|
||||
@@ -527,71 +579,6 @@ std::string TestContentIn404HtmlResponse::pageCssLink() const
|
||||
+ R"(" rel="Stylesheet" />)";
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::hiddenBookNameInput() const
|
||||
{
|
||||
return bookName.empty()
|
||||
? ""
|
||||
: R"(<input type="hidden" name="content" value=")" + bookName + R"(" />)";
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::searchPatternInput() const
|
||||
{
|
||||
const std::string searchboxTooltip = isTranslatedVersion()
|
||||
? "Որոնել '" + bookTitle + "'֊ում"
|
||||
: "Search '" + bookTitle + "'";
|
||||
return R"( <input autocomplete="off" id="kiwixsearchbox" name="pattern" type="text" size="50" title=")"
|
||||
+ searchboxTooltip
|
||||
+ R"(" aria-label=")"
|
||||
+ searchboxTooltip
|
||||
+ R"(">
|
||||
)";
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::taskbarLinks() const
|
||||
{
|
||||
if ( bookName.empty() )
|
||||
return "";
|
||||
|
||||
const auto goToMainPageOfBook = isTranslatedVersion()
|
||||
? "Դեպի '" + bookTitle + "'֊ի գլխավոր էջը"
|
||||
: "Go to the main page of '" + bookTitle + "'";
|
||||
|
||||
const std::string goToRandomPage = isTranslatedVersion()
|
||||
? "Բացել պատահական էջ"
|
||||
: "Go to a randomly selected page";
|
||||
|
||||
return R"(<a id="kiwix_serve_taskbar_home_button" title=")"
|
||||
+ goToMainPageOfBook
|
||||
+ R"(" aria-label=")"
|
||||
+ goToMainPageOfBook
|
||||
+ R"(" href="/ROOT/)"
|
||||
+ bookName
|
||||
+ R"(/"><button>)"
|
||||
+ bookTitle
|
||||
+ R"(</button></a>
|
||||
<a id="kiwix_serve_taskbar_random_button" title=")"
|
||||
+ goToRandomPage
|
||||
+ R"(" aria-label=")"
|
||||
+ goToRandomPage
|
||||
+ R"("
|
||||
href="/ROOT/random?content=)"
|
||||
+ bookName
|
||||
+ R"("><button>🎲</button></a>)";
|
||||
}
|
||||
|
||||
bool TestContentIn404HtmlResponse::isTranslatedVersion() const
|
||||
{
|
||||
return url.find("userlang=hy") != std::string::npos;
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::goToWelcomePageText() const
|
||||
{
|
||||
return isTranslatedVersion()
|
||||
? "Գրադարանի էջ"
|
||||
: "Go to welcome page";
|
||||
}
|
||||
|
||||
|
||||
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
|
||||
{
|
||||
public:
|
||||
@@ -624,12 +611,12 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/random?content=non-existent-book&userlang=hy",
|
||||
expected_page_title=="Սխալ հասցե" &&
|
||||
{ /* url */ "/ROOT/random?content=non-existent-book&userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
expected_body==R"(
|
||||
<h1>Սխալ հասցե</h1>
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
Գիրքը բացակայում է՝ non-existent-book
|
||||
[I18N TESTING] No such book: non-existent-book. Sorry.
|
||||
</p>
|
||||
)" },
|
||||
|
||||
@@ -649,12 +636,12 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/catalog/?userlang=hy",
|
||||
expected_page_title=="Սխալ հասցե" &&
|
||||
{ /* url */ "/ROOT/catalog/?userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
expected_body==R"(
|
||||
<h1>Սխալ հասցե</h1>
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
Սխալ հասցե՝ /ROOT/catalog/
|
||||
[I18N TESTING] URL not found: /ROOT/catalog/
|
||||
</p>
|
||||
)" },
|
||||
|
||||
@@ -666,12 +653,12 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/catalog/invalid_endpoint?userlang=hy",
|
||||
expected_page_title=="Սխալ հասցե" &&
|
||||
{ /* url */ "/ROOT/catalog/invalid_endpoint?userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
expected_body==R"(
|
||||
<h1>Սխալ հասցե</h1>
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
Սխալ հասցե՝ /ROOT/catalog/invalid_endpoint
|
||||
[I18N TESTING] URL not found: /ROOT/catalog/invalid_endpoint
|
||||
</p>
|
||||
)" },
|
||||
|
||||
@@ -723,17 +710,17 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT/content/zimfile/invalid-article?userlang=hy",
|
||||
expected_page_title=="Սխալ հասցե" &&
|
||||
{ /* url */ "/ROOT/content/zimfile/invalid-article?userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
expected_body==R"(
|
||||
<h1>Սխալ հասցե</h1>
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
Սխալ հասցե՝ /ROOT/content/zimfile/invalid-article
|
||||
[I18N TESTING] URL not found: /ROOT/content/zimfile/invalid-article
|
||||
</p>
|
||||
<p>
|
||||
Որոնել <a href="/ROOT/search?content=zimfile&pattern=invalid-article">invalid-article</a>
|
||||
[I18N TESTING] Make a full text search for <a href="/ROOT/search?content=zimfile&pattern=invalid-article">invalid-article</a>
|
||||
</p>
|
||||
)" },
|
||||
|
||||
@@ -852,7 +839,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.
|
||||
@@ -909,21 +896,21 @@ TEST_F(ServerTest, HttpXmlError)
|
||||
/* HTTP status code */ 400,
|
||||
/* expected response XML */ R"(
|
||||
<error>Invalid request</error>
|
||||
<detail>The requested URL "/ROOT/search?content=zimfile&format=xml" is not a valid request.</detail>
|
||||
<detail>The requested URL "/ROOT/search?format=xml&content=zimfile" 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?content=non-existing-book&format=xml&pattern=asdfqwerty" is not a valid request.</detail>
|
||||
<detail>The requested URL "/ROOT/search?format=xml&content=non-existing-book&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?content=non-existing-book&format=xml&pattern=a"<script foo>" is not a valid request.</detail>
|
||||
<detail>The requested URL "/ROOT/search?format=xml&content=non-existing-book&pattern=a"<script foo>" 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
|
||||
@@ -932,7 +919,7 @@ TEST_F(ServerTest, HttpXmlError)
|
||||
/* HTTP status code */ 400,
|
||||
/* expected response XML */ R"(
|
||||
<error>Invalid request</error>
|
||||
<detail>The requested URL "/ROOT/search?books.filter.lang=eng&format=xml&pattern=" is not a valid request.</detail>
|
||||
<detail>The requested URL "/ROOT/search?format=xml&books.filter.lang=eng&pattern" is not a valid request.</detail>
|
||||
<detail>No query provided.</detail>
|
||||
)" },
|
||||
{ /* url */ "/ROOT/search?format=xml&pattern=foo",
|
||||
@@ -1014,9 +1001,9 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=hy",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=test",
|
||||
/*Accept-Language:*/ "",
|
||||
/* expected <h1> */ "Սխալ հասցե"
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
@@ -1025,13 +1012,13 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
},
|
||||
{
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "hy",
|
||||
/* expected <h1> */ "Սխալ հասցե"
|
||||
/*Accept-Language:*/ "test",
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
// userlang query parameter takes precedence over Accept-Language
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=en",
|
||||
/*Accept-Language:*/ "hy",
|
||||
/*Accept-Language:*/ "test",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
@@ -1039,7 +1026,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:*/ "hy;q=0.9, en;q=0.2",
|
||||
/*Accept-Language:*/ "test;q=0.9, en;q=0.2",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
};
|
||||
@@ -1064,6 +1051,8 @@ 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)
|
||||
@@ -1107,6 +1096,8 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,11 +1130,13 @@ TEST_F(ServerTest, RawEntry)
|
||||
EXPECT_EQ(200, p->status);
|
||||
EXPECT_EQ(std::string(p->body), std::string(entry.getItem(true).getData()));
|
||||
|
||||
/* Now normal content is not decorated in any way, either
|
||||
// ... but the "normal" content is not
|
||||
p = zfs1_->GET("/ROOT/content/zimfile/A/Ray_Charles");
|
||||
EXPECT_EQ(200, p->status);
|
||||
EXPECT_NE(std::string(p->body), std::string(entry.getItem(true).getData()));
|
||||
EXPECT_TRUE(p->body.find("taskbar") != std::string::npos);
|
||||
EXPECT_TRUE(p->body.find("<link type=\"root\" href=\"/ROOT\">") != std::string::npos);
|
||||
*/
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, HeadMethodIsSupported)
|
||||
@@ -1169,12 +1162,45 @@ 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")));
|
||||
}
|
||||
}
|
||||
@@ -1198,21 +1224,32 @@ TEST_F(ServerTest, ETagIsTheSameAcrossHeadAndGet)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETags)
|
||||
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETagsForDynamicContent)
|
||||
{
|
||||
ZimFileServer zfs2(SERVER_PORT + 1, /*withTaskbar=*/true, ZIMFILES);
|
||||
ZimFileServer zfs2(SERVER_PORT + 1, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES);
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
if ( !res.etag_expected ) continue;
|
||||
if ( res.kind != DYNAMIC_CONTENT ) 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"} } );
|
||||
@@ -1225,7 +1262,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"} } );
|
||||
@@ -1270,7 +1307,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} });
|
||||
@@ -1297,8 +1334,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);
|
||||
EXPECT_EQ(200, g2->status);
|
||||
EXPECT_EQ(200, h->status) << res;
|
||||
EXPECT_EQ(200, g2->status) << res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1505,11 +1542,11 @@ R"EXPECTEDRESPONSE([
|
||||
]
|
||||
)EXPECTEDRESPONSE"
|
||||
},
|
||||
{ /* url: */ "/ROOT/suggest?content=zimfile&term=abracadabra&userlang=hy",
|
||||
{ /* url: */ "/ROOT/suggest?content=zimfile&term=abracadabra&userlang=test",
|
||||
R"EXPECTEDRESPONSE([
|
||||
{
|
||||
"value" : "abracadabra ",
|
||||
"label" : "որոնել 'abracadabra'...",
|
||||
"label" : "[I18N TESTING] cOnTaInInG 'abracadabra'...",
|
||||
"kind" : "pattern"
|
||||
//EOLWHITESPACEMARKER
|
||||
}
|
||||
@@ -1591,3 +1628,54 @@ TEST_F(ServerTest, suggestions_in_range)
|
||||
ASSERT_EQ(currCount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, viewerSettings)
|
||||
{
|
||||
const auto JS_CONTENT_TYPE = "application/javascript; charset=utf-8";
|
||||
{
|
||||
resetServer(ZimFileServer::NO_TASKBAR_NO_LINK_BLOCKING);
|
||||
const auto r = zfs1_->GET("/ROOT/viewer_settings.js");
|
||||
ASSERT_EQ(r->status, 200);
|
||||
ASSERT_EQ(getHeaderValue(r->headers, "Content-Type"), JS_CONTENT_TYPE);
|
||||
ASSERT_EQ(r->body,
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: false,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: false
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
{
|
||||
resetServer(ZimFileServer::BLOCK_EXTERNAL_LINKS);
|
||||
ASSERT_EQ(zfs1_->GET("/ROOT/viewer_settings.js")->body,
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: false,
|
||||
linkBlockingEnabled: true,
|
||||
libraryButtonEnabled: false
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
{
|
||||
resetServer(ZimFileServer::WITH_TASKBAR);
|
||||
ASSERT_EQ(zfs1_->GET("/ROOT/viewer_settings.js")->body,
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: true,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: false
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
{
|
||||
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
|
||||
ASSERT_EQ(zfs1_->GET("/ROOT/viewer_settings.js")->body,
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: true,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: true
|
||||
}
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#define SERVER_PORT 8101
|
||||
#include "server_testing_tools.h"
|
||||
|
||||
|
||||
std::string makeSearchResultsHtml(const std::string& pattern,
|
||||
const std::string& header,
|
||||
const std::string& results,
|
||||
@@ -112,7 +113,7 @@ std::string makeSearchResultsHtml(const std::string& pattern,
|
||||
|
||||
</style>
|
||||
<title>Search: %PATTERN%</title>
|
||||
<link type="root" href="/ROOT"></head>
|
||||
</head>
|
||||
<body bgcolor="white">
|
||||
<div class="header">
|
||||
%HEADER%
|
||||
@@ -555,7 +556,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 ServerTest.searchResults test-case
|
||||
// of the ServerSearchTest.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
|
||||
@@ -916,7 +917,7 @@ struct TestData
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ServerTest, searchResults)
|
||||
TEST(ServerSearchTest, searchResults)
|
||||
{
|
||||
const TestData testData[] = {
|
||||
{
|
||||
@@ -1340,14 +1341,12 @@ TEST_F(ServerTest, 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 */ 1,
|
||||
/* totalResultCount */ 2,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {
|
||||
SEARCH_RESULT(
|
||||
@@ -1357,6 +1356,14 @@ TEST_F(ServerTest, 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 */ {}
|
||||
},
|
||||
@@ -1451,18 +1458,86 @@ TEST_F(ServerTest, 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 = taskbarlessZimFileServer().GET(htmlSearchUrl.c_str());
|
||||
const auto htmlRes = zfs.GET(htmlSearchUrl.c_str());
|
||||
EXPECT_EQ(htmlRes->status, 200);
|
||||
t.checkHtml(htmlRes->body);
|
||||
|
||||
const std::string xmlSearchUrl = t.xmlSearchUrl();
|
||||
const auto xmlRes1 = zfs1_->GET(xmlSearchUrl.c_str());
|
||||
const auto xmlRes2 = taskbarlessZimFileServer().GET(xmlSearchUrl.c_str());
|
||||
EXPECT_EQ(xmlRes1->status, 200);
|
||||
EXPECT_EQ(xmlRes2->status, 200);
|
||||
EXPECT_EQ(xmlRes1->body, xmlRes2->body);
|
||||
t.checkXml(xmlRes1->body);
|
||||
const auto xmlRes = zfs.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,24 @@ public: // types
|
||||
typedef std::shared_ptr<httplib::Response> Response;
|
||||
typedef std::vector<std::string> FilePathCollection;
|
||||
|
||||
enum Options
|
||||
{
|
||||
NO_TASKBAR_NO_LINK_BLOCKING = 0,
|
||||
|
||||
WITH_TASKBAR = 1 << 1,
|
||||
WITH_LIBRARY_BUTTON = 1 << 2,
|
||||
BLOCK_EXTERNAL_LINKS = 1 << 3,
|
||||
NO_NAME_MAPPER = 1 << 4,
|
||||
|
||||
WITH_TASKBAR_AND_LIBRARY_BUTTON = WITH_TASKBAR | WITH_LIBRARY_BUTTON,
|
||||
|
||||
DEFAULT_OPTIONS = WITH_TASKBAR | WITH_LIBRARY_BUTTON
|
||||
};
|
||||
|
||||
public: // functions
|
||||
ZimFileServer(int serverPort, std::string libraryFilePath);
|
||||
ZimFileServer(int serverPort, Options options, std::string libraryFilePath);
|
||||
ZimFileServer(int serverPort,
|
||||
bool withTaskbar,
|
||||
Options options,
|
||||
const FilePathCollection& zimpaths,
|
||||
std::string indexTemplateString = "");
|
||||
~ZimFileServer();
|
||||
@@ -78,14 +92,15 @@ private:
|
||||
private: // data
|
||||
kiwix::Library library;
|
||||
kiwix::Manager manager;
|
||||
std::unique_ptr<kiwix::HumanReadableNameMapper> nameMapper;
|
||||
std::unique_ptr<kiwix::NameMapper> nameMapper;
|
||||
std::unique_ptr<kiwix::Server> server;
|
||||
std::unique_ptr<httplib::Client> client;
|
||||
const bool withTaskbar = true;
|
||||
const Options options = DEFAULT_OPTIONS;
|
||||
};
|
||||
|
||||
ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
|
||||
ZimFileServer::ZimFileServer(int serverPort, Options _options, std::string libraryFilePath)
|
||||
: manager(&this->library)
|
||||
, options(_options)
|
||||
{
|
||||
if ( kiwix::isRelativePath(libraryFilePath) )
|
||||
libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath);
|
||||
@@ -94,11 +109,11 @@ ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
|
||||
}
|
||||
|
||||
ZimFileServer::ZimFileServer(int serverPort,
|
||||
bool _withTaskbar,
|
||||
Options _options,
|
||||
const FilePathCollection& zimpaths,
|
||||
std::string indexTemplateString)
|
||||
: manager(&this->library)
|
||||
, withTaskbar(_withTaskbar)
|
||||
, options(_options)
|
||||
{
|
||||
for ( const auto& zimpath : zimpaths ) {
|
||||
if (!manager.addBookFromPath(zimpath, zimpath, "", false))
|
||||
@@ -110,14 +125,19 @@ ZimFileServer::ZimFileServer(int serverPort,
|
||||
void ZimFileServer::run(int serverPort, std::string indexTemplateString)
|
||||
{
|
||||
const std::string address = "127.0.0.1";
|
||||
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
|
||||
if (options & NO_NAME_MAPPER) {
|
||||
nameMapper.reset(new kiwix::IdNameMapper());
|
||||
} else {
|
||||
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
|
||||
}
|
||||
server.reset(new kiwix::Server(&library, nameMapper.get()));
|
||||
server->setRoot("ROOT");
|
||||
server->setAddress(address);
|
||||
server->setPort(serverPort);
|
||||
server->setNbThreads(2);
|
||||
server->setVerbose(false);
|
||||
server->setTaskbar(withTaskbar, withTaskbar);
|
||||
server->setTaskbar(options & WITH_TASKBAR, options & WITH_LIBRARY_BUTTON);
|
||||
server->setBlockExternalLinks(options & BLOCK_EXTERNAL_LINKS);
|
||||
server->setMultiZimSearchLimit(3);
|
||||
if (!indexTemplateString.empty()) {
|
||||
server->setIndexTemplateString(indexTemplateString);
|
||||
@@ -136,9 +156,6 @@ ZimFileServer::~ZimFileServer()
|
||||
|
||||
class ServerTest : public ::testing::Test
|
||||
{
|
||||
private:
|
||||
std::unique_ptr<ZimFileServer> taskbarlessZfs_;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<ZimFileServer> zfs1_;
|
||||
|
||||
@@ -151,19 +168,15 @@ protected:
|
||||
|
||||
protected:
|
||||
void SetUp() override {
|
||||
zfs1_.reset(new ZimFileServer(SERVER_PORT, /*withTaskbar=*/true, ZIMFILES));
|
||||
resetServer(ZimFileServer::DEFAULT_OPTIONS);
|
||||
}
|
||||
|
||||
ZimFileServer& taskbarlessZimFileServer()
|
||||
{
|
||||
if ( ! taskbarlessZfs_ ) {
|
||||
taskbarlessZfs_.reset(new ZimFileServer(SERVER_PORT+1, /*withTaskbar=*/false, ZIMFILES));
|
||||
}
|
||||
return *taskbarlessZfs_;
|
||||
void resetServer(ZimFileServer::Options options) {
|
||||
zfs1_.reset();
|
||||
zfs1_.reset(new ZimFileServer(SERVER_PORT, options, ZIMFILES));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
zfs1_.reset();
|
||||
taskbarlessZfs_.reset();
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user