mirror of
https://github.com/kiwix/libkiwix.git
synced 2025-12-25 07:28:00 -05:00
Compare commits
193 Commits
widgetEndp
...
robust_uri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd9785fe85 | ||
|
|
b7a019469c | ||
|
|
76dfc03751 | ||
|
|
ca079a72cc | ||
|
|
471c5b89f4 | ||
|
|
3bf8211b70 | ||
|
|
ec81d5904d | ||
|
|
82dcba542a | ||
|
|
63e0d5c7c2 | ||
|
|
772243e832 | ||
|
|
bad13d76b4 | ||
|
|
0bde4d9412 | ||
|
|
239b108fa7 | ||
|
|
c5ccbd37e2 | ||
|
|
822fb3748a | ||
|
|
aa2e443eb8 | ||
|
|
82d477009d | ||
|
|
e49081da80 | ||
|
|
07c7d3931d | ||
|
|
cf59a93cf1 | ||
|
|
e35e7585e0 | ||
|
|
fcb97c3c06 | ||
|
|
0edee4d066 | ||
|
|
b9937e6859 | ||
|
|
59012c50b4 | ||
|
|
7a98878273 | ||
|
|
8eb527389e | ||
|
|
78b2c1a273 | ||
|
|
497c0700b5 | ||
|
|
bac12010aa | ||
|
|
dad33a850c | ||
|
|
0968fc98ee | ||
|
|
ff44d88f21 | ||
|
|
1e7baee9d7 | ||
|
|
d9342acf5b | ||
|
|
b3f1ab6579 | ||
|
|
f5c9b2404a | ||
|
|
8b1fe21e4e | ||
|
|
815c59ff6d | ||
|
|
90318dfb6b | ||
|
|
f3d2f474a7 | ||
|
|
12140098e6 | ||
|
|
c7d8081e9a | ||
|
|
a10067e6b6 | ||
|
|
28e9fb48b6 | ||
|
|
634f3fcf14 | ||
|
|
88597e1834 | ||
|
|
69b3e1f8a7 | ||
|
|
669d8898ac | ||
|
|
14f0f79061 | ||
|
|
600ff07986 | ||
|
|
1d74b5e311 | ||
|
|
c0fe6f4aee | ||
|
|
aa7053bbe8 | ||
|
|
99f24eb598 | ||
|
|
6790a144a1 | ||
|
|
cd3d2110d9 | ||
|
|
b404241d0b | ||
|
|
2d42d6dc60 | ||
|
|
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 |
27
.github/move.yml
vendored
27
.github/move.yml
vendored
@@ -1,27 +0,0 @@
|
||||
# Configuration for Move Issues - https://github.com/dessant/move-issues
|
||||
|
||||
# Delete the command comment when it contains no other content
|
||||
deleteCommand: true
|
||||
|
||||
# Close the source issue after moving
|
||||
closeSourceIssue: true
|
||||
|
||||
# Lock the source issue after moving
|
||||
lockSourceIssue: false
|
||||
|
||||
# Mention issue and comment authors
|
||||
mentionAuthors: true
|
||||
|
||||
# Preserve mentions in the issue content
|
||||
keepContentMentions: true
|
||||
|
||||
# Move labels that also exist on the target repository
|
||||
moveLabels: true
|
||||
|
||||
# Set custom aliases for targets
|
||||
# aliases:
|
||||
# r: repo
|
||||
# or: owner/repo
|
||||
|
||||
# Repository to extend settings from
|
||||
# _extends: repo
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -3,7 +3,7 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -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
|
||||
|
||||
22
.github/workflows/package.yml
vendored
22
.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 }}
|
||||
@@ -73,8 +73,8 @@ jobs:
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
name: Upload dev package
|
||||
# Only upload on pushes to master
|
||||
if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' && startswith(matrix.distro, 'ubuntu-')
|
||||
# Only upload on pushes to git default branch
|
||||
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
|
||||
with:
|
||||
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
|
||||
repository: ppa:kiwixteam/dev
|
||||
|
||||
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.
|
||||
|
||||
77
README.md
77
README.md
@@ -7,10 +7,10 @@ GNU/Linux, macOS, Android, iOS, ...).
|
||||
|
||||
[](https://download.kiwix.org/release/libkiwix/)
|
||||
[](https://github.com/kiwix/libkiwix/wiki/Repology)
|
||||
[](https://github.com/kiwix/libkiwix/actions?query=branch%3Amaster)
|
||||
[](https://github.com/kiwix/libkiwix/actions?query=branch%3Amain)
|
||||
[](https://libkiwix.readthedocs.org/en/latest/?badge=latest)
|
||||
[](https://www.codefactor.io/repository/github/kiwix/libkiwix)
|
||||
[](https://codecov.io/gh/kiwix/libkiwix)
|
||||
[](https://codecov.io/gh/kiwix/libkiwix)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Disclaimer
|
||||
@@ -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
|
||||
-------
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ Welcome to libkiwix's documentation!
|
||||
|
||||
usage
|
||||
api/ref_api
|
||||
widget
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
Kiwix serve widget
|
||||
====================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
The kiwix-serve widget provides an easy to embed way to show the `kiwix-serve` homepage.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
To use the widget, simply add an iframe with its `src` attribute set to the `widget` endpoint.
|
||||
Example HTML Page ::
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Widget Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="http://192.168.18.8:8080/widget?disabledesc&disablefilter&disabledownload" width=1000 height=1000></iframe>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
This creates an iframe with the kiwix-serve homepage contents.
|
||||
|
||||
Arguments are explained below.
|
||||
|
||||
Possible Arguments
|
||||
-------------------
|
||||
|
||||
Currently, the following arguments are supported.
|
||||
|
||||
disabledesc (value = N/A)
|
||||
Disables the description part of a tile.
|
||||
|
||||
disablefilter (value = N/A)
|
||||
Disables the search filters: language, category, tag and search function.
|
||||
|
||||
disableclick (value = N/A)
|
||||
Disables clicking the book to open it for reading.
|
||||
|
||||
disabledownload (value = N/A)
|
||||
Disables the download button (if avaialable at all) on the tile.
|
||||
|
||||
|
||||
Custom CSS and JS
|
||||
-----------------
|
||||
|
||||
You can add your custom CSS rules and Javascript code to the widget.
|
||||
|
||||
To do that, use the following code as template::
|
||||
|
||||
<iframe id="receiver" src="http://192.168.18.8:8080/widget?disabledesc=&disablefilter=&disabledownload=" width="1000" height="1000">
|
||||
<p>Your browser does not support iframes.</p>
|
||||
</iframe>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
var receiver = document.getElementById('receiver').contentWindow;
|
||||
function sendMessage() {
|
||||
let msg = {
|
||||
css: `
|
||||
.book__header {
|
||||
color:red;
|
||||
}`,
|
||||
js: `
|
||||
function widgetTest() {
|
||||
console.log("Testing widget");
|
||||
}
|
||||
widgetTest();
|
||||
`
|
||||
}
|
||||
receiver.postMessage(msg, 'http://192.168.18.8:8080/widget');
|
||||
}
|
||||
sendMessage();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
The CSS/JS fields are optional, you may send both or only one.
|
||||
|
||||
@@ -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)},
|
||||
{"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{
|
||||
@@ -205,7 +216,7 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const
|
||||
{"endpoint_root", endpointRoot},
|
||||
{"feed_id", gen_uuid(libraryId + endpoint + "?" + query)},
|
||||
{"filter", onlyAsNonEmptyMustacheValue(query)},
|
||||
{"query", query.empty() ? "" : "?" + urlEncode(query)},
|
||||
{"query", query.empty() ? "" : "?" + query},
|
||||
{"totalResults", to_string(m_totalResults)},
|
||||
{"startIndex", to_string(m_startIndex)},
|
||||
{"itemsPerPage", to_string(m_count)},
|
||||
@@ -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
|
||||
@@ -94,7 +94,7 @@ kainjow::mustache::data buildQueryData
|
||||
kainjow::mustache::data query;
|
||||
query.set("pattern", kiwix::encodeDiples(pattern));
|
||||
std::ostringstream ss;
|
||||
ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern, true);
|
||||
ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern);
|
||||
ss << "&" << bookQuery;
|
||||
query.set("unpaginatedQuery", ss.str());
|
||||
auto lang = extractValueFromQuery(bookQuery, "books.filter.lang");
|
||||
@@ -166,14 +166,15 @@ 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++) {
|
||||
kainjow::mustache::data result;
|
||||
std::string zim_id(it.getZimId());
|
||||
const std::string zim_id(it.getZimId());
|
||||
const auto path = mp_nameMapper->getNameForId(zim_id) + "/" + it.getPath();
|
||||
result.set("title", it.getTitle());
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath()));
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(path));
|
||||
result.set("snippet", it.getSnippet());
|
||||
if (mp_library) {
|
||||
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
|
||||
@@ -206,7 +207,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;
|
||||
};
|
||||
|
||||
|
||||
@@ -70,6 +70,14 @@ public: // functions
|
||||
return s;
|
||||
}
|
||||
|
||||
size_t getStringCount(const std::string& lang) const {
|
||||
try {
|
||||
return lang2TableMap.at(lang)->entryCount;
|
||||
} catch(const std::out_of_range&) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private: // functions
|
||||
const I18nStringTable* getStringsFor(const std::string& lang) const {
|
||||
try {
|
||||
@@ -84,13 +92,17 @@ private: // data
|
||||
const I18nStringTable* enStrings;
|
||||
};
|
||||
|
||||
const I18nStringDB& getStringDb()
|
||||
{
|
||||
static const I18nStringDB stringDb;
|
||||
return stringDb;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::string getTranslatedString(const std::string& lang, const std::string& key)
|
||||
{
|
||||
static const I18nStringDB stringDb;
|
||||
|
||||
return stringDb.get(lang, key);
|
||||
return getStringDb().get(lang, key);
|
||||
}
|
||||
|
||||
namespace i18n
|
||||
@@ -111,4 +123,70 @@ std::string ParameterizedMessage::getText(const std::string& lang) const
|
||||
return i18n::expandParameterizedString(lang, msgId, params);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
LangPreference parseSingleLanguagePreference(const std::string& s)
|
||||
{
|
||||
const size_t langStart = s.find_first_not_of(" \t\n");
|
||||
if ( langStart == std::string::npos ) {
|
||||
return {"", 0};
|
||||
}
|
||||
|
||||
const size_t langEnd = s.find(';', langStart);
|
||||
if ( langEnd == std::string::npos ) {
|
||||
return {s.substr(langStart), 1};
|
||||
}
|
||||
|
||||
const std::string lang = s.substr(langStart, langEnd - langStart);
|
||||
// We don't care about langEnd == langStart which will result in an empty
|
||||
// language name - it will be dismissed by parseUserLanguagePreferences()
|
||||
|
||||
float q = 1.0;
|
||||
int nCharsScanned;
|
||||
if ( 1 == sscanf(s.c_str() + langEnd + 1, "q=%f%n", &q, &nCharsScanned)
|
||||
&& langEnd + 1 + nCharsScanned == s.size() ) {
|
||||
return {lang, q};
|
||||
}
|
||||
|
||||
return {"", 0};
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
UserLangPreferences parseUserLanguagePreferences(const std::string& s)
|
||||
{
|
||||
UserLangPreferences result;
|
||||
std::istringstream iss(s);
|
||||
std::string singleLangPrefStr;
|
||||
while ( std::getline(iss, singleLangPrefStr, ',') )
|
||||
{
|
||||
const auto langPref = parseSingleLanguagePreference(singleLangPrefStr);
|
||||
if ( !langPref.lang.empty() && langPref.preference > 0 ) {
|
||||
result.push_back(langPref);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs)
|
||||
{
|
||||
if ( prefs.empty() ) {
|
||||
return "en";
|
||||
}
|
||||
|
||||
std::string bestLangSoFar("en");
|
||||
float bestScoreSoFar = 0;
|
||||
const auto& stringDb = getStringDb();
|
||||
for ( const auto& entry : prefs ) {
|
||||
const float score = entry.preference * stringDb.getStringCount(entry.lang);
|
||||
if ( score > bestScoreSoFar ) {
|
||||
bestScoreSoFar = score;
|
||||
bestLangSoFar = entry.lang;
|
||||
}
|
||||
}
|
||||
return bestLangSoFar;
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
@@ -89,6 +89,18 @@ private: // data
|
||||
const Parameters params;
|
||||
};
|
||||
|
||||
struct LangPreference
|
||||
{
|
||||
const std::string lang;
|
||||
const float preference;
|
||||
};
|
||||
|
||||
typedef std::vector<LangPreference> UserLangPreferences;
|
||||
|
||||
UserLangPreferences parseUserLanguagePreferences(const std::string& s);
|
||||
|
||||
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIX_SERVER_I18N
|
||||
|
||||
@@ -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
|
||||
@@ -227,7 +245,7 @@ std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const Req
|
||||
auto bookName = request.get_argument("content");
|
||||
try {
|
||||
const auto bookIds = Library::BookIdSet{mp_nameMapper->getIdForName(bookName)};
|
||||
const auto queryString = request.get_query([&](const std::string& key){return key == "content";}, true);
|
||||
const auto queryString = request.get_query([&](const std::string& key){return key == "content";});
|
||||
return {queryString, bookIds};
|
||||
} catch (const std::out_of_range&) {
|
||||
throw Error(noSuchBookErrorMsg(bookName));
|
||||
@@ -252,7 +270,7 @@ std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const Req
|
||||
}
|
||||
}
|
||||
const auto bookIds = Library::BookIdSet(id_vec.begin(), id_vec.end());
|
||||
const auto queryString = request.get_query([&](const std::string& key){return key == "books.id";}, true);
|
||||
const auto queryString = request.get_query([&](const std::string& key){return key == "books.id";});
|
||||
return {queryString, bookIds};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
@@ -270,7 +288,7 @@ std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const Req
|
||||
throw Error(noSuchBookErrorMsg(bookName));
|
||||
}
|
||||
}
|
||||
const auto queryString = request.get_query([&](const std::string& key){return key == "books.name";}, true);
|
||||
const auto queryString = request.get_query([&](const std::string& key){return key == "books.name";});
|
||||
return {queryString, bookIds};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
@@ -281,7 +299,7 @@ std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const Req
|
||||
throw Error(nonParameterizedMessage("no-book-found"));
|
||||
}
|
||||
const auto bookIds = Library::BookIdSet(id_vec.begin(), id_vec.end());
|
||||
const auto queryString = request.get_query([&](const std::string& key){return startsWith(key, "books.filter.");}, true);
|
||||
const auto queryString = request.get_query([&](const std::string& key){return startsWith(key, "books.filter.");});
|
||||
return {queryString, bookIds};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -577,9 +607,6 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
if (isEndpointUrl(url, "catch"))
|
||||
return handle_catch(request);
|
||||
|
||||
if (isEndpointUrl(url, "widget"))
|
||||
return handle_widget(request);
|
||||
|
||||
std::string contentUrl = m_root + "/content" + url;
|
||||
const std::string query = request.get_query();
|
||||
if ( ! query.empty() )
|
||||
@@ -603,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()) {
|
||||
@@ -656,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());
|
||||
@@ -671,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)
|
||||
@@ -756,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
|
||||
@@ -832,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()) {
|
||||
@@ -855,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 {
|
||||
@@ -864,16 +913,10 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_widget(const RequestContext& request)
|
||||
{
|
||||
return ContentResponse::build(*this, RESOURCE::templates::widget_html, get_default_data(), "text/html; charset=utf-8", true);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_captured_external(const RequestContext& request)
|
||||
{
|
||||
std::string source = "";
|
||||
@@ -936,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);
|
||||
@@ -960,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);
|
||||
@@ -1015,13 +1055,17 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (archive == nullptr) {
|
||||
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true);
|
||||
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern);
|
||||
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);
|
||||
@@ -1029,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());
|
||||
@@ -1049,11 +1096,10 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
if (m_verbose.load())
|
||||
printf("Failed to find %s\n", urlStr.c_str());
|
||||
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true);
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern);
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern))
|
||||
+ TaskbarInfo(bookName, archive.get());
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,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
|
||||
// ^^^^^ ^ ^
|
||||
@@ -1101,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()) {
|
||||
@@ -1144,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);
|
||||
@@ -143,20 +142,24 @@ class InternalServer {
|
||||
std::unique_ptr<Response> handle_content(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_raw(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_locally_customized_resource(const RequestContext& request);
|
||||
std::unique_ptr<Response> handle_widget(const RequestContext& request);
|
||||
|
||||
std::vector<std::string> search_catalog(const RequestContext& request,
|
||||
kiwix::OPDSDumper& opdsDumper);
|
||||
|
||||
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;
|
||||
@@ -178,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;
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
#include "i18n.h"
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
@@ -66,12 +68,13 @@ fullURL2LocalURL(const std::string& full_url, const std::string& rootLocation)
|
||||
} // unnamed namespace
|
||||
|
||||
RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
std::string rootLocation,
|
||||
std::string _rootLocation,
|
||||
const std::string& _url,
|
||||
const std::string& _method,
|
||||
const std::string& version) :
|
||||
rootLocation(_rootLocation),
|
||||
full_url(_url),
|
||||
url(fullURL2LocalURL(_url, rootLocation)),
|
||||
url(fullURL2LocalURL(_url, _rootLocation)),
|
||||
method(str2RequestMethod(_method)),
|
||||
version(version),
|
||||
requestIndex(s_requestIndex++),
|
||||
@@ -80,6 +83,7 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
{
|
||||
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
|
||||
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
|
||||
MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this);
|
||||
|
||||
try {
|
||||
acceptEncodingGzip =
|
||||
@@ -89,6 +93,8 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
try {
|
||||
byteRange_ = ByteRange::parse(get_header(MHD_HTTP_HEADER_RANGE));
|
||||
} catch (const std::out_of_range&) {}
|
||||
|
||||
userlang = determine_user_language();
|
||||
}
|
||||
|
||||
RequestContext::~RequestContext()
|
||||
@@ -107,6 +113,22 @@ 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 += urlEncode(key);
|
||||
if ( value ) {
|
||||
_this->queryString += "=";
|
||||
_this->queryString += urlEncode(value);
|
||||
}
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
MHD_Result RequestContext::fill_cookie(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->cookies[key] = value == nullptr ? "" : value;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
@@ -172,6 +194,10 @@ std::string RequestContext::get_full_url() const {
|
||||
return full_url;
|
||||
}
|
||||
|
||||
std::string RequestContext::get_root_path() const {
|
||||
return rootLocation.empty() ? "/" : rootLocation;
|
||||
}
|
||||
|
||||
bool RequestContext::is_valid_url() const {
|
||||
return !url.empty();
|
||||
}
|
||||
@@ -190,16 +216,33 @@ std::string RequestContext::get_header(const std::string& name) const {
|
||||
}
|
||||
|
||||
std::string RequestContext::get_user_language() const
|
||||
{
|
||||
return userlang.lang;
|
||||
}
|
||||
|
||||
bool RequestContext::user_language_comes_from_cookie() const
|
||||
{
|
||||
return userlang.selectedBy == UserLanguage::SelectorKind::COOKIE;
|
||||
}
|
||||
|
||||
RequestContext::UserLanguage RequestContext::determine_user_language() const
|
||||
{
|
||||
try {
|
||||
return get_argument("userlang");
|
||||
return {UserLanguage::SelectorKind::QUERY_PARAM, get_argument("userlang")};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
try {
|
||||
return get_header("Accept-Language");
|
||||
return {UserLanguage::SelectorKind::COOKIE, cookies.at("userlang")};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
return "en";
|
||||
try {
|
||||
const std::string acceptLanguage = get_header("Accept-Language");
|
||||
const auto userLangPrefs = parseUserLanguagePreferences(acceptLanguage);
|
||||
const auto lang = selectMostSuitableLanguage(userLangPrefs);
|
||||
return {UserLanguage::SelectorKind::ACCEPT_LANGUAGE_HEADER, lang};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
return {UserLanguage::SelectorKind::DEFAULT, "en"};
|
||||
}
|
||||
|
||||
std::string RequestContext::get_requested_format() const
|
||||
|
||||
@@ -91,22 +91,20 @@ class RequestContext {
|
||||
std::string get_url() const;
|
||||
std::string get_url_part(int part) const;
|
||||
std::string get_full_url() const;
|
||||
std::string get_root_path() 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 {
|
||||
std::string get_query(F filter) const {
|
||||
std::string q;
|
||||
const char* sep = "";
|
||||
auto encode = [=](const std::string& value) { return mustEncode?urlEncode(value, true):value; };
|
||||
for ( const auto& a : arguments ) {
|
||||
if (!filter(a.first)) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& v: a.second) {
|
||||
q += sep + encode(a.first) + '=' + encode(v);
|
||||
q += sep + urlEncode(a.first) + '=' + urlEncode(v);
|
||||
sep = "&";
|
||||
}
|
||||
}
|
||||
@@ -120,7 +118,25 @@ class RequestContext {
|
||||
std::string get_user_language() const;
|
||||
std::string get_requested_format() const;
|
||||
|
||||
bool user_language_comes_from_cookie() const;
|
||||
|
||||
private: // types
|
||||
struct UserLanguage
|
||||
{
|
||||
enum SelectorKind
|
||||
{
|
||||
QUERY_PARAM,
|
||||
COOKIE,
|
||||
ACCEPT_LANGUAGE_HEADER,
|
||||
DEFAULT
|
||||
};
|
||||
|
||||
SelectorKind selectedBy;
|
||||
std::string lang;
|
||||
};
|
||||
|
||||
private: // data
|
||||
std::string rootLocation;
|
||||
std::string full_url;
|
||||
std::string url;
|
||||
RequestMethod method;
|
||||
@@ -132,9 +148,15 @@ class RequestContext {
|
||||
ByteRange byteRange_;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::map<std::string, std::vector<std::string>> arguments;
|
||||
std::map<std::string, std::string> cookies;
|
||||
std::string queryString;
|
||||
UserLanguage userlang;
|
||||
|
||||
private: // functions
|
||||
UserLanguage determine_user_language() const;
|
||||
|
||||
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_argument(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"
|
||||
@@ -64,7 +64,13 @@ bool is_compressible_mime_type(const std::string& mimeType)
|
||||
|| mimeType.find("application/javascript") != std::string::npos
|
||||
|| mimeType.find("application/atom") != std::string::npos
|
||||
|| mimeType.find("application/opensearchdescription") != std::string::npos
|
||||
|| mimeType.find("application/json") != std::string::npos;
|
||||
|| mimeType.find("application/json") != std::string::npos
|
||||
|
||||
// Web fonts
|
||||
|| mimeType.find("application/font-") != std::string::npos
|
||||
|| mimeType.find("application/x-font-") != std::string::npos
|
||||
|| mimeType.find("application/vnd.ms-fontobject") != std::string::npos
|
||||
|| mimeType.find("font/") != std::string::npos;
|
||||
}
|
||||
|
||||
bool compress(std::string &content) {
|
||||
@@ -102,6 +108,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 +126,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 +143,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 +164,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 +257,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 +341,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 +349,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 +359,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 +379,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());
|
||||
@@ -450,6 +387,13 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
|
||||
}
|
||||
|
||||
if ( ! request.user_language_comes_from_cookie() ) {
|
||||
const std::string cookie = "userlang=" + request.get_user_language()
|
||||
+ ";Path=" + request.get_root_path()
|
||||
+ ";Max-Age=31536000";
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_SET_COOKIE, cookie.c_str());
|
||||
}
|
||||
|
||||
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
|
||||
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
|
||||
|
||||
@@ -461,24 +405,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 +417,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 +430,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 +442,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);
|
||||
@@ -380,10 +322,76 @@ kainjow::mustache::data kiwix::onlyAsNonEmptyMustacheValue(const std::string& s)
|
||||
std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data)
|
||||
{
|
||||
kainjow::mustache::mustache tmpl(template_str);
|
||||
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
|
||||
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
|
||||
data.set("urlencoded", urlencode);
|
||||
std::stringstream ss;
|
||||
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
|
||||
|
||||
@@ -161,15 +161,14 @@ std::string kiwix::encodeDiples(const std::string& str)
|
||||
return result;
|
||||
}
|
||||
|
||||
/* urlEncode() based on javascript encodeURI() &
|
||||
encodeURIComponent(). Mostly code from rstudio/httpuv (GPLv3) */
|
||||
namespace
|
||||
{
|
||||
|
||||
bool isReservedUrlChar(char c)
|
||||
{
|
||||
switch (c) {
|
||||
case ';':
|
||||
case ',':
|
||||
case '/':
|
||||
case '?':
|
||||
case ':':
|
||||
case '@':
|
||||
@@ -177,22 +176,22 @@ bool isReservedUrlChar(char c)
|
||||
case '=':
|
||||
case '+':
|
||||
case '$':
|
||||
case '#':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool needsEscape(char c, bool encodeReserved)
|
||||
bool isHarmlessUriChar(char c)
|
||||
{
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return false;
|
||||
return true;
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return false;
|
||||
return true;
|
||||
if (c >= '0' && c <= '9')
|
||||
return false;
|
||||
if (isReservedUrlChar(c))
|
||||
return encodeReserved;
|
||||
return true;
|
||||
|
||||
switch (c) {
|
||||
case '-':
|
||||
case '_':
|
||||
@@ -203,8 +202,46 @@ bool needsEscape(char c, bool encodeReserved)
|
||||
case '\'':
|
||||
case '(':
|
||||
case ')':
|
||||
return false;
|
||||
case '/':
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mustBeUriEncodedFor(kiwix::URIComponentKind target, char c)
|
||||
{
|
||||
if (isHarmlessUriChar(c))
|
||||
return false;
|
||||
|
||||
switch (c) {
|
||||
case '/': // There is no reason to encode the path separator in the general
|
||||
// case. It must be encoded only in a path component when its
|
||||
// semantics of a path separator has to be suppressed.
|
||||
return false;
|
||||
|
||||
case '@': // In a relative URL of the form abc@def/xyz (with no / in abc)
|
||||
// a non-encoded @ will make "abc" and "def" to be interpreted as
|
||||
// username and host components, respectively
|
||||
return target == kiwix::URIComponentKind::PATH;
|
||||
|
||||
case ':': // In a relative URL of the form abc:def/xyz (with no / in abc)
|
||||
// a non-encoded : will make "abc" and "def" to be interpreted as
|
||||
// host and port components, respectively
|
||||
return target == kiwix::URIComponentKind::PATH;
|
||||
|
||||
case '?': // A non-encoded '?' acts as a separator between the path
|
||||
// and query components
|
||||
return target == kiwix::URIComponentKind::PATH;
|
||||
|
||||
case '&': return target == kiwix::URIComponentKind::QUERY;
|
||||
case '=': return target == kiwix::URIComponentKind::QUERY;
|
||||
case '+': return target == kiwix::URIComponentKind::QUERY;
|
||||
|
||||
case '#': // A non-encoded '#' in either path or query-component
|
||||
// would mark the beginning of the fragment component
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -230,23 +267,43 @@ int hexToInt(char c) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string kiwix::urlEncode(const std::string& value, bool encodeReserved)
|
||||
} // unnamed namespace
|
||||
|
||||
std::string kiwix::urlEncode(const std::string& value)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << std::hex << std::uppercase;
|
||||
for (std::string::const_iterator it = value.begin();
|
||||
it != value.end();
|
||||
it++) {
|
||||
|
||||
if (!needsEscape(*it, encodeReserved)) {
|
||||
os << *it;
|
||||
for (const char c : value) {
|
||||
if (isHarmlessUriChar(c)) {
|
||||
os << c;
|
||||
} else {
|
||||
os << '%' << std::setw(2) << static_cast<unsigned int>(static_cast<unsigned char>(*it));
|
||||
const unsigned int charVal = static_cast<unsigned char>(c);
|
||||
os << '%' << std::setw(2) << std::setfill('0') << charVal;
|
||||
}
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
std::string uriEncode(URIComponentKind target, const std::string& value)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << std::hex << std::uppercase;
|
||||
for (const char c : value) {
|
||||
if ( mustBeUriEncodedFor(target, c) ) {
|
||||
const unsigned int charVal = static_cast<unsigned char>(c);
|
||||
os << '%' << std::setw(2) << std::setfill('0') << charVal;
|
||||
} else {
|
||||
os << c;
|
||||
}
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
std::string kiwix::urlDecode(const std::string& value, bool component)
|
||||
{
|
||||
std::ostringstream os;
|
||||
@@ -267,15 +324,15 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
|
||||
int iHi = hexToInt(hi);
|
||||
int iLo = hexToInt(lo);
|
||||
if (iHi < 0 || iLo < 0) {
|
||||
// Invalid escape sequence
|
||||
os << '%' << hi << lo;
|
||||
continue;
|
||||
// Invalid escape sequence
|
||||
os << '%' << hi << lo;
|
||||
continue;
|
||||
}
|
||||
char c = (char)(iHi << 4 | iLo);
|
||||
if (!component && isReservedUrlChar(c)) {
|
||||
os << '%' << hi << lo;
|
||||
os << '%' << hi << lo;
|
||||
} else {
|
||||
os << c;
|
||||
os << c;
|
||||
}
|
||||
} else {
|
||||
os << *it;
|
||||
|
||||
@@ -55,9 +55,22 @@ private:
|
||||
};
|
||||
|
||||
|
||||
std::string urlEncode(const std::string& value, bool encodeReserved = false);
|
||||
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
|
||||
* difference that the slash (/) symbol is NOT encoded. */
|
||||
std::string urlEncode(const std::string& value);
|
||||
std::string urlDecode(const std::string& value, bool component = false);
|
||||
|
||||
// Only URI components that are of interest to libkiwix
|
||||
// are included in the below enumeration type
|
||||
enum class URIComponentKind
|
||||
{
|
||||
PATH,
|
||||
QUERY
|
||||
};
|
||||
|
||||
// Encode 'value' for usage in a URI componenet specified by 'target'
|
||||
std::string uriEncode(URIComponentKind target, const std::string& value);
|
||||
|
||||
std::string join(const std::vector<std::string>& list, const std::string& sep);
|
||||
|
||||
std::string ucAll(const std::string& word);
|
||||
|
||||
33
static/i18n/ar.json
Normal file
33
static/i18n/ar.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Asma",
|
||||
"Ravan",
|
||||
"محمد أحمد عبد الفتاح"
|
||||
]
|
||||
},
|
||||
"name": "الإنجليزية",
|
||||
"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}}\"",
|
||||
"confusion-of-tongues": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة."
|
||||
}
|
||||
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",
|
||||
|
||||
32
static/i18n/sl.json
Normal file
32
static/i18n/sl.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Eleassar"
|
||||
]
|
||||
},
|
||||
"name": "slovenščina",
|
||||
"suggest-full-text-search": "vsebuje »{{{SEARCH_TERMS}}}« ...",
|
||||
"no-such-book": "Ni take knjige: {{BOOK_NAME}}",
|
||||
"too-many-books": "Preveč zahtevanih knjig ({{NB_BOOKS}}), omejitev je {{LIMIT}}",
|
||||
"no-book-found": "Izbirnim merilom ne ustreza nobena knjiga",
|
||||
"url-not-found": "Zahtevanega URL-ja »{{url}}« v tem strežniku ni bilo mogoče najti.",
|
||||
"suggest-search": "Preiščite celotno besedilo za <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Ni bilo mogoče izbrati naključnega članka :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ni veljaven zahtevek za neobdelano vsebino.",
|
||||
"no-value-for-arg": "Argument {{ARGUMENT}} nima določene nobene vrednosti",
|
||||
"no-query": "Poizvedba ni podana.",
|
||||
"raw-entry-not-found": "Ni mogoče najti vnosa {{ENTRY}} vrste {{DATATYPE}}",
|
||||
"400-page-title": "Neveljaven zahtevek",
|
||||
"400-page-heading": "Neveljaven zahtevek",
|
||||
"404-page-title": "Vsebine ni mogoče najti",
|
||||
"404-page-heading": "Ni najdeno",
|
||||
"500-page-title": "Notranja napaka strežnika",
|
||||
"500-page-heading": "Notranja napaka strežnika",
|
||||
"fulltext-search-unavailable": "Iskanje po celotnem besedilu ni na voljo",
|
||||
"no-search-results": "Iskalnik po celotnem besedilu za to vsebino ni na voljo.",
|
||||
"library-button-text": "Pojdite na pozdravno stran",
|
||||
"home-button-text": "Pojdite na glavno stran »{{BOOK_TITLE}}«",
|
||||
"random-page-button-text": "Pojdite na naključno izbrano stran",
|
||||
"searchbox-tooltip": "Poiščite »{{BOOK_TITLE}}«",
|
||||
"confusion-of-tongues": "V iskanju bi bili uporabljeni dve ali več knjig v različnih jezikih, kar lahko pripelje do nejasnih zadetkov."
|
||||
}
|
||||
@@ -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,33 @@
|
||||
{
|
||||
"@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": "內部伺服器錯誤",
|
||||
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋"
|
||||
"fulltext-search-unavailable": "全文搜尋無效",
|
||||
"no-search-results": "全文搜尋引擎不適用此內容。",
|
||||
"library-button-text": "前往歡迎首頁",
|
||||
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
|
||||
"random-page-button-text": "前往隨機選取頁面",
|
||||
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋",
|
||||
"confusion-of-tongues": "搜索裡有加入兩本或更多不同語言的書籍,這可能會導致混淆結果。"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
i18n/ar.json
|
||||
i18n/bn.json
|
||||
i18n/cs.json
|
||||
i18n/de.json
|
||||
i18n/en.json
|
||||
i18n/fr.json
|
||||
i18n/he.json
|
||||
@@ -8,10 +11,14 @@ 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/sl.json
|
||||
i18n/sv.json
|
||||
i18n/test.json
|
||||
i18n/tr.json
|
||||
i18n/zh-hans.json
|
||||
i18n/zh-hant.json
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
resource_files = run_command(res_manager,
|
||||
'--list-all',
|
||||
files('resources_list.txt')
|
||||
files('resources_list.txt'),
|
||||
check: true
|
||||
).stdout().strip().split('\n')
|
||||
|
||||
preprocessed_resources = custom_target('preprocessed_resource_files',
|
||||
@@ -15,7 +16,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@',
|
||||
@@ -33,7 +34,8 @@ lib_resources = custom_target('resources',
|
||||
i18n_resource_files = run_command(find_program('python3'),
|
||||
'-c',
|
||||
'import sys; f=open(sys.argv[1]); print(f.read())',
|
||||
files('i18n_resources_list.txt')
|
||||
files('i18n_resources_list.txt'),
|
||||
check: true
|
||||
).stdout().strip().split('\n')
|
||||
|
||||
i18n_resources = custom_target('i18n_resources',
|
||||
|
||||
@@ -4,41 +4,38 @@ skin/magnet.png
|
||||
skin/download.png
|
||||
skin/hash.png
|
||||
skin/search-icon.svg
|
||||
skin/taskbar.js
|
||||
skin/iso6391To3.js
|
||||
skin/isotope.pkgd.min.js
|
||||
skin/index.js
|
||||
skin/autoComplete.min.js
|
||||
skin/widget.js
|
||||
skin/taskbar.css
|
||||
skin/index.css
|
||||
skin/fonts/Poppins.ttf
|
||||
skin/fonts/Roboto.ttf
|
||||
skin/block_external.js
|
||||
skin/search_results.css
|
||||
skin/blank.html
|
||||
skin/viewer.js
|
||||
viewer.html
|
||||
templates/search_result.html
|
||||
templates/search_result.xml
|
||||
templates/error.html
|
||||
templates/error.xml
|
||||
templates/index.html
|
||||
templates/widget.html
|
||||
templates/suggestion.json
|
||||
templates/head_taskbar.html
|
||||
templates/taskbar_part.html
|
||||
templates/external_blocker_part.html
|
||||
templates/captured_external.html
|
||||
templates/catalog_entries.xml
|
||||
templates/catalog_v2_root.xml
|
||||
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
|
||||
|
||||
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,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const kiwixServe = (function() {
|
||||
(function() {
|
||||
const root = document.querySelector(`link[type='root']`).getAttribute('href');
|
||||
const incrementalLoadingParams = {
|
||||
start: 0,
|
||||
@@ -17,7 +17,6 @@ const kiwixServe = (function() {
|
||||
let params = new URLSearchParams(window.location.search || filters || '');
|
||||
let timer;
|
||||
let languages = {};
|
||||
let allowBookClick = true;
|
||||
|
||||
function queryUrlBuilder() {
|
||||
let url = `${root}/catalog/search?`;
|
||||
@@ -86,7 +85,7 @@ const kiwixServe = (function() {
|
||||
}
|
||||
|
||||
function generateBookHtml(book, sort = false) {
|
||||
let link = book.querySelector('link[type="text/html"]').getAttribute('href');
|
||||
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
|
||||
let iconUrl;
|
||||
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
|
||||
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
|
||||
@@ -111,6 +110,9 @@ const kiwixServe = (function() {
|
||||
} catch {
|
||||
downloadLink = '';
|
||||
}
|
||||
const bookName = link.split('/').pop();
|
||||
const viewerLink = `${root}/viewer#${bookName}`;
|
||||
|
||||
const humanFriendlyZimSize = humanFriendlySize(zimSize);
|
||||
|
||||
const divTag = document.createElement('div');
|
||||
@@ -121,12 +123,9 @@ const kiwixServe = (function() {
|
||||
}
|
||||
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
|
||||
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
|
||||
if (!allowBookClick) {
|
||||
link = "javascript:void(0)";
|
||||
}
|
||||
divTag.innerHTML = `
|
||||
<div class="book__wrapper">
|
||||
<a class="book__link" href="${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">
|
||||
@@ -183,12 +182,12 @@ const kiwixServe = (function() {
|
||||
<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>
|
||||
@@ -207,7 +206,7 @@ const kiwixServe = (function() {
|
||||
<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" />
|
||||
@@ -251,16 +250,14 @@ const kiwixServe = (function() {
|
||||
toggleFooter();
|
||||
}
|
||||
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
|
||||
if (kiwixResultText) {
|
||||
if (results) {
|
||||
let resultText = `${results} books`;
|
||||
if (results === 1) {
|
||||
resultText = `${results} book`;
|
||||
}
|
||||
kiwixResultText.innerHTML = resultText;
|
||||
} else {
|
||||
kiwixResultText.innerHTML = ``;
|
||||
if (results) {
|
||||
let resultText = `${results} books`;
|
||||
if (results === 1) {
|
||||
resultText = `${results} book`;
|
||||
}
|
||||
kiwixResultText.innerHTML = resultText;
|
||||
} else {
|
||||
kiwixResultText.innerHTML = ``;
|
||||
}
|
||||
loader.style.display = 'none';
|
||||
return books;
|
||||
@@ -271,20 +268,16 @@ const kiwixServe = (function() {
|
||||
await fetch(query).then(async (resp) => {
|
||||
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
|
||||
let optionStr = '';
|
||||
const entryList = data.querySelectorAll('entry');
|
||||
const nodeQueryElem = document.querySelector(nodeQuery);
|
||||
if (entryList && nodeQueryElem) {
|
||||
entryList.forEach(entry => {
|
||||
const title = getInnerHtml(entry, 'title');
|
||||
const value = getInnerHtml(entry, valueEntryNode);
|
||||
const hfTitle = humanFriendlyTitle(title);
|
||||
if (valueEntryNode == 'language') {
|
||||
languages[value] = hfTitle;
|
||||
}
|
||||
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
|
||||
});
|
||||
nodeQueryElem.innerHTML += optionStr;
|
||||
}
|
||||
data.querySelectorAll('entry').forEach(entry => {
|
||||
const title = getInnerHtml(entry, 'title');
|
||||
const value = getInnerHtml(entry, valueEntryNode);
|
||||
const hfTitle = humanFriendlyTitle(title);
|
||||
if (valueEntryNode == 'language') {
|
||||
languages[value] = hfTitle;
|
||||
}
|
||||
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
|
||||
});
|
||||
document.querySelector(nodeQuery).innerHTML += optionStr;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,10 +392,6 @@ const kiwixServe = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
function disableBookClick() {
|
||||
allowBookClick = false;
|
||||
}
|
||||
|
||||
function addTagElement(tagValue, resetFilter) {
|
||||
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
|
||||
tagElement.style.display = 'inline-block';
|
||||
@@ -443,15 +432,13 @@ const kiwixServe = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateBookCount(event) {
|
||||
window.addEventListener('resize', (event) => {
|
||||
if (timer) {clearTimeout(timer)}
|
||||
timer = setTimeout(() => {
|
||||
incrementalLoadingParams.count = incrementalLoadingParams.count && viewPortToCount();
|
||||
loadSubset();
|
||||
}, 100, event);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', (event) => updateBookCount(event));
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', loadSubset);
|
||||
|
||||
@@ -491,11 +478,10 @@ const kiwixServe = (function() {
|
||||
const currentLink = window.location.search;
|
||||
const newLink = `?${params.toString()}`;
|
||||
if (currentLink != newLink) {
|
||||
window.history.pushState({}, null, newLink);
|
||||
window.history.pushState({}, null, newLink);
|
||||
}
|
||||
}
|
||||
updateVisibleParams();
|
||||
updateBookCount();
|
||||
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
|
||||
if (!window.location.search) {
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
@@ -508,10 +494,5 @@ const kiwixServe = (function() {
|
||||
}
|
||||
setCookie(filterCookieName, params.toString());
|
||||
}
|
||||
|
||||
return {
|
||||
updateBookCount,
|
||||
disableBookClick
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
});
|
||||
420
static/skin/viewer.js
Normal file
420
static/skin/viewer.js
Normal file
@@ -0,0 +1,420 @@
|
||||
// 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 = root + url;
|
||||
}
|
||||
|
||||
function gotoRandomPage() {
|
||||
gotoUrl(`/random?content=${currentBook}`);
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
const searchbox = document.getElementById('kiwixsearchbox');
|
||||
const q = encodeURIComponent(searchbox.value);
|
||||
gotoUrl(`/search?books.name=${currentBook}&pattern=${q}`);
|
||||
}
|
||||
|
||||
function makeJSLink(jsCodeString, linkText, linkAttr="") {
|
||||
// Values of the href attribute are assumed by the browser to be
|
||||
// fully URI-encoded (no matter what the scheme is). Therefore, in
|
||||
// order to prevent the browser from decoding any URI-encoded parts
|
||||
// in the JS code we have to URI-encode a second time.
|
||||
// (see https://stackoverflow.com/questions/33721510)
|
||||
const uriEncodedJSCode = encodeURIComponent(jsCodeString);
|
||||
return `<a ${linkAttr} href="javascript:${uriEncodedJSCode}">${linkText}</a>`;
|
||||
}
|
||||
|
||||
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) => {
|
||||
const uriEncodedBookName = encodeURIComponent(currentBook);
|
||||
let url;
|
||||
if (data.value.kind == "path") {
|
||||
const path = encodeURIComponent(htmlDecode(data.value.path));
|
||||
url = `/content/${uriEncodedBookName}/${path}`;
|
||||
} else {
|
||||
const pattern = encodeURIComponent(htmlDecode(data.value.value));
|
||||
url = `/search?content=${uriEncodedBookName}&pattern=${pattern}`;
|
||||
}
|
||||
// url can't contain any double quote and/or backslash symbols
|
||||
// since they should have been URI-encoded. Therefore putting it
|
||||
// inside double quotes should result in valid javascript.
|
||||
const jsAction = `gotoUrl("${url}")`;
|
||||
const linkText = htmlDecode(data.value.label);
|
||||
item.innerHTML = makeJSLink(jsAction, linkText, 'class="suggest"');
|
||||
},
|
||||
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,107 +0,0 @@
|
||||
function disableSearchFilters(widgetStyles) {
|
||||
const hideNavRule = `
|
||||
.kiwixNav {
|
||||
display: none;
|
||||
}`;
|
||||
const hideResultsLabelRule = `
|
||||
.kiwixHomeBody__results {
|
||||
display: none;
|
||||
}`;
|
||||
const hideTagFilterRule = `
|
||||
.book__tags {
|
||||
pointer-events: none;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [hideNavRule, hideResultsLabelRule, hideTagFilterRule]);
|
||||
}
|
||||
|
||||
function disableBookClick() {
|
||||
kiwixServe.disableBookClick();
|
||||
}
|
||||
|
||||
function disableDownload(widgetStyles) {
|
||||
const hideBookDownloadRule = `
|
||||
.book__download {
|
||||
display: none;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [hideBookDownloadRule]);
|
||||
}
|
||||
|
||||
function disableDescription(widgetStyles) {
|
||||
const decreaseHeightRule = `
|
||||
.book__wrapper {
|
||||
height:128px;
|
||||
grid-template-rows: 70px 0 1fr 1fr;
|
||||
}`;
|
||||
const hideDescRule = `
|
||||
.book__description {
|
||||
display: none;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [decreaseHeightRule, hideDescRule]);
|
||||
}
|
||||
|
||||
function hideFooter(widgetStyles) {
|
||||
const hideFooterRule = `
|
||||
.kiwixfooter {
|
||||
display: none !important;
|
||||
}`;
|
||||
insertNewCssRules(widgetStyles, [hideFooterRule]);
|
||||
}
|
||||
|
||||
function insertNewCssRules(stylesheet, ruleList) {
|
||||
if (stylesheet) {
|
||||
for (rule of ruleList) {
|
||||
stylesheet.insertRule(rule, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addCustomCss(cssCode) {
|
||||
let customCSS = document.createElement('style');
|
||||
customCSS.innerHTML = cssCode;
|
||||
document.head.appendChild(customCSS);
|
||||
}
|
||||
|
||||
function addCustomJs(jsCode) {
|
||||
new Function(`"use strict";${jsCode}`)();
|
||||
}
|
||||
|
||||
function handleMessages(event) {
|
||||
if ('css' in event.data) {
|
||||
addCustomCss(event.data.css);
|
||||
}
|
||||
if ('js' in event.data) {
|
||||
addCustomJs(event.data.js);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWidget() {
|
||||
const params = new URLSearchParams(window.location.search || filters || '');
|
||||
const widgetStyleElem = document.createElement('style');
|
||||
document.head.appendChild(widgetStyleElem);
|
||||
|
||||
const widgetStyles = widgetStyleElem.sheet;
|
||||
|
||||
const disableFilters = params.has('disablefilter');
|
||||
const disableClick = params.has('disableclick');
|
||||
const disableDwld = params.has('disabledownload');
|
||||
const disableDesc = params.has('disabledesc');
|
||||
|
||||
const blankBase = document.createElement('base');
|
||||
blankBase.target = '_blank';
|
||||
document.head.appendChild(blankBase); // open all links in new tab
|
||||
|
||||
if (disableFilters)
|
||||
disableSearchFilters(widgetStyles);
|
||||
if (disableClick)
|
||||
disableBookClick();
|
||||
if (disableDwld)
|
||||
disableDownload(widgetStyles);
|
||||
if (disableDesc)
|
||||
disableDescription(widgetStyles);
|
||||
|
||||
hideFooter(widgetStyles);
|
||||
kiwixServe.updateBookCount();
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessages);
|
||||
handleWidget();
|
||||
@@ -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}}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Welcome to Kiwix Server</title>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/index.css?KIWIXCACHEID"
|
||||
rel="Stylesheet"
|
||||
/>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "poppins";
|
||||
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "roboto";
|
||||
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
</style>
|
||||
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
|
||||
<script src="{{root}}/skin/iso6391To3.js?KIWIXCACHEID"></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/widget.js?KIWIXCACHEID" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class='kiwixNav'>
|
||||
<div class="kiwixNav__filters">
|
||||
<div class="kiwixNav__select">
|
||||
<select name="lang" id="languageFilter" class='kiwixNav__kiwixFilter filter'>
|
||||
<option value="" selected>All languages</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kiwixNav__select">
|
||||
<select name="category" id="categoryFilter" class='kiwixNav__kiwixFilter filter'>
|
||||
<option value="" selected>All categories</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
|
||||
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
|
||||
<span class="kiwixButton tagFilterLabel"></span>
|
||||
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="kiwixHomeBody">
|
||||
<div class="book__list">
|
||||
<h3 class="kiwixHomeBody__results"></h3>
|
||||
</div>
|
||||
<div id="fadeOut" class="fadeOut"></div>
|
||||
</div>
|
||||
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
|
||||
<div id="kiwixfooter" class="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div>
|
||||
</body>
|
||||
<script>
|
||||
function closeModal() {
|
||||
for(modal of document.getElementsByClassName('modal-wrapper')) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
Binary file not shown.
1
test/data/corner_cases/c#
Symbolic link
1
test/data/corner_cases/c#
Symbolic link
@@ -0,0 +1 @@
|
||||
c#.html
|
||||
10
test/data/corner_cases/c#.html
Normal file
10
test/data/corner_cases/c#.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>C#</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>C# (pronounced see sharp) is a general-purpose, high-level multi-paradigm programming language. C# encompasses static typing, strong typing, lexically scoped, imperative, declarative, functional, generic, object-oriented (class-based), and component-oriented programming disciplines</p>
|
||||
</body>
|
||||
</html>
|
||||
1
test/data/corner_cases/c_sharp.html
Symbolic link
1
test/data/corner_cases/c_sharp.html
Symbolic link
@@ -0,0 +1 @@
|
||||
c#.html
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
rm -f corner_cases.zim
|
||||
zimwriterfs -w empty.html \
|
||||
-f empty.png \
|
||||
-l=en \
|
||||
-t="ZIM corner cases" \
|
||||
-d="" \
|
||||
-c="" \
|
||||
-p="" \
|
||||
zimwriterfs --withoutFTIndex --dont-check-arguments \
|
||||
-w empty.html \
|
||||
-I empty.png \
|
||||
-l en \
|
||||
-t "ZIM corner cases" \
|
||||
-d "" \
|
||||
-c "" \
|
||||
-p "" \
|
||||
corner_cases \
|
||||
corner_cases.zim \
|
||||
&& echo 'corner_cases.zim was successfully created' \
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -188,7 +193,7 @@ TEST_F(LibraryServerTest, catalog_search_by_phrase)
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q="ray charles")</title>\n"
|
||||
" <title>Filtered zims (q=%22ray%20charles%22)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
@@ -207,7 +212,7 @@ TEST_F(LibraryServerTest, catalog_search_by_words)
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=ray charles)</title>\n"
|
||||
" <title>Filtered zims (q=ray%20charles)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>3</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
@@ -228,7 +233,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=description:ray description:charles)</title>\n"
|
||||
" <title>Filtered zims (q=description%3Aray%20description%3Acharles)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
@@ -245,7 +250,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=title:"ray charles")</title>\n"
|
||||
" <title>Filtered zims (q=title%3A%22ray%20charles%22)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
@@ -264,7 +269,7 @@ TEST_F(LibraryServerTest, catalog_search_with_word_exclusion)
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (q=ray -uncategorized)</title>\n"
|
||||
" <title>Filtered zims (q=ray%20-uncategorized)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
@@ -283,7 +288,7 @@ TEST_F(LibraryServerTest, catalog_search_by_tag)
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (tag=_category:jazz)</title>\n"
|
||||
" <title>Filtered zims (tag=_category%3Ajazz)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
@@ -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%2Cfra)</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"
|
||||
@@ -651,7 +694,7 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?q=%22ray%20charles%22")
|
||||
" <title>Filtered Entries (q="ray charles")</title>\n"
|
||||
" <title>Filtered Entries (q=%22ray%20charles%22)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</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%2Cfra")
|
||||
" <title>Filtered Entries (lang=eng%2Cfra)</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%3Ajazz)</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')
|
||||
|
||||
@@ -37,34 +37,34 @@ TEST(OpdsCatalog, getSearchUrl)
|
||||
}
|
||||
{
|
||||
Filter f;
|
||||
f.query("abc def");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def");
|
||||
f.query("abc def#xyz");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def%23xyz");
|
||||
}
|
||||
{
|
||||
Filter f;
|
||||
f.category("ted");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted");
|
||||
f.category("ted&bob");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted%26bob");
|
||||
}
|
||||
{
|
||||
Filter f;
|
||||
f.lang("eng");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng");
|
||||
f.lang("eng,fra");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng%2Cfra");
|
||||
}
|
||||
{
|
||||
Filter f;
|
||||
f.name("second");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?name=second");
|
||||
f.name("second?");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?name=second%3F");
|
||||
}
|
||||
{
|
||||
Filter f;
|
||||
f.acceptTags({"paper", "plastic"});
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?tag=paper;plastic");
|
||||
f.acceptTags({"#paper", "#plastic"});
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?tag=%23paper%3B%23plastic");
|
||||
}
|
||||
{
|
||||
Filter f;
|
||||
f.query("abc");
|
||||
f.category("ted");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc&category=ted");
|
||||
f.query("abc=123");
|
||||
f.category("@ted");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%3D123&category=%40ted");
|
||||
}
|
||||
{
|
||||
Filter f;
|
||||
@@ -79,7 +79,7 @@ TEST(OpdsCatalog, getSearchUrl)
|
||||
f.lang("html");
|
||||
f.name("edsonarantesdonascimento");
|
||||
f.acceptTags({"body", "script"});
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body;script");
|
||||
EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body%3Bscript");
|
||||
}
|
||||
#undef EXPECT_SEARCH_URL
|
||||
}
|
||||
|
||||
235
test/otherTools.cpp
Normal file
235
test/otherTools.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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 "../src/server/i18n.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"
|
||||
);
|
||||
}
|
||||
|
||||
std::string toString(const kiwix::LangPreference& x)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "{" << x.lang << ", " << x.preference << "}";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string toString(const kiwix::UserLangPreferences& prefs) {
|
||||
std::ostringstream oss;
|
||||
for ( const auto& x : prefs )
|
||||
oss << toString(x);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
TEST(I18n, parseUserLanguagePreferences)
|
||||
{
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("")),
|
||||
""
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("*")),
|
||||
"{*, 1}"
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("fr")),
|
||||
"{fr, 1}"
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("fr-CH")),
|
||||
"{fr-CH, 1}"
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("fr, en-US")),
|
||||
"{fr, 1}{en-US, 1}"
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("ru;q=0.5")),
|
||||
"{ru, 0.5}"
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("fr-CH,ru;q=0.5")),
|
||||
"{fr-CH, 1}{ru, 0.5}"
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("ru;q=0.5, *;q=0.1")),
|
||||
"{ru, 0.5}{*, 0.1}"
|
||||
);
|
||||
|
||||
// rejected input
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("ru;")),
|
||||
""
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("ru;q")),
|
||||
""
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("ru;q=")),
|
||||
""
|
||||
);
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("ru;0.8")),
|
||||
""
|
||||
);
|
||||
|
||||
EXPECT_EQ(toString(kiwix::parseUserLanguagePreferences("fr,ru;0.8,en;q=0.5")),
|
||||
"{fr, 1}{en, 0.5}"
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
633
test/server.cpp
633
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=ab5374c5" },
|
||||
{ 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" },
|
||||
|
||||
{ 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/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/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/languages" },
|
||||
{ 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/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=2fcc4ac4" 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=ab5374c5" 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>
|
||||
)" },
|
||||
|
||||
@@ -840,7 +827,7 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
The requested URL "/ROOT/search?content=non-existing-book&pattern=a"<script foo>" is not a valid request.
|
||||
The requested URL "/ROOT/search?content=non-existing-book&pattern=a%22%3Cscript%20foo%3E" is not a valid request.
|
||||
</p>
|
||||
<p>
|
||||
No such book: non-existing-book
|
||||
@@ -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%22%3Cscript%20foo%3E" 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",
|
||||
@@ -989,57 +976,154 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
{
|
||||
struct TestData
|
||||
{
|
||||
const std::string description;
|
||||
const std::string url;
|
||||
const std::string acceptLanguageHeader;
|
||||
const char* const requestCookie; // Cookie: header of the request
|
||||
const char* const responseSetCookie; // Set-Cookie: header of the response
|
||||
const std::string expectedH1;
|
||||
|
||||
operator TestContext() const
|
||||
{
|
||||
return TestContext{
|
||||
TestContext ctx{
|
||||
{"description", description},
|
||||
{"url", url},
|
||||
{"acceptLanguageHeader", acceptLanguageHeader},
|
||||
};
|
||||
|
||||
if ( requestCookie ) {
|
||||
ctx.push_back({"requestCookie", requestCookie});
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
};
|
||||
|
||||
const char* const NO_COOKIE = nullptr;
|
||||
|
||||
const TestData testData[] = {
|
||||
{
|
||||
"Default user language is English",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"userlang URL query parameter is respected",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=en",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=hy",
|
||||
"userlang URL query parameter is respected",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=test",
|
||||
/*Accept-Language:*/ "",
|
||||
/* expected <h1> */ "Սխալ հասցե"
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"'Accept-Language: *' is handled",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "*",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"Accept-Language: header is respected",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "hy",
|
||||
/* expected <h1> */ "Սխալ հասցե"
|
||||
/*Accept-Language:*/ "test",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
// userlang query parameter takes precedence over Accept-Language
|
||||
"userlang cookie is respected",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "userlang=test",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is correctly parsed",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "anothercookie=123; userlang=test",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is correctly parsed",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "userlang=test; anothercookie=abc",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is correctly parsed",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "cookie1=abc; userlang=test; cookie2=xyz",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"Multiple userlang cookies are not a problem",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "cookie1=abc; userlang=en; userlang=test; cookie2=xyz",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* 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",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
// The value of the Accept-Language header is not currently parsed.
|
||||
// In case of a comma separated list of languages (optionally weighted
|
||||
// with quality values) the default (en) language is used instead.
|
||||
"userlang query parameter takes precedence over its cookie counterpart",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article?userlang=en",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "userlang=test",
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"userlang in cookies takes precedence over Accept-Language",
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "hy;q=0.9, en;q=0.2",
|
||||
/*Accept-Language:*/ "test",
|
||||
/*Request Cookie:*/ "userlang=en",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"Most suitable language is selected from the Accept-Language header",
|
||||
// In case of a comma separated list of languages (optionally weighted
|
||||
// with quality values) the most suitable language is selected.
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "test;q=0.9, en;q=0.2",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"Most suitable language is selected from the Accept-Language header",
|
||||
// In case of a comma separated list of languages (optionally weighted
|
||||
// with quality values) the most suitable language is selected.
|
||||
/*url*/ "/ROOT/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "test;q=0.2, en;q=0.9",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
};
|
||||
@@ -1051,7 +1135,16 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
if ( !t.acceptLanguageHeader.empty() ) {
|
||||
headers.insert({"Accept-Language", t.acceptLanguageHeader});
|
||||
}
|
||||
if ( t.requestCookie ) {
|
||||
headers.insert({"Cookie", t.requestCookie});
|
||||
}
|
||||
const auto r = zfs1_->GET(t.url.c_str(), headers);
|
||||
if ( t.responseSetCookie ) {
|
||||
ASSERT_TRUE(r->has_header("Set-Cookie")) << t;
|
||||
EXPECT_EQ(t.responseSetCookie, getHeaderValue(r->headers, "Set-Cookie")) << t;
|
||||
} else {
|
||||
EXPECT_FALSE(r->has_header("Set-Cookie"));
|
||||
}
|
||||
std::regex_search(r->body, h1Match, h1Regex);
|
||||
const std::string h1(h1Match[1]);
|
||||
EXPECT_EQ(h1, t.expectedH1) << t;
|
||||
@@ -1064,6 +1157,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,9 +1202,22 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, RedirectionsToURLsWithSpecialSymbols)
|
||||
{
|
||||
auto g = zfs1_->GET("/ROOT/content/corner_cases/c_sharp.html");
|
||||
ASSERT_EQ(302, g->status);
|
||||
ASSERT_TRUE(g->has_header("Location"));
|
||||
ASSERT_EQ(g->get_header_value("Location"), "/ROOT/content/corner_cases/c%23.html");
|
||||
ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate");
|
||||
ASSERT_FALSE(g->has_header("ETag"));
|
||||
}
|
||||
|
||||
|
||||
TEST_F(ServerTest, BookMainPageIsRedirectedToArticleIndex)
|
||||
{
|
||||
{
|
||||
@@ -1139,11 +1247,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 +1279,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 +1341,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 +1379,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 +1424,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 +1451,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1375,7 +1529,7 @@ TEST_F(ServerTest, InvalidAndMultiRangeByteRangeRequestsResultIn416Responses)
|
||||
|
||||
TEST_F(ServerTest, ValidByteRangeRequestsOfZeroSizedEntriesResultIn416Responses)
|
||||
{
|
||||
const char url[] = "/ROOT/content/corner_cases/-/empty.js";
|
||||
const char url[] = "/ROOT/content/corner_cases/empty.js";
|
||||
|
||||
const char* ranges[] = {
|
||||
"bytes=0-",
|
||||
@@ -1505,11 +1659,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 +1745,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%
|
||||
@@ -195,7 +196,7 @@ struct SearchResult
|
||||
|
||||
const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/content/zimfile/A/Genius_+_Soul_=_Jazz",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Genius_%2B_Soul_%3D_Jazz",
|
||||
/*title*/ "Genius + Soul = Jazz",
|
||||
/*snippet*/ R"SNIPPET(...Grammy Hall of Fame in 2011. It was re-issued in the UK, first in 1989 on the Castle Communications "Essential Records" label, and by Rhino Records in 1997 on a single CD together with Charles' 1970 My Kind of <b>Jazz</b>. In 2010, Concord Records released a deluxe edition comprising digitally remastered versions of Genius + Soul = <b>Jazz</b>, My Kind of <b>Jazz</b>, <b>Jazz</b> Number II, and My Kind of <b>Jazz</b> Part 3. Professional ratings Review scores Source Rating Allmusic link Warr.org link Encyclopedia of Popular Music...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -235,7 +236,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays:_The_Music_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays%3A_The_Music_of_Ray_Charles",
|
||||
/*title*/ "Catchin' Some Rays: The Music of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...<b>jazz</b> singer Roseanna Vitro, released in August 1997 on the Telarc <b>Jazz</b> label. Catchin' Some Rays: The Music of Ray Charles Studio album by Roseanna Vitro Released August 1997 Recorded March 26, 1997 at Sound on Sound, NYC April 4,1997 at Quad Recording Studios, NYC Genre Vocal <b>jazz</b> Length 61:00 Label Telarc <b>Jazz</b> CD-83419 Producer Paul Wickliffe Roseanna Vitro chronology Passion Dance (1996) Catchin' Some Rays: The Music of Ray Charles (1997) The Time of My Life: Roseanna Vitro Sings the Songs of......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -243,7 +244,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say:_John_Scofield_Plays_the_Music_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say%3A_John_Scofield_Plays_the_Music_of_Ray_Charles",
|
||||
/*title*/ "That's What I Say: John Scofield Plays the Music of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(That's What I Say: John Scofield Plays the Music of Ray Charles Studio album by John Scofield Released June 7, 2005 (2005-06-07) Recorded December 2004 Studio Avatar Studios, New York City Genre <b>Jazz</b> Length 65:21 Label Verve Producer Steve Jordan John Scofield chronology EnRoute: John Scofield Trio LIVE (2004) That's What I Say: John Scofield Plays the Music of Ray Charles (2005) Out Louder (2006) Professional ratings Review scores Source Rating Allmusic All About <b>Jazz</b> All About <b>Jazz</b>...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -283,7 +284,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again:_Celebrating_the_Genius_of_Ray_Charles",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again%3A_Celebrating_the_Genius_of_Ray_Charles",
|
||||
/*title*/ "Here We Go Again: Celebrating the Genius of Ray Charles",
|
||||
/*snippet*/ R"SNIPPET(...and <b>jazz</b> trumpeter Wynton Marsalis. It was recorded during concerts at the Rose Theater in New York City, on February 9 and 10, 2009. The album received mixed reviews, in which the instrumentation of Marsalis' orchestra was praised by the critics. Here We Go Again: Celebrating the Genius of Ray Charles Live album by Willie Nelson and Wynton Marsalis Released March 29, 2011 (2011-03-29) Recorded February 9 –10 2009 Venue Rose Theater, New York Genre <b>Jazz</b>, country Length 61:49 Label Blue Note......)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -355,7 +356,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
|
||||
),
|
||||
|
||||
SEARCH_RESULT(
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Sings,_Basie_Swings",
|
||||
/*link*/ "/ROOT/content/zimfile/A/Ray_Sings%2C_Basie_Swings",
|
||||
/*title*/ "Ray Sings, Basie Swings",
|
||||
/*snippet*/ R"SNIPPET(...from 1973 with newly recorded instrumental tracks by the contemporary Count Basie Orchestra. Professional ratings Review scores Source Rating AllMusic Ray Sings, Basie Swings Compilation album by Ray Charles, Count Basie Orchestra Released October 3, 2006 (2006-10-03) Recorded Mid-1970s, February - May 2006 Studio Los Angeles Genre Soul, <b>jazz</b>, Swing Label Concord/Hear Music Producer Gregg Field Ray Charles chronology Genius & Friends (2005) Ray Sings, Basie Swings (2006) Rare Genius: The Undiscovered Masters (2010)...)SNIPPET",
|
||||
/*bookTitle*/ "Ray Charles",
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,4 +105,93 @@ TEST(stringTools, extractFromString)
|
||||
ASSERT_THROW(extractFromString<float>("3.14.5"), std::invalid_argument);
|
||||
}
|
||||
|
||||
};
|
||||
namespace URLEncoding
|
||||
{
|
||||
|
||||
const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const char digits[] = "0123456789";
|
||||
const char nonEncodableSymbols[] = ".-_~()*!/";
|
||||
const char uriDelimSymbols[] = ":@?=+&#$;,";
|
||||
|
||||
const char otherSymbols[] = R"(`%^[]{}\|"<>)";
|
||||
|
||||
const char whitespace[] = " \n\t\r";
|
||||
|
||||
const char someNonASCIIChars[] = "Σ♂♀ツ";
|
||||
|
||||
}
|
||||
|
||||
TEST(stringTools, urlEncode)
|
||||
{
|
||||
using namespace URLEncoding;
|
||||
|
||||
EXPECT_EQ(urlEncode(letters), letters);
|
||||
|
||||
EXPECT_EQ(urlEncode(digits), digits);
|
||||
|
||||
EXPECT_EQ(urlEncode(nonEncodableSymbols), nonEncodableSymbols);
|
||||
|
||||
EXPECT_EQ(urlEncode(uriDelimSymbols), "%3A%40%3F%3D%2B%26%23%24%3B%2C");
|
||||
|
||||
EXPECT_EQ(urlEncode(otherSymbols), "%60%25%5E%5B%5D%7B%7D%5C%7C%22%3C%3E");
|
||||
|
||||
EXPECT_EQ(urlEncode(whitespace), "%20%0A%09%0D");
|
||||
|
||||
EXPECT_EQ(urlEncode(someNonASCIIChars), "%CE%A3%E2%99%82%E2%99%80%E3%83%84");
|
||||
}
|
||||
|
||||
TEST(stringTools, urlDecode)
|
||||
{
|
||||
using namespace URLEncoding;
|
||||
|
||||
const std::string allTestChars = std::string(letters)
|
||||
+ digits
|
||||
+ nonEncodableSymbols
|
||||
+ uriDelimSymbols
|
||||
+ otherSymbols
|
||||
+ whitespace
|
||||
+ someNonASCIIChars;
|
||||
|
||||
for ( const char c : allTestChars ) {
|
||||
const std::string str(1, c);
|
||||
EXPECT_EQ(urlDecode(urlEncode(str), true), str);
|
||||
}
|
||||
|
||||
EXPECT_EQ(urlDecode(urlEncode(allTestChars), true), allTestChars);
|
||||
|
||||
const std::string encodedUriDelimSymbols = urlEncode(uriDelimSymbols);
|
||||
EXPECT_EQ(urlDecode(encodedUriDelimSymbols, false), encodedUriDelimSymbols);
|
||||
}
|
||||
|
||||
TEST(stringTools, uriEncode)
|
||||
{
|
||||
const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, letters), letters);
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, letters), letters);
|
||||
|
||||
const char digits[] = "0123456789";
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, digits), digits);
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, digits), digits);
|
||||
|
||||
const char nonEncodableSymbols[] = ".-_~()*!/";
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, nonEncodableSymbols), nonEncodableSymbols);
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, nonEncodableSymbols), nonEncodableSymbols);
|
||||
|
||||
const char uriDelimSymbols[] = ":@?=+&#$;,";
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, uriDelimSymbols), "%3A%40%3F=+&%23%24%3B%2C");
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, uriDelimSymbols), ":@?%3D%2B%26%23%24%3B%2C");
|
||||
|
||||
const char otherSymbols[] = R"(`%^[]{}\|"<>)";
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, otherSymbols), "%60%25%5E%5B%5D%7B%7D%5C%7C%22%3C%3E");
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, otherSymbols), uriEncode(URIComponentKind::QUERY, otherSymbols));
|
||||
|
||||
const char whitespace[] = " \n\t\r";
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, whitespace), "%20%0A%09%0D");
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, whitespace), uriEncode(URIComponentKind::QUERY, whitespace));
|
||||
|
||||
const char someNonASCIIChars[] = "Σ♂♀ツ";
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, someNonASCIIChars), "%CE%A3%E2%99%82%E2%99%80%E3%83%84");
|
||||
EXPECT_EQ(uriEncode(URIComponentKind::PATH, someNonASCIIChars), uriEncode(URIComponentKind::QUERY, someNonASCIIChars));
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
Reference in New Issue
Block a user