Compare commits

..

105 Commits

Author SHA1 Message Date
Kelson
ddde6db16f Merge pull request #1061 from kiwix/release-3.1.0
Release 3.1.0
2024-02-25 15:11:31 +01:00
Emmanuel Engelhart
50d1394a0a Add 13.1.0 Changelog 2024-02-25 15:11:13 +01:00
Emmanuel Engelhart
a6040b2ecd Bump-up version to 13.1.0 2024-02-25 15:11:13 +01:00
Kelson
4e755bc949 Merge pull request #1062 from kiwix/compilation_warnings
Fixed compilation warnings
2024-02-25 15:01:17 +01:00
Veloman Yunkan
cfab4c946a Fixed compilation warnings 2024-02-25 16:15:29 +04:00
Kelson
57a265f73c Merge pull request #1059 from kiwix/translatewiki 2024-02-22 19:23:34 +01:00
translatewiki.net
3f945813f2 Localisation updates from https://translatewiki.net. 2024-02-22 13:07:51 +01:00
Veloman Yunkan
86100b39ed Merge pull request #1047 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2024-02-20 14:40:42 +04:00
Matthieu Gautier
b2ae6d1fca Update i18n translation files. 2024-02-20 10:40:44 +01:00
translatewiki.net
e82b62c552 Localisation updates from https://translatewiki.net. 2024-02-19 13:07:49 +01:00
Kelson
5fba3f434e Merge pull request #1054 from kiwix/polyfilljs
Enter polyfills.js
2024-02-15 16:04:45 +01:00
Veloman Yunkan
3ac36e8ebd Enter polyfills.js
The `String.replaceAll` polyfill was borrowed (at 0% annual intereset
rate) from https://github.com/kiwix/kiwix-js/pull/1190/files.
2024-02-15 16:03:29 +01:00
Kelson
1babbc0e4a Merge pull request #1043 from kiwix/bookmarks_migrations
Migrate bookmarks between books
2024-02-15 16:01:46 +01:00
Matthieu Gautier
6b05eeb24b Add a small test on getBestTargetBookId and flavour. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
73b855ce6b Add a getBestTargetBookId directly taking bookName, flavour and date. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
eaca7010bc Fix definition of UPGRADE_ONLY and ALLOW_DOWNGRADE.
`MigrationMode` was kind of defined in the context of an internal mode
used by `migrateBookmark(...)`.
But now, with `getBestTargetBookId`, it is broken.

This commit fix that and the associated implementation.
Now `UPGRADE_ONLY` will make `getBestTargetBookId` return only newer books.
and `ALLOW_DOWNGRADE` will return older books only if current book is
invalid.
2024-02-15 14:52:57 +01:00
Matthieu Gautier
6efdc43964 Correcly search for book's title with double quote (").
At indexation time, double quote are ignored, so a title as
`TED "talks" - Business` is indexed as `ted talks business`.

By removing the quotes, we ensure that our title "phrase" is not closed
too early and we correctly search for `ted PHRASE talks PHRASE business`
instead of `ted AND talks AND business`.
2024-02-15 14:52:57 +01:00
Matthieu Gautier
7a0ab3a429 Update tests to check book's title with double quotes (")
On top of modifying the existing test, the commit also make
`MigrateBookmark` test fails as `migrateBookmarks` now migrates
from `wrong-book-id-noname` to `Dummy id`.

Fix will be provided in next commit.
2024-02-15 14:52:57 +01:00
Matthieu Gautier
3e9d50fecb Make getBestTargetBookId public. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
f3a604380c Do not migrate bookmarks to an older book.
At least, it must be explicitly asked by the user.
2024-02-15 14:52:57 +01:00
Matthieu Gautier
167e0dc4b3 Only migrate bookmarks to books with the same flavour.
If there is no book with the same flavour, but book with same name and
different flavour, we do the migration to the other book.
2024-02-15 14:52:57 +01:00
Matthieu Gautier
14c9530afa [Test] Introduce variant books in sample library.
We will need them to test flavour/date bookmarks migration.
2024-02-15 14:52:57 +01:00
Matthieu Gautier
8d97686b81 Introduce migrateBookmarks to move (invalid) bookmarks to new books. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
b16f6b9561 Allow to filter books by flavour. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
a546effa15 Allow bookmark to be created from a Book and url/title. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
699f96ca0d Add book's flavour in bookmark. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
5a0644d32b Also store book's name in bookmark. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
903f476f77 Test bookmarks serializations. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
bf1ab03332 [Test] Add missing flavour in books. 2024-02-15 14:52:57 +01:00
Matthieu Gautier
82cb1133e5 [Test] Add missing name in sample library.xml 2024-02-15 14:52:57 +01:00
Matthieu Gautier
9b9c61a194 Use a recursive_mutex instead of a mutex.
This allow us to internally call thread_safe function from already
locked context.
2024-02-15 14:52:57 +01:00
Matthieu Gautier
c768d05b5b Merge pull request #1056 from kiwix/fix_macos_build
[CI] Fix macos python installation.
2024-02-15 14:52:19 +01:00
Matthieu Gautier
fe018efc70 Update to new macos' python 3.12
Brew update its receipe about python and now use python 3.12 instead of
python 3.11.
2024-02-15 14:16:29 +01:00
Matthieu Gautier
e625c25ef1 Merge pull request #1048 from Begasus/haiku
Haiku
2024-02-08 15:10:42 +01:00
Begasus
b2ae1d66f5 Fix for getifaddrs on Haiku 2024-02-08 11:52:37 +01:00
Begasus
2818dd3151 Fix undeclared SIOCGIFCONF for Haiku 2024-02-08 11:51:12 +01:00
Kelson
09eec822c1 Merge pull request #1046 from kiwix/translation_of_search_results_page
Translation of search results page
2024-02-01 21:33:25 +01:00
Veloman Yunkan
34cd553642 Updated languages.js 2024-02-01 18:33:34 +04:00
Veloman Yunkan
70dd738801 Front-end calls the /search endpoint with userlang 2024-02-01 18:31:32 +04:00
Veloman Yunkan
958067d94d Backend translates the search results page
Now the search results page is presented by the backend in the language
controlled by the value of the `userlang` URL query parameter (or, if
the latter is missing, the value of the `Accept-Language:` HTTP header).

Note that the front-end doesn't yet take advantage of this
functionality.
2024-02-01 18:27:54 +04:00
Veloman Yunkan
33a3277400 Search result info as translatable text
However it is NOT actually translated by the backend yet
2024-02-01 18:27:33 +04:00
Veloman Yunkan
8f5714be07 Search results page header as translatable text
However it is NOT actually translated by the backend yet
2024-02-01 18:27:11 +04:00
Veloman Yunkan
c4fa42f20b Search results page title as translatable text
However it is NOT actually translated by the backend yet
2024-02-01 18:22:36 +04:00
Matthieu Gautier
795fcb9de4 Merge pull request #1044 from kiwix/default_ui_language_is_resolved_in_the_frontend
Default UI language is resolved in the frontend
2024-01-31 17:54:56 +01:00
Veloman Yunkan
c697611064 Dropped defaultUserLanguage from viewer_settings.js 2024-01-31 17:55:17 +04:00
Veloman Yunkan
e5dab19844 Default UI language is resolved in the frontend
This change eliminates any need for defaultUserLanguage in
viewer_settings.js.
2024-01-31 17:55:09 +04:00
Veloman Yunkan
1f44465d09 Added translation counts to skin/languages.js
Note that static/skin/languages.js must be generated/updated manually
by running the static/generate_i18n_resources_list.py script. Previously
it had to be done only when new languages were added. Now the
translation counts will also need to be updated when new entries are
added to static/skin/i18n/en.json or upon merging a few translatewiki PRs.
2024-01-31 17:52:56 +04:00
Veloman Yunkan
258a6d029f Changed the format of skin/languages.js
... so that extra info about the count of translated strings can be
added.

Note that due to increased size skin/languages.js lost its
too-small-to-be-worth-compressing status.
2024-01-31 17:47:41 +04:00
Veloman Yunkan
fc211d9a2e Cleaned up traces of userlang control via cookie 2024-01-31 17:41:37 +04:00
Veloman Yunkan
aff801e6cc Merge pull request #1033 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2024-01-30 14:22:44 +04:00
translatewiki.net
3479589d53 Localisation updates from https://translatewiki.net. 2024-01-29 13:09:38 +01:00
Matthieu Gautier
d2f20dba66 Merge pull request #1032 from kiwix/error_response_i18n
Translation of error pages
2024-01-29 10:58:55 +01:00
Veloman Yunkan
dc3960c5f8 Fix against a malicious "</script>" in KIWIX_RESPONSE_DATA 2024-01-29 10:53:36 +01:00
Veloman Yunkan
1f9026f295 "</script>" inside KIWIX_RESPONSE_DATA is bad
Added a test case demonstrating how a bad error response could be
generated if </script> appears inside KIWIX_RESPONSE_DATA. That seems to
be the only problematic interaction between HTML-like syntax inside
javascript code (hence the deleted XXX comments on the other two test
cases).
2024-01-29 10:53:36 +01:00
Veloman Yunkan
30b3f05497 All kiwix-serve errors are now frontend-translatable
But the question is do we need all of them to be translatable in the
frontend? Maybe only responses to /random, /content and /search endpoints (that
are displayed in the viewer) should be translatable?

Also, the test cases against vulnerabilities in kiwix-serve seem to suggest
that KIWIX_RESPONSE_DATA should be HTML-encoded too.
2024-01-29 10:53:36 +01:00
Veloman Yunkan
13a6863183 Enabled frontend-side translation of 500 error page 2024-01-29 10:53:36 +01:00
Veloman Yunkan
bb1a730253 Workaround for missing support for of std::variant
std::variant is not supported by the old version of gcc used under
aarch64.
2024-01-29 10:53:36 +01:00
Veloman Yunkan
e1f067c086 Undid the demo of frontend-side error page translation
This undoes frontend-side translation of the demo case with the purpose
of having "clean" unit tests to support further work on this PR.
2024-01-29 10:53:36 +01:00
Veloman Yunkan
103a4516db Demo of error page translation
This commit demonstrates front-end-side translation of an error page
for a URL like /viewer#INVALIDBOOK/whatever (where INVALIDBOOK should
be a book name NOT present in the library).

Known issues:

- This change breaks a couple of subtests in the
  ServerTest.Http404HtmlError unit test.

- Changing the UI language while an error page is displayed in the
  viewer doesn't retranslate it.
2024-01-29 10:53:36 +01:00
Veloman Yunkan
bceba4da06 HTML-template data is HTML-encoded
Non-HTML-encoded HTML-template data causes problems in HTML
even when it appears inside JS string (resulting in the <script> tag being
closed by a </script> appearing inside a JS string).

Besides, the KIWIX_RESPONSE_DATA and KIWIX_RESPONSE_TEMPLATE variables
are set on the window object so that they can be accessed from the top
context.

This commit eliminates the need for the `escapeQuote` parameter in
`escapeForJSON()` (that was introduced earlier in this PR) since now it
is set to false in all call contexts. However from the consistency point
of view, the default and intuitive behaviour of `escapeForJSON()` should
be to escape the quote symbols, which justifies the existence of that
parameter.
2024-01-10 00:28:37 +04:00
Veloman Yunkan
e14de69271 The page template is embedded in the error response
This is a shortcut change since it doesn't make sense to send the error
page template with every error response (the viewer can fetch it from
the server once but that's slightly more work).
2024-01-10 00:28:37 +04:00
Veloman Yunkan
d2fedf9123 Added error details in testing of error responses 2024-01-10 00:28:37 +04:00
Veloman Yunkan
b151a2a480 Added KIWIX_RESPONSE_DATA to error response
Now the data used to generate an error response can be made to be
embedded in the response as a JS object KIWIX_RESPONSE_DATA.
2024-01-10 00:26:13 +04:00
Veloman Yunkan
8b8a2eede7 Slight enhancement of escapeForJSON()
- More familiar escape sequences for tab, newline and carriage return
  symbols.

- Quote symbol is escaped by default too, however that behaviour can
  be disabled for uses in HTML-related contexts where quotes should then
  be replaced with the character entity &quot;
2024-01-10 00:26:13 +04:00
Veloman Yunkan
f3d3ab13cb Exposed escapeForJSON() in kiwix namespace
Note that it is declared in stringTools.h but its definition remains in
otherTools.cpp (to minimize the diff).
2024-01-10 00:26:13 +04:00
Veloman Yunkan
1553d52593 Lazy translation during error response generation
Now when parameterized messages are added to an error response, they are
not immediately instantiated (translated). Instead the message id and
the parameters of the message are recorded. The instantiation of the
messages happens right before generating the final content of the
response.
2024-01-10 00:26:13 +04:00
Veloman Yunkan
f298acd45f Unmustached i18n::Parameters 2024-01-10 00:26:13 +04:00
Veloman Yunkan
0b542fe66d New implementation of ContentResponseBlueprint::Data 2024-01-10 00:25:18 +04:00
Veloman Yunkan
e72fc2391d Enter ContentResponseBlueprint::Data
ContentResponseBlueprint::m_data is now an opaque data member
implemented in the .cpp and ready to be switched from
kainjow::mustache::data to a different implementation.
2024-01-09 22:50:34 +04:00
Veloman Yunkan
d39e91f6bc Moved constructor into .cpp 2024-01-09 22:46:06 +04:00
Veloman Yunkan
0b7cd614c6 Fixed an encapsulation breach 2024-01-09 20:44:44 +04:00
Veloman Yunkan
54191bcfab Retired HTTP500Response::generateResponseObject()
... whereupon `ContentResponseBlueprint::generateResponseObject()` (and
`ContentResponseBlueprint` as a whole) no longer needs to be
polymorphic.
2024-01-09 20:44:44 +04:00
Veloman Yunkan
797f4c432c Testing of MIME-type of HTTP 500 response 2024-01-09 20:44:44 +04:00
Veloman Yunkan
c57b8a0c7c Testing of HTTPErrorResponse translation 2024-01-09 20:44:44 +04:00
Veloman Yunkan
aee6c23082 Decoupled RequestContext from MHD_Connection
This will simplify testing of Response utilities.
2024-01-09 20:44:44 +04:00
Veloman Yunkan
af228bf45f Dropped cookies from RequestContext
This should have been done in PR#997 in order to better guarantee
a lasting solution to issue#995.
2024-01-09 20:44:44 +04:00
Veloman Yunkan
b9323f17bb Introduced testing of HTTP response utils 2024-01-09 20:44:44 +04:00
Veloman Yunkan
8993f99587 ParameterizedMessage is actually a class 2024-01-09 20:44:44 +04:00
Veloman Yunkan
96b6f41244 Added i18n unit test 2024-01-09 20:25:59 +04:00
Veloman Yunkan
3f0ea083e6 Moved microhttpd_wrapper.h under server/ 2024-01-09 20:20:51 +04:00
Matthieu Gautier
9c5f5c7be0 Merge pull request #1036 from kiwix/fix_viewer_href
Get correct href value on `onClick` for "warc2zim" files.
2024-01-04 17:18:14 +01:00
Matthieu Gautier
9375f97b60 Get correct href value on onClick for "warc2zim" files.
Next to come warc2zim archive will come with "wombat" embedded.
The purpose of wombat is to be an interface with js code to mask that
we are in a scrapped/zim context to the js.

So it rewrite the `.href` attributes to the original url (ie, an
absolute url to the original website), even if the local relative url
is valid.

Let's ask to wombat to not rewrite href in our special case.
2024-01-04 17:03:40 +01:00
Matthieu Gautier
2ad5e510c6 Merge pull request #1035 from kiwix/ghaction
Use kiwix-build's github action to download dependencies.
2023-12-20 11:53:26 +01:00
Matthieu Gautier
a2e56e2422 Make homebrew don't try to update installed dependencies. 2023-12-20 11:45:28 +01:00
Matthieu Gautier
8cc724b4a4 Use kiwix-build's github action to download dependencies. 2023-12-20 11:45:28 +01:00
Kelson
fa212fd6ae Merge pull request #1027 from kiwix/polish-apple-ci
Better use GitHub action .env directive
2023-12-04 22:39:45 +01:00
Emmanuel Engelhart
c0073b3bc7 Better use GitHub action .env directive 2023-12-04 20:51:46 +01:00
Matthieu Gautier
0d2b6b3344 Merge pull request #1030 from kiwix/cleanup_of_error_response_generation 2023-12-04 10:59:55 +01:00
Veloman Yunkan
5f27b4b651 Taking advantage of std::make_unique() 2023-11-29 21:32:16 +04:00
Veloman Yunkan
7a85c92025 Dropped root from HTTPErrorResponse & friends 2023-11-29 21:32:16 +04:00
Veloman Yunkan
6e2be481fd Dropped the root param from ItemResponse::build() 2023-11-29 21:32:16 +04:00
Veloman Yunkan
db3b76247f Last step of removing root from ContentResponse 2023-11-29 21:32:16 +04:00
Veloman Yunkan
6a651e04e5 1st step in removing root from ContentResponse
It turned out that ContentResponse::m_root is no longer used.

At this point, the root parameter is dropped only from the 3-ary variant
of ContentResponse::build(), so that its all call sites are
automatically discovered by the compiler (and updated manually).
Including the other (4-ary) variant of ContentResponse::build() in this
change might result in the semantic change of expressions like
`ContentResponse::build(x, y, z)` and failure to update them.
2023-11-29 21:32:16 +04:00
Veloman Yunkan
22ea3106c5 Passing only root location instead of the entire server 2023-11-29 21:32:16 +04:00
Veloman Yunkan
2d132d701e Dropped the server param from Response::build*() 2023-11-29 21:32:16 +04:00
Veloman Yunkan
f81a5a1a4b Moved verbosity control to Response::send()
It makes little sense to pass the verbosity control to the `Response`
constructor if it is used only in `Response::send()`.
2023-11-29 21:32:12 +04:00
Veloman Yunkan
3dce025f47 Deleted an unused function 2023-11-29 17:16:23 +04:00
Veloman Yunkan
e470c97f74 Got rid of InvalidUrlMsg 2023-11-29 15:42:21 +04:00
Veloman Yunkan
a7ea908bcd HTTPErrorResponse no longer accepts std::strings 2023-11-29 15:35:53 +04:00
Veloman Yunkan
41f25083da Replaced UrlNotFoundMsg with UrlNotFoundResponse 2023-11-29 14:31:38 +04:00
Veloman Yunkan
3188b0afe6 Translated a hard-coded error message 2023-11-29 14:18:06 +04:00
Kelson
f8aae395f3 Merge pull request #1018 from kiwix/ci-ios
Test iOS cross-compile in CI
2023-11-23 08:32:30 +01:00
renaud gaudin
c5088aad7b fixed typo in deps filename to fetch 2023-11-23 07:33:51 +01:00
Emmanuel Engelhart
269a659160 Download proper deps file 2023-11-23 07:33:51 +01:00
Emmanuel Engelhart
7161df9e4c Test iOS cross-compile in CI 2023-11-23 07:33:51 +01:00
65 changed files with 2468 additions and 629 deletions

View File

@@ -8,7 +8,17 @@ on:
jobs:
macOS:
runs-on: macos-13
strategy:
fail-fast: false
matrix:
os:
- macos-13
target:
- native_dyn
- iOS_arm64
- iOS_x86_64
runs-on: ${{ matrix.os }}
env:
HOME: /Users/runner
steps:
@@ -19,25 +29,34 @@ jobs:
run: |
brew update
brew unlink python3
# upgrade from python@3.11.2_1 to python@3.11.3 fails to overwrite those
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.11 /usr/local/bin/idle3 /usr/local/bin/idle3.11 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.11 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.11 /usr/local/bin/python3.11-config
# upgrade from python@3.12 to python@3.12.2 fails to overwrite those
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.12 /usr/local/bin/idle3 /usr/local/bin/idle3.12 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.12 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.12 /usr/local/bin/python3.12-config
brew install pkg-config ninja meson
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
- name: Install dependencies
env:
ARCHIVE_NAME: deps2_macos_native_dyn_libkiwix.tar.xz
run: |
wget -O- https://tmp.kiwix.org/ci/${{env.ARCHIVE_NAME}} | tar -xJ -C ${{env.HOME}}
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
with:
os_name: macos
target_platform: ${{ matrix.target }}
- name: Compile source code
- name: Compile
env:
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib/pkgconfig
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_${{matrix.target}}/INSTALL/lib/pkgconfig
CPPFLAGS: -I${{env.HOME}}/BUILD_native_dyn/INSTALL/include
MESON_OPTION: --default-library=shared -Db_coverage=true
MESON_CROSSFILE: ${{env.HOME}}/BUILD_${{matrix.target}}/meson_cross_file.txt
shell: bash
run: |
meson . build --default-library=shared -Db_coverage=true
if [[ ! "${{matrix.target}}" =~ native_.* ]]; then
MESON_OPTION="$MESON_OPTION --cross-file $MESON_CROSSFILE -Dstatic-linkage=true"
fi
meson . build ${MESON_OPTION}
ninja -C build
- name: Test libkiwix
if: startsWith(matrix.target, 'native_')
env:
SKIP_BIG_MEMORY_TEST: 1
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib:${{env.HOME}}/BUILD_native_dyn/INSTALL/lib64
@@ -87,11 +106,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install deps
shell: bash
run: |
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_libkiwix.tar.xz
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C /home/runner
- name: Install dependencies
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
with:
target_platform: ${{ matrix.target }}
- name: Compile
shell: bash
run: |

View File

@@ -1,3 +1,14 @@
libkiwix 13.1.0
===============
* Server:
- Properly translated error pages (@veloman-yunkan #1032)
- Properly translated search result page (@veloman-yunkan #1046)
- Default UI language is resolved in frontend (@veloman-yunkan #1044)
- Better support of older Web browsers by polyfilling replaceAll() (@veloman-yunkan #1054)
* New API to migrate bookmarks between books (@mgautierfr #1043)
* Fixed compilation on Haiku OS (@Begasus #1048)
libkiwix 13.0.0
===============
@@ -53,8 +64,6 @@ libkiwix 12.1.0
* Remove libkiwix android publisher from the repository (@kelson42 #884)
* Various fixes of meson and CI. (@mgautierfr @kelson42)
libkiwix 12.0.0
===============
@@ -94,7 +103,6 @@ libkiwix 12.0.0
* Fix documentation (@kelson42 #816)
* Udpate translation (#787 #839 #847)
libkiwix 11.0.0
===============

View File

@@ -29,19 +29,33 @@ class xml_node;
namespace kiwix
{
class Book;
/**
* A class to store information about a bookmark (an article in a book)
*/
class Bookmark
{
public:
/**
* Create an empty bookmark.
*
* Bookmark must be populated with `set*` methods
*/
Bookmark();
/**
* Create a bookmark given a Book, a path and a title.
*/
Bookmark(const Book& book, const std::string& path, const std::string& title);
~Bookmark();
void updateFromXml(const pugi::xml_node& node);
const std::string& getBookId() const { return m_bookId; }
const std::string& getBookTitle() const { return m_bookTitle; }
const std::string& getBookName() const { return m_bookName; }
const std::string& getBookFlavour() const { return m_bookFlavour; }
const std::string& getUrl() const { return m_url; }
const std::string& getTitle() const { return m_title; }
const std::string& getLanguage() const { return m_language; }
@@ -49,6 +63,8 @@ class Bookmark
void setBookId(const std::string& bookId) { m_bookId = bookId; }
void setBookTitle(const std::string& bookTitle) { m_bookTitle = bookTitle; }
void setBookName(const std::string& bookName) { m_bookName = bookName; }
void setBookFlavour(const std::string& bookFlavour) { m_bookFlavour = bookFlavour; }
void setUrl(const std::string& url) { m_url = url; }
void setTitle(const std::string& title) { m_title = title; }
void setLanguage(const std::string& language) { m_language = language; }
@@ -57,6 +73,8 @@ class Bookmark
protected:
std::string m_bookId;
std::string m_bookTitle;
std::string m_bookName;
std::string m_bookFlavour;
std::string m_url;
std::string m_title;
std::string m_language;

View File

@@ -55,6 +55,22 @@ enum supportedListMode {
NOVALID = 1 << 5
};
enum MigrationMode {
/** When migrating bookmarks, do not allow to migrate to an older book than the currently pointed one
* (or date stored in the bookmark if book is invalid)
*
* If no newer books are found, no upgrade is made.
*/
UPGRADE_ONLY = 0,
/** Try hard to do a migration. This mostly does:
* - Try to find a newer book.
* - If book is invalid: find a best book, potentially older.
* Older book will never be returned if current book is a valid one.
*/
ALLOW_DOWNGRADE = 1,
};
class Filter {
public: // types
using Tags = std::vector<std::string>;
@@ -71,6 +87,7 @@ class Filter {
std::string _query;
bool _queryIsPartial;
std::string _name;
std::string _flavour;
public: // functions
Filter();
@@ -130,6 +147,7 @@ class Filter {
Filter& maxSize(size_t size);
Filter& query(std::string query, bool partial=true);
Filter& name(std::string name);
Filter& flavour(std::string flavour);
Filter& clearLang();
Filter& clearCategory();
@@ -152,6 +170,9 @@ class Filter {
bool hasCreator() const;
const std::string& getCreator() const { return _creator; }
bool hasFlavour() const;
const std::string& getFlavour() const { return _flavour; }
const Tags& getAcceptTags() const { return _acceptTags; }
const Tags& getRejectTags() const { return _rejectTags; }
@@ -250,7 +271,7 @@ class Library: public std::enable_shared_from_this<Library>
void addBookmark(const Bookmark& bookmark);
/**
* Remove a bookmarkk
* Remove a bookmark
*
* @param zimId The zimId of the bookmark.
* @param url The url of the bookmark.
@@ -258,6 +279,66 @@ class Library: public std::enable_shared_from_this<Library>
*/
bool removeBookmark(const std::string& zimId, const std::string& url);
/**
* Migrate all invalid bookmarks.
*
* All invalid bookmarks (ie pointing to unknown books, no check is made on bookmark pointing to
* invalid articles of valid book) will be migrated (if possible) to a better book.
* "Better book", will be determined using method `getBestTargetBookId`.
*
* @return A tuple<int, int>: <The number of bookmarks updated>, <Number of invalid bookmarks before migration was performed>.
*/
std::tuple<int, int> migrateBookmarks(MigrationMode migrationMode = ALLOW_DOWNGRADE);
/**
* Migrate all bookmarks associated to a specific book.
*
* All bookmarks associated to `sourceBookId` book will be migrated to a better book.
* "Better book", will be determined using method `getBestTargetBookId`.
*
* @param sourceBookId the source bookId of the bookmarks to migrate.
* @param migrationMode how we will find the best book.
* @return The number of bookmarks updated.
*/
int migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode = UPGRADE_ONLY);
/**
* Migrate bookmarks
*
* Migrate all bookmarks pointing to `source` to `destination`.
*
* @param sourceBookId the source bookId of the bookmarks to migrate.
* @param targetBookId the destination bookId to migrate the bookmarks to.
* @return The number of bookmarks updated.
*/
int migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId);
/**
* Get the best available bookId for a bookmark.
*
* Given a bookmark, return the best available bookId.
* "best available bookId" is determined using heuristitcs based on book name, flavour and date.
*
* @param bookmark The bookmark to search the bookId for.
* @param migrationMode The migration mode to use.
* @return A bookId. Potentially empty string if no suitable book found.
*/
std::string getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const;
/**
* Get the best bookId for a combination of book's name, flavour and date.
*
* Given a bookName (mandatory), try to find the best book.
* If preferedFlavour is given, will try to find a book with the same flavour. If not found, return a book with a different flavour.
* If minDate is given, return a book newer than minDate. If not found, return a empty bookId.
*
* @param bookName The name of the book
* @param preferedFlavour The prefered flavour.
* @param minDate the minimal book date acceptable. Must be a string in the format "YYYY-MM-DD".
* @return A bookId corresponding to the query, or empty string if not found.
*/
std::string getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour="", const std::string& minDate="") const;
// XXX: This is a non-thread-safe operation
const Book& getBookById(const std::string& id) const;
// XXX: This is a non-thread-safe operation
@@ -403,12 +484,13 @@ private: // functions
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
BookIdCollection filterViaBookDB(const Filter& filter) const;
std::string getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) const;
unsigned int getBookCount_not_protected(const bool localBooks, const bool remoteBooks) const;
void updateBookDB(const Book& book);
void dropCache(const std::string& bookId);
private: //data
mutable std::mutex m_mutex;
mutable std::recursive_mutex m_mutex;
Library::Revision m_revision;
std::map<std::string, Entry> m_books;
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;

View File

@@ -72,6 +72,13 @@ class SearchRenderer
this->pageLength = pageLength;
}
/**
* set user language
*/
void setUserLang(const std::string& lang){
this->userlang = lang;
}
/**
* Generate the html page with the resutls of the search.
*
@@ -105,6 +112,7 @@ class SearchRenderer
unsigned int pageLength;
unsigned int estimatedResultCount;
unsigned int resultStart;
std::string userlang = "en";
};

View File

@@ -1,5 +1,5 @@
project('libkiwix', 'cpp',
version : '13.0.0',
version : '13.1.0',
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])

View File

@@ -18,6 +18,7 @@
*/
#include "bookmark.h"
#include "book.h"
#include <pugixml.hpp>
@@ -28,6 +29,17 @@ Bookmark::Bookmark()
{
}
Bookmark::Bookmark(const Book& book, const std::string& path, const std::string& title):
m_bookId(book.getId()),
m_bookTitle(book.getTitle()),
m_bookName(book.getName()),
m_bookFlavour(book.getFlavour()),
m_url(path),
m_title(title),
m_language(book.getCommaSeparatedLanguages()),
m_date(book.getDate())
{}
/* Destructor */
Bookmark::~Bookmark()
{
@@ -38,6 +50,8 @@ void Bookmark::updateFromXml(const pugi::xml_node& node)
auto bookNode = node.child("book");
m_bookId = bookNode.child("id").child_value();
m_bookTitle = bookNode.child("title").child_value();
m_bookName = bookNode.child("name").child_value();
m_bookFlavour = bookNode.child("flavour").child_value();
m_language = bookNode.child("language").child_value();
m_date = bookNode.child("date").child_value();
m_title = node.child("title").child_value();

View File

@@ -110,7 +110,7 @@ Library::~Library() = default;
bool Library::addBook(const Book& book)
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
++m_revision;
/* Try to find it */
updateBookDB(book);
@@ -141,13 +141,13 @@ bool Library::addBook(const Book& book)
void Library::addBookmark(const Bookmark& bookmark)
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_bookmarks.push_back(bookmark);
}
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for(auto it=m_bookmarks.begin(); it!=m_bookmarks.end(); it++) {
if (it->getBookId() == zimId && it->getUrl() == url) {
m_bookmarks.erase(it);
@@ -157,6 +157,159 @@ bool Library::removeBookmark(const std::string& zimId, const std::string& url)
return false;
}
std::tuple<int, int> Library::migrateBookmarks(MigrationMode migrationMode) {
std::set<std::string> sourceBooks;
int invalidBookmarks = 0;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for(auto& bookmark:m_bookmarks) {
if (m_books.find(bookmark.getBookId()) == m_books.end()) {
invalidBookmarks += 1;
sourceBooks.insert(bookmark.getBookId());
}
}
}
int changed = 0;
for(auto& sourceBook:sourceBooks) {
changed += migrateBookmarks(sourceBook, migrationMode);
}
return std::make_tuple(changed, invalidBookmarks);
}
std::string Library::getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) const {
// This function try to get the best book for a bookmark from a book collection.
// It assumes that all books in the collection are "acceptable".
// (this definiton is not clear but for now it is book's name is equal to bookmark's bookName)
//
// The algorithm first sort the colletion by "flavour equality" and date.
// "flavour equality" is if book's flavour is same that bookmark's flavour (let's say "flavourA" here)
// So we have the sorted collection:
// - flavourA, date 5
// - flavourA, date 4
// - flavourB, date 6
// - flavourC, date 5
// - flavourB, date 3
//
// Then, depending of migrationMode:
// - If ALLOW_DOWNGRADE => take the first one
// - If UPGRADE_ONLY => loop on books until we find a book newer than bookmark.
// So if bookmark date is 5 => flavourB, date 6
// if bookmark date is 4 => flavourA, date 5
// if bookmark date is 7 => No book
if (books.empty()) {
return "";
}
sort(books, DATE, false);
stable_sort(books.begin(), books.end(), [&](const std::string& bookId1, const std::string& bookId2) {
const auto& book1 = getBookById(bookId1);
const auto& book2 = getBookById(bookId2);
bool same_flavour1 = book1.getFlavour() == bookmark.getBookFlavour();
bool same_flavour2 = book2.getFlavour() == bookmark.getBookFlavour();
// return True if bookId1 is before bookId2, ie if same_flavour1 and not same_flavour2
return same_flavour1 > same_flavour2;
});
if (migrationMode == ALLOW_DOWNGRADE) {
return books[0];
} else {
for (const auto& bookId: books) {
const auto& book = getBookById(bookId);
if (book.getDate() >= bookmark.getDate()) {
return bookId;
}
}
}
return "";
}
std::string remove_quote(std::string input) {
std::replace(input.begin(), input.end(), '"', ' ');
return input;
}
std::string Library::getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour, const std::string& minDate) const {
// Let's reuse our algorithm based on bookmark.
MigrationMode migrationMode = UPGRADE_ONLY;
auto bookmark = Bookmark();
bookmark.setBookName(bookName);
bookmark.setBookFlavour(preferedFlavour);
if (minDate.empty()) {
migrationMode = ALLOW_DOWNGRADE;
} else {
bookmark.setDate(minDate);
}
return getBestTargetBookId(bookmark, migrationMode);
}
std::string Library::getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
// Search for a existing book with the same name
auto book_filter = Filter();
if (!bookmark.getBookName().empty()) {
book_filter.name(bookmark.getBookName());
} else {
// We don't have a name stored (older bookmarks)
// Fallback on title (All bookmarks should have one, but let's be safe against wrongly filled bookmark)
if (bookmark.getBookTitle().empty()) {
// No bookName nor bookTitle, no way to find target book.
return "";
}
book_filter.query("title:\"" + remove_quote(bookmark.getBookTitle()) + "\"");
}
auto targetBooks = filter(book_filter);
auto bestBook = getBestFromBookCollection(targetBooks, bookmark, migrationMode);
if (bestBook.empty()) {
try {
getBookById(bookmark.getBookId());
return bookmark.getBookId();
} catch (std::out_of_range&) {}
}
return bestBook;
}
int Library::migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
Bookmark firstBookmarkToChange;
for(auto& bookmark:m_bookmarks) {
if (bookmark.getBookId() == sourceBookId) {
firstBookmarkToChange = bookmark;
break;
}
}
if (firstBookmarkToChange.getBookId().empty()) {
return 0;
}
std::string betterBook = getBestTargetBookId(firstBookmarkToChange, migrationMode);
if (betterBook.empty()) {
return 0;
}
return migrateBookmarks(sourceBookId, betterBook);
}
int Library::migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId) {
if (sourceBookId == targetBookId) {
return 0;
}
int changed = 0;
for (auto& bookmark:m_bookmarks) {
if (bookmark.getBookId() == sourceBookId) {
bookmark.setBookId(targetBookId);
changed +=1;
}
}
return changed;
}
void Library::dropCache(const std::string& id)
{
@@ -166,7 +319,7 @@ void Library::dropCache(const std::string& id)
bool Library::removeBookById(const std::string& id)
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_bookDB->delete_document("Q" + id);
dropCache(id);
// We do not change the cache size here
@@ -184,7 +337,7 @@ bool Library::removeBookById(const std::string& id)
Library::Revision Library::getRevision() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_revision;
}
@@ -192,7 +345,7 @@ uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
{
BookIdCollection booksToRemove;
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for ( const auto& entry : m_books) {
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
booksToRemove.push_back(entry.first);
@@ -217,7 +370,7 @@ const Book& Library::getBookById(const std::string& id) const
Book Library::getBookByIdThreadSafe(const std::string& id) const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return getBookById(id);
}
@@ -275,7 +428,7 @@ std::shared_ptr<ZimSearcher> Library::getSearcherByIds(const BookIdSet& ids)
unsigned int Library::getBookCount(const bool localBooks,
const bool remoteBooks) const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return getBookCount_not_protected(localBooks, remoteBooks);
}
@@ -288,7 +441,7 @@ bool Library::writeToFile(const std::string& path) const
dumper.setBaseDir(baseDir);
std::string xml;
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
xml = dumper.dumpLibXMLContent(allBookIds);
};
return writeTextFile(path, xml);
@@ -304,7 +457,7 @@ bool Library::writeBookmarksToFile(const std::string& path) const
Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
AttributeCounts propValueCounts;
for (const auto& pair: m_books) {
@@ -336,7 +489,7 @@ std::vector<std::string> Library::getBooksLanguages() const
Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
AttributeCounts langsWithCounts;
for (const auto& pair: m_books) {
@@ -352,7 +505,7 @@ Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
std::vector<std::string> Library::getBooksCategories() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
std::set<std::string> categories;
for (const auto& pair: m_books) {
@@ -383,7 +536,7 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
}
std::vector<kiwix::Bookmark> validBookmarks;
auto booksId = getBooksIds();
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for(auto& bookmark:m_bookmarks) {
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
validBookmarks.push_back(bookmark);
@@ -394,7 +547,7 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
Library::BookIdCollection Library::getBooksIds() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
BookIdCollection bookIds;
for (auto& pair: m_books) {
@@ -437,6 +590,7 @@ void Library::updateBookDB(const Book& book)
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
doc.add_term("XN"+normalizeText(book.getName()));
indexer.index_text(normalizeText(book.getFlavour()), 1, "XF");
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
@@ -477,6 +631,7 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
queryParser.add_prefix("title", "S");
queryParser.add_prefix("description", "XD");
queryParser.add_prefix("name", "XN");
queryParser.add_prefix("flavour", "XF");
queryParser.add_prefix("category", "XC");
queryParser.add_prefix("lang", "L");
queryParser.add_prefix("publisher", "XP");
@@ -503,6 +658,11 @@ Xapian::Query nameQuery(const std::string& name)
return Xapian::Query("XN" + normalizeText(name));
}
Xapian::Query flavourQuery(const std::string& name)
{
return Xapian::Query("XF" + normalizeText(name));
}
Xapian::Query multipleParamQuery(const std::string& commaSeparatedList, const std::string& prefix)
{
Xapian::Query q;
@@ -570,6 +730,9 @@ Xapian::Query buildXapianQuery(const Filter& filter)
if ( filter.hasName() ) {
q = Xapian::Query(Xapian::Query::OP_AND, q, nameQuery(filter.getName()));
}
if ( filter.hasFlavour() ) {
q = Xapian::Query(Xapian::Query::OP_AND, q, flavourQuery(filter.getFlavour()));
}
if ( filter.hasCategory() ) {
q = Xapian::Query(Xapian::Query::OP_AND, q, categoryQuery(filter.getCategory()));
}
@@ -600,7 +763,7 @@ Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
BookIdCollection bookIds;
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
Xapian::Enquire enquire(*m_bookDB);
enquire.set_query(query);
const auto results = enquire.get_mset(0, m_books.size());
@@ -615,7 +778,7 @@ Library::BookIdCollection Library::filter(const Filter& filter) const
{
BookIdCollection result;
const auto preliminaryResult = filterViaBookDB(filter);
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for(auto id : preliminaryResult) {
if(filter.accept(m_books.at(id))) {
result.push_back(id);
@@ -689,7 +852,7 @@ void Library::sort(BookIdCollection& bookIds, supportedListSortBy sort, bool asc
// NOTE: for the entire duration of the sort. Will need to obtain (under a
// NOTE: lock) the required atributes from the books once, and then the
// NOTE: sorting will run on a copy of data without locking.
std::lock_guard<std::mutex> lock(m_mutex);
std::lock_guard<std::recursive_mutex> lock(m_mutex);
switch(sort) {
case TITLE:
std::sort(bookIds.begin(), bookIds.end(), Comparator<TITLE>(this, ascending));
@@ -735,6 +898,7 @@ enum filterTypes {
QUERY = FLAG(12),
NAME = FLAG(13),
CATEGORY = FLAG(14),
FLAVOUR = FLAG(15),
};
Filter& Filter::local(bool accept)
@@ -836,6 +1000,13 @@ Filter& Filter::name(std::string name)
activeFilters |= NAME;
return *this;
}
Filter& Filter::flavour(std::string flavour)
{
_flavour = flavour;
activeFilters |= FLAVOUR;
return *this;
}
Filter& Filter::clearLang()
{
@@ -881,6 +1052,12 @@ bool Filter::hasCreator() const
return ACTIVE(_CREATOR);
}
bool Filter::hasFlavour() const
{
return ACTIVE(FLAVOUR);
}
bool Filter::accept(const Book& book) const
{
auto local = !book.getPath().empty();

View File

@@ -97,11 +97,15 @@ void LibXMLDumper::handleBookmark(Bookmark bookmark, pugi::xml_node root_node) {
auto book = library->getBookByIdThreadSafe(bookmark.getBookId());
ADD_TEXT_ENTRY(book_node, "id", book.getId());
ADD_TEXT_ENTRY(book_node, "title", book.getTitle());
ADD_TEXT_ENTRY(book_node, "name", book.getName());
ADD_TEXT_ENTRY(book_node, "flavour", book.getFlavour());
ADD_TEXT_ENTRY(book_node, "language", book.getCommaSeparatedLanguages());
ADD_TEXT_ENTRY(book_node, "date", book.getDate());
} catch (...) {
ADD_TEXT_ENTRY(book_node, "id", bookmark.getBookId());
ADD_TEXT_ENTRY(book_node, "title", bookmark.getBookTitle());
ADD_TEXT_ENTRY(book_node, "name", bookmark.getBookName());
ADD_TEXT_ENTRY(book_node, "flavour", bookmark.getBookFlavour());
ADD_TEXT_ENTRY(book_node, "language", bookmark.getLanguage());
ADD_TEXT_ENTRY(book_node, "date", bookmark.getDate());
}
@@ -135,7 +139,7 @@ std::string LibXMLDumper::dumpLibXMLBookmark()
pugi::xml_node bookmarksNode = doc.append_child("bookmarks");
if (library) {
for (auto& bookmark: library->getBookmarks()) {
for (auto& bookmark: library->getBookmarks(false)) {
handleBookmark(bookmark, bookmarksNode);
}
}

View File

@@ -32,9 +32,42 @@
#include "libkiwix-resources.h"
#include "tools/stringTools.h"
#include "server/i18n.h"
namespace kiwix
{
namespace
{
ParameterizedMessage searchResultsPageTitleMsg(const std::string& searchPattern)
{
return ParameterizedMessage("search-results-page-title",
{{"SEARCH_PATTERN", searchPattern}}
);
}
ParameterizedMessage searchResultsPageHeaderMsg(const std::string& searchPattern,
const kainjow::mustache::data& r)
{
if ( r.get("count")->string_value() == "0" ) {
return ParameterizedMessage("empty-search-results-page-header",
{{"SEARCH_PATTERN", searchPattern}}
);
} else {
return ParameterizedMessage("search-results-page-header",
{
{"SEARCH_PATTERN", searchPattern},
{"START", r.get("start")->string_value()},
{"END", r.get("end") ->string_value()},
{"COUNT", r.get("count")->string_value()},
}
);
}
}
} // unnamed namespace
/* Constructor */
SearchRenderer::SearchRenderer(zim::SearchResultSet srs,
unsigned int start, unsigned int estimatedResultCount)
@@ -170,10 +203,20 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
result.set("absolutePath", absPathPrefix + urlEncode(path));
result.set("snippet", it.getSnippet());
if (library) {
result.set("bookTitle", library->getBookById(zim_id).getTitle());
const std::string bookTitle = library->getBookById(zim_id).getTitle();
const ParameterizedMessage bookInfoMsg("search-result-book-info",
{{"BOOK_TITLE", bookTitle}}
);
result.set("bookInfo", bookInfoMsg.getText(userlang)); // for HTML
result.set("bookTitle", bookTitle); // for XML
}
if (it.getWordCount() >= 0) {
result.set("wordCount", kiwix::beautifyInteger(it.getWordCount()));
const auto wordCountStr = kiwix::beautifyInteger(it.getWordCount());
const ParameterizedMessage wordCountMsg("word-count",
{{"COUNT", wordCountStr}}
);
result.set("wordCountInfo", wordCountMsg.getText(userlang)); // for HTML
result.set("wordCount", wordCountStr); // for XML
}
items.push_back(result);
@@ -181,7 +224,6 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
kainjow::mustache::data results;
results.set("items", items);
results.set("count", kiwix::beautifyInteger(estimatedResultCount));
results.set("hasResults", estimatedResultCount != 0);
results.set("start", kiwix::beautifyInteger(resultStart));
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength-1, estimatedResultCount)));
@@ -198,12 +240,15 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
searchBookQuery
);
kainjow::mustache::data allData;
allData.set("searchProtocolPrefix", searchProtocolPrefix);
allData.set("results", results);
allData.set("pagination", pagination);
allData.set("query", query);
const auto pageHeaderMsg = searchResultsPageHeaderMsg(searchPattern, results);
const kainjow::mustache::object allData{
{"PAGE_TITLE", searchResultsPageTitleMsg(searchPattern).getText(userlang)},
{"PAGE_HEADER", pageHeaderMsg.getText(userlang)},
{"searchProtocolPrefix", searchProtocolPrefix},
{"results", results},
{"pagination", pagination},
{"query", query},
};
kainjow::mustache::mustache tmpl(tmpl_str);

View File

@@ -112,8 +112,12 @@ std::string expandParameterizedString(const std::string& lang,
const std::string& key,
const Parameters& params)
{
kainjow::mustache::object mustacheParams;
for( const auto& kv : params ) {
mustacheParams[kv.first] = kv.second;
}
const std::string tmpl = getTranslatedString(lang, key);
return render_template(tmpl, params);
return render_template(tmpl, mustacheParams);
}
} // namespace i18n

View File

@@ -20,6 +20,7 @@
#ifndef KIWIX_SERVER_I18N
#define KIWIX_SERVER_I18N
#include <map>
#include <string>
#include <mustache.hpp>
@@ -44,7 +45,7 @@ std::string getTranslatedString(const std::string& lang, const std::string& key)
namespace i18n
{
typedef kainjow::mustache::object Parameters;
typedef std::map<std::string, std::string> Parameters;
std::string expandParameterizedString(const std::string& lang,
const std::string& key,
@@ -93,10 +94,10 @@ private:
} // namespace i18n
struct ParameterizedMessage
class ParameterizedMessage
{
public: // types
typedef kainjow::mustache::object Parameters;
typedef i18n::Parameters Parameters;
public: // functions
ParameterizedMessage(const std::string& msgId, const Parameters& params)
@@ -106,11 +107,20 @@ public: // functions
std::string getText(const std::string& lang) const;
const std::string& getMsgId() const { return msgId; }
const Parameters& getParams() const { return params; }
private: // data
const std::string msgId;
const Parameters params;
};
inline ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
{
const ParameterizedMessage::Parameters noParams;
return ParameterizedMessage(msgId, noParams);
}
struct LangPreference
{
const std::string lang;

View File

@@ -190,12 +190,6 @@ ParameterizedMessage tooManyBooksMsg(size_t nbBooks, size_t limit)
);
}
ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
{
const ParameterizedMessage::Parameters noParams;
return ParameterizedMessage(msgId, noParams);
}
struct Error : public std::runtime_error {
explicit Error(const ParameterizedMessage& message)
: std::runtime_error("Error while handling request"),
@@ -519,6 +513,19 @@ static MHD_Result staticHandlerCallback(void* cls,
cont_cls);
}
namespace
{
MHD_Result add_name_value_pair(void *nvp, enum MHD_ValueKind kind,
const char *key, const char *value)
{
auto& nameValuePairs = *reinterpret_cast<RequestContext::NameValuePairs*>(nvp);
nameValuePairs.push_back({key, value});
return MHD_YES;
}
} // unnamed namespace
MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
const char* fullUrl,
const char* method,
@@ -535,7 +542,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
}
const auto url = fullURL2LocalURL(fullUrl, m_rootPrefixOfDecodedURL);
RequestContext request(connection, m_root, url, method, version);
RequestContext::NameValuePairs headers, queryArgs;
MHD_get_connection_values(connection, MHD_HEADER_KIND, add_name_value_pair, &headers);
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, add_name_value_pair, &queryArgs);
RequestContext request(m_root, url, method, version, headers, queryArgs);
if (m_verbose.load() ) {
request.print_debug_info();
@@ -564,7 +574,7 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
response->set_etag_body(getLibraryId());
}
auto ret = response->send(request, connection);
auto ret = response->send(request, m_verbose.load(), connection);
auto end_time = std::chrono::steady_clock::now();
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
if (m_verbose.load()) {
@@ -593,20 +603,19 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
{
try {
if (! request.is_valid_url()) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
if ( request.get_url() == "" ) {
// Redirect /ROOT_LOCATION to /ROOT_LOCATION/ (note the added slash)
// so that relative URLs are resolved correctly
const std::string query = getSearchComponent(request);
return Response::build_redirect(*this, m_root + "/" + query);
return Response::build_redirect(m_root + "/" + query);
}
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
if ( etag )
return Response::build_304(*this, etag);
return Response::build_304(etag);
const auto url = request.get_url();
if ( isLocallyCustomizedResource(url) )
@@ -647,15 +656,15 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
const std::string contentUrl = m_root + "/content" + urlEncode(url);
const std::string query = getSearchComponent(request);
return Response::build_redirect(*this, contentUrl + query);
return Response::build_redirect(contentUrl + query);
} catch (std::exception& e) {
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
return HTTP500Response(*this, request)
+ e.what();
return HTTP500Response(request)
+ ParameterizedMessage("non-translated-text", {{"MSG", e.what()}});
} catch (...) {
fprintf(stderr, "===== Unhandled unknown error\n");
return HTTP500Response(*this, request)
+ "Unknown error";
return HTTP500Response(request)
+ nonParameterizedMessage("unknown-error");
}
}
@@ -668,7 +677,7 @@ MustacheData InternalServer::get_default_data() const
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
{
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
return ContentResponse::build(m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
}
/**
@@ -697,8 +706,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
}
if ( startsWith(request.get_url(), "/suggest/") ) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
std::string bookName, bookId;
@@ -712,7 +720,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
}
if (archive == nullptr) {
return HTTP404Response(*this, request)
return HTTP404Response(request)
+ noSuchBookErrorMsg(bookName);
}
@@ -747,7 +755,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
results.addFTSearchSuggestion(request.get_user_language(), queryString);
}
return ContentResponse::build(*this, results.getJSON(), "application/json; charset=utf-8");
return ContentResponse::build(results.getJSON(), "application/json; charset=utf-8");
}
std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestContext& request)
@@ -759,10 +767,9 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
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" },
{"default_user_language", request.get_user_language() }
{"enable_library_button", m_withLibraryButton ? "true" : "false" }
};
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
return ContentResponse::build(RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
}
std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const
@@ -817,19 +824,13 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
const auto bookId = mp_nameMapper->getIdForName(urlParts[2]);
content = getNoJSDownloadPageHTML(bookId, userLang);
} catch (const std::out_of_range&) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
} else {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
return ContentResponse::build(
*this,
content,
"text/html; charset=utf-8"
);
return ContentResponse::build(content, "text/html; charset=utf-8");
}
namespace
@@ -867,14 +868,12 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
try {
const auto accessType = staticResourceAccessType(request, resourceCacheId);
auto response = ContentResponse::build(
*this,
getResource(resourceName),
getMimeTypeForFile(resourceName));
response->set_kind(accessType);
return std::move(response);
} catch (const ResourceNotFound& e) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
}
@@ -887,20 +886,17 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
if ( startsWith(request.get_url(), "/search/") ) {
if (request.get_url() == "/search/searchdescription.xml") {
return ContentResponse::build(
*this,
RESOURCE::ft_opensearchdescription_xml,
get_default_data(),
"application/opensearchdescription+xml");
}
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
try {
return handle_search_request(request);
} catch (const Error& e) {
return HTTP400Response(*this, request)
+ invalidUrlMsg
return HTTP400Response(request)
+ e.message();
}
}
@@ -942,10 +938,11 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
// 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,
HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND,
"fulltext-search-unavailable",
"404-page-heading",
cssUrl);
cssUrl,
/*includeKiwixResponseData=*/true);
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.
@@ -970,15 +967,14 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
renderer.setProtocolPrefix(m_root + "/content/");
renderer.setSearchProtocolPrefix(m_root + "/search");
renderer.setPageLength(pageLength);
renderer.setUserLang(request.get_user_language());
if (request.get_requested_format() == "xml") {
return ContentResponse::build(
*this,
renderer.getXml(*mp_nameMapper, mp_library.get()),
"application/rss+xml; charset=utf-8"
);
}
auto response = ContentResponse::build(
*this,
renderer.getHtml(*mp_nameMapper, mp_library.get()),
"text/html; charset=utf-8"
);
@@ -1001,8 +997,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
}
if ( startsWith(request.get_url(), "/random/") ) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
std::string bookName;
@@ -1016,7 +1011,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
}
if (archive == nullptr) {
return HTTP404Response(*this, request)
return HTTP404Response(request)
+ noSuchBookErrorMsg(bookName);
}
@@ -1024,7 +1019,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
auto entry = archive->getRandomEntry();
return build_redirect(bookName, getFinalItem(*archive, entry));
} catch(zim::EntryNotFound& e) {
return HTTP404Response(*this, request)
return HTTP404Response(request)
+ nonParameterizedMessage("random-article-failure");
}
}
@@ -1037,13 +1032,12 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
} catch (const std::out_of_range& e) {}
if (source.empty()) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
auto data = get_default_data();
data.set("source", source);
return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
return ContentResponse::build(RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
}
std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request)
@@ -1056,8 +1050,7 @@ std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& req
return handle_captured_external(request);
}
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
std::vector<std::string>
@@ -1117,7 +1110,7 @@ InternalServer::build_redirect(const std::string& bookName, const zim::Item& ite
{
const auto contentPath = "/content/" + bookName + "/" + item.getPath();
const auto url = m_root + kiwix::urlEncode(contentPath);
return Response::build_redirect(*this, url);
return Response::build_redirect(url);
}
std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& request)
@@ -1141,15 +1134,14 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
if (archive == nullptr) {
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern);
return HTTP404Response(*this, request)
+ urlNotFoundMsg
return UrlNotFoundResponse(request)
+ 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);
return Response::build_304(etag);
auto urlStr = url.substr(prefixLength + bookName.size());
if (urlStr[0] == '/') {
@@ -1168,7 +1160,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
// '-' namespaces, in which case that resource is returned instead.
return build_redirect(bookName, getFinalItem(*archive, entry));
}
auto response = ItemResponse::build(*this, request, entry.getItem());
auto response = ItemResponse::build(request, entry.getItem());
response->set_etag_body(archiveUuid);
if ( !startsWith(entry.getItem().getMimetype(), "application/pdf") ) {
@@ -1189,8 +1181,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
printf("Failed to find %s\n", urlStr.c_str());
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern);
return HTTP404Response(*this, request)
+ urlNotFoundMsg
return UrlNotFoundResponse(request)
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
}
}
@@ -1208,13 +1199,11 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
bookName = request.get_url_part(1);
kind = request.get_url_part(2);
} catch (const std::out_of_range& e) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
if (kind != "meta" && kind!= "content") {
return HTTP404Response(*this, request)
+ urlNotFoundMsg
return UrlNotFoundResponse(request)
+ invalidRawAccessMsg(kind);
}
@@ -1225,15 +1214,14 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
} catch (const std::out_of_range& e) {}
if (archive == nullptr) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg
return UrlNotFoundResponse(request)
+ 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);
return Response::build_304(etag);
// Remove the beggining of the path:
// /raw/<bookName>/<kind>/foo
@@ -1244,7 +1232,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
try {
if (kind == "meta") {
auto item = archive->getMetadataItem(itemPath);
auto response = ItemResponse::build(*this, request, item);
auto response = ItemResponse::build(request, item);
response->set_etag_body(archiveUuid);
return response;
} else {
@@ -1252,7 +1240,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
if (entry.isRedirect()) {
return build_redirect(bookName, entry.getItem(true));
}
auto response = ItemResponse::build(*this, request, entry.getItem());
auto response = ItemResponse::build(request, entry.getItem());
response->set_etag_body(archiveUuid);
return response;
}
@@ -1260,8 +1248,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
if (m_verbose.load()) {
printf("Failed to find %s\n", itemPath.c_str());
}
return HTTP404Response(*this, request)
+ urlNotFoundMsg
return UrlNotFoundResponse(request)
+ rawEntryNotFoundMsg(kind, itemPath);
}
}
@@ -1286,12 +1273,10 @@ std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(con
auto byteRange = request.get_range().resolve(resourceData.size());
if (byteRange.kind() != ByteRange::RESOLVED_FULL_CONTENT) {
return Response::build_416(*this, resourceData.size());
return Response::build_416(resourceData.size());
}
return ContentResponse::build(*this,
resourceData,
crd.mimeType);
return ContentResponse::build(resourceData, crd.mimeType);
}
}

View File

@@ -188,10 +188,6 @@ class InternalServer {
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);
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
};
}

View File

@@ -63,8 +63,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
host = request.get_header("Host");
url = request.get_url_part(1);
} catch (const std::out_of_range&) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
if (url == "v2") {
@@ -72,12 +71,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
if (url == "searchdescription.xml") {
auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
auto response = ContentResponse::build(RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
return std::move(response);
}
@@ -95,7 +93,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
auto response = ContentResponse::build(
*this,
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
opdsMimeType[OPDS_ACQUISITION_FEED]);
return std::move(response);
@@ -111,15 +108,14 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
try {
url = request.get_url_part(2);
} catch (const std::out_of_range&) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
if (url == "root.xml") {
return handle_catalog_v2_root(request);
} else if (url == "searchdescription.xml") {
const std::string endpoint_root = m_root + "/catalog/v2";
return ContentResponse::build(*this,
return ContentResponse::build(
RESOURCE::catalog_v2_searchdescription_xml,
kainjow::mustache::object({{"endpoint_root", endpoint_root}}),
"application/opensearchdescription+xml"
@@ -138,8 +134,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
} else if (url == "illustration") {
return handle_catalog_v2_illustration(request);
} else {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
}
@@ -147,7 +142,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
{
const std::string libraryId = getLibraryId();
return ContentResponse::build(
*this,
RESOURCE::templates::catalog_v2_root_xml,
kainjow::mustache::object{
{"date", gen_date_str()},
@@ -170,7 +164,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const Reques
const auto bookIds = search_catalog(request, opdsDumper);
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
return ContentResponse::build(
*this,
opdsFeed,
opdsMimeType[OPDS_ACQUISITION_FEED]
);
@@ -181,8 +174,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
try {
mp_library->getBookById(entryId);
} catch (const std::out_of_range&) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
@@ -190,7 +182,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
opdsDumper.setLibraryId(getLibraryId());
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
return ContentResponse::build(
*this,
opdsFeed,
opdsMimeType[OPDS_ENTRY]
);
@@ -202,7 +193,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build(
*this,
opdsDumper.categoriesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED]
);
@@ -214,7 +204,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const Requ
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build(
*this,
opdsDumper.languagesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED]
);
@@ -228,13 +217,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
auto size = request.get_argument<unsigned int>("size");
auto illustration = book.getIllustration(size);
return ContentResponse::build(
*this,
illustration->getData(),
illustration->mimeType
);
} catch(...) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
return UrlNotFoundResponse(request);
}
}

View File

@@ -51,11 +51,12 @@ RequestMethod str2RequestMethod(const std::string& method) {
} // unnamed namespace
RequestContext::RequestContext(struct MHD_Connection* connection,
const std::string& _rootLocation, // URI-encoded
RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded
const std::string& unrootedUrl, // URI-decoded
const std::string& _method,
const std::string& version) :
const std::string& version,
const NameValuePairs& headers,
const NameValuePairs& queryArgs) :
rootLocation(_rootLocation),
url(unrootedUrl),
method(str2RequestMethod(_method)),
@@ -64,9 +65,13 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
acceptEncodingGzip(false),
byteRange_()
{
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);
for ( const auto& kv : headers ) {
add_header(kv.first, kv.second);
}
for ( const auto& kv : queryArgs ) {
add_argument(kv.first, kv.second);
}
try {
acceptEncodingGzip =
@@ -83,18 +88,14 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
RequestContext::~RequestContext()
{}
MHD_Result RequestContext::fill_header(void *__this, enum MHD_ValueKind kind,
const char *key, const char *value)
void RequestContext::add_header(const char *key, const char *value)
{
RequestContext *_this = static_cast<RequestContext*>(__this);
_this->headers[lcAll(key)] = value;
return MHD_YES;
this->headers[lcAll(key)] = value;
}
MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
const char *key, const char* value)
void RequestContext::add_argument(const char *key, const char* value)
{
RequestContext *_this = static_cast<RequestContext*>(__this);
RequestContext *_this = this;
_this->arguments[key].push_back(value == nullptr ? "" : value);
if ( ! _this->queryString.empty() ) {
_this->queryString += "&";
@@ -104,15 +105,6 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
_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;
}
void RequestContext::print_debug_info() const {

View File

@@ -29,7 +29,7 @@
#include <stdexcept>
#include "byte_range.h"
#include "tools/stringTools.h"
#include "../tools/stringTools.h"
extern "C" {
#include "microhttpd_wrapper.h"
@@ -55,12 +55,17 @@ class IndexError: public std::runtime_error {};
class RequestContext {
public: // types
typedef std::vector<std::pair<const char*, const char*>> NameValuePairs;
public: // functions
RequestContext(struct MHD_Connection* connection,
const std::string& rootLocation, // URI-encoded
RequestContext(const std::string& rootLocation, // URI-encoded
const std::string& unrootedUrl, // URI-decoded
const std::string& method,
const std::string& version);
const std::string& version,
const NameValuePairs& headers,
const NameValuePairs& queryArgs);
~RequestContext();
void print_debug_info() const;
@@ -145,16 +150,14 @@ 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*);
void add_header(const char* name, const char* value);
void add_argument(const char* name, const char* value);
};
template<> std::string RequestContext::get_argument(const std::string& name) const;

View File

@@ -32,6 +32,9 @@
#include <zlib.h>
#include <array>
#include <list>
#include <map>
#include <regex>
// This is somehow a magic value.
// If this value is too small, we will compress (and lost cpu time) too much
@@ -47,6 +50,8 @@ namespace kiwix {
namespace
{
typedef kainjow::mustache::data MustacheData;
// some utilities
std::string get_mime_type(const zim::Item& item)
@@ -119,9 +124,8 @@ const char* getCacheControlHeader(Response::Kind k)
} // unnamed namespace
Response::Response(bool verbose)
: m_verbose(verbose),
m_returnCode(MHD_HTTP_OK)
Response::Response()
: m_returnCode(MHD_HTTP_OK)
{
add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
}
@@ -133,14 +137,14 @@ void Response::set_kind(Kind k)
m_etag.set_option(ETag::ZIM_CONTENT);
}
std::unique_ptr<Response> Response::build(const InternalServer& server)
std::unique_ptr<Response> Response::build()
{
return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
return std::make_unique<Response>();
}
std::unique_ptr<Response> Response::build_304(const InternalServer& server, const ETag& etag)
std::unique_ptr<Response> Response::build_304(const ETag& etag)
{
auto response = Response::build(server);
auto response = Response::build();
response->set_code(MHD_HTTP_NOT_MODIFIED);
response->m_etag = etag;
if ( etag.get_option(ETag::ZIM_CONTENT) ) {
@@ -152,67 +156,260 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
return response;
}
const UrlNotFoundMsg urlNotFoundMsg;
const InvalidUrlMsg invalidUrlMsg;
std::string ContentResponseBlueprint::getMessage(const std::string& msgId) const
namespace
{
return getTranslatedString(m_request.get_user_language(), msgId);
// This class was introduced in order to work around the missing support
// for std::variant (and std::optional) under some of the current build
// platforms.
template<class T>
class Optional
{
public: // functions
Optional() {}
Optional(const T& t) : ptr(new T(t)) {}
Optional(const Optional& o) : ptr(o.has_value() ? new T(*o) : nullptr) {}
Optional(Optional&& o) : ptr(std::move(o.ptr)) {}
Optional& operator=(const Optional& o)
{
*this = Optional(o);
return *this;
}
Optional& operator=(Optional&& o)
{
ptr = std::move(o.ptr);
return *this;
}
bool has_value() const { return ptr.get() != nullptr; }
const T& operator*() const { return *ptr; }
T& operator*() { return *ptr; }
private: // data
std::unique_ptr<T> ptr;
};
} // unnamed namespace
class ContentResponseBlueprint::Data
{
public:
typedef std::list<Data> List;
typedef std::map<std::string, Data> Object;
private:
// std::variant<std::string, bool, List, Object> data;
// XXX: libkiwix is compiled on platforms where std::variant
// XXX: is not yet supported. Hence this hack. Only one
// XXX: of the below data members is expected to contain a value.
Optional<std::string> m_stringValue;
Optional<bool> m_boolValue;
Optional<List> m_listValue;
Optional<Object> m_objectValue;
public:
Data() {}
Data(const std::string& s) : m_stringValue(s) {}
Data(bool b) : m_boolValue(b) {}
Data(const List& l) : m_listValue(l) {}
Data(const Object& o) : m_objectValue(o) {}
MustacheData toMustache(const std::string& lang) const;
Data& operator[](const std::string& key)
{
return (*m_objectValue)[key];
}
void push_back(const Data& d) { (*m_listValue).push_back(d); }
static Data onlyAsNonEmptyValue(const std::string& s)
{
return s.empty() ? Data(false) : Data(s);
}
static Data from(const ParameterizedMessage& pmsg)
{
Object obj;
for(const auto& kv : pmsg.getParams()) {
obj[kv.first] = kv.second;
}
return Object{
{ "msgid", pmsg.getMsgId() },
{ "params", Data(obj) }
};
}
std::string asJSON() const;
void dumpJSON(std::ostream& os) const;
private:
bool isString() const { return m_stringValue.has_value(); }
bool isList() const { return m_listValue.has_value(); }
bool isObject() const { return m_objectValue.has_value(); }
const std::string& stringValue() const { return *m_stringValue; }
bool boolValue() const { return *m_boolValue; }
const List& listValue() const { return *m_listValue; }
const Object& objectValue() const { return *m_objectValue; }
const Data* get(const std::string& key) const
{
if ( !isObject() )
return nullptr;
const auto& obj = objectValue();
const auto it = obj.find(key);
return it != obj.end() ? &it->second : nullptr;
}
};
MustacheData ContentResponseBlueprint::Data::toMustache(const std::string& lang) const
{
if ( this->isList() ) {
kainjow::mustache::list l;
for ( const auto& x : this->listValue() ) {
l.push_back(x.toMustache(lang));
}
return l;
} else if ( this->isObject() ) {
const Data* msgId = this->get("msgid");
const Data* msgParams = this->get("params");
if ( msgId && msgId->isString() && msgParams && msgParams->isObject() ) {
std::map<std::string, std::string> params;
for(const auto& kv : msgParams->objectValue()) {
params[kv.first] = kv.second.stringValue();
}
const ParameterizedMessage msg(msgId->stringValue(), ParameterizedMessage::Parameters(params));
return msg.getText(lang);
} else {
kainjow::mustache::object o;
for ( const auto& kv : this->objectValue() ) {
o[kv.first] = kv.second.toMustache(lang);
}
return o;
}
} else if ( this->isString() ) {
return this->stringValue();
} else {
return this->boolValue();
}
}
void ContentResponseBlueprint::Data::dumpJSON(std::ostream& os) const
{
if ( this->isString() ) {
os << '"' << escapeForJSON(this->stringValue()) << '"';
} else if ( this->isList() ) {
const char * sep = " ";
os << "[";
for ( const auto& x : this->listValue() ) {
os << sep;
x.dumpJSON(os);
sep = ", ";
}
os << " ]";
} else if ( this->isObject() ) {
const char * sep = " ";
os << "{";
for ( const auto& kv : this->objectValue() ) {
os << sep << '"' << kv.first << "\" : ";
kv.second.dumpJSON(os);
sep = ", ";
}
os << " }";
} else {
os << (this->boolValue() ? "true" : "false");
}
}
std::string ContentResponseBlueprint::Data::asJSON() const
{
std::ostringstream oss;
this->dumpJSON(oss);
// This JSON is going to be used in HTML inside a <script></script> tag.
// If it contains "</script>" (or "</script >") as a substring, then the HTML
// parser will be confused. Since for a valid JSON that may happen only inside
// a JSON string, we can safely take advantage of the answers to
// https://stackoverflow.com/questions/28259389/how-to-put-script-in-a-javascript-string
// and work around the issue by inserting an otherwise harmless backslash.
return std::regex_replace(oss.str(), std::regex("</script"), "</scr\\ipt");
}
ContentResponseBlueprint::ContentResponseBlueprint(const RequestContext* request,
int httpStatusCode,
const std::string& mimeType,
const std::string& templateStr,
bool includeKiwixResponseData)
: m_request(*request)
, m_httpStatusCode(httpStatusCode)
, m_mimeType(mimeType)
, m_template(templateStr)
, m_includeKiwixResponseData(includeKiwixResponseData)
, m_data(new Data)
{}
ContentResponseBlueprint::~ContentResponseBlueprint() = default;
std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObject() const
{
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
kainjow::mustache::data d = m_data->toMustache(m_request.get_user_language());
if ( m_includeKiwixResponseData ) {
d.set("KIWIX_RESPONSE_TEMPLATE", escapeForJSON(m_template, false));
d.set("KIWIX_RESPONSE_DATA", m_data->asJSON());
}
auto r = ContentResponse::build(m_template, d, m_mimeType);
r->set_code(m_httpStatusCode);
return r;
}
HTTPErrorResponse::HTTPErrorResponse(const InternalServer& server,
const RequestContext& request,
HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request,
int httpStatusCode,
const std::string& pageTitleMsgId,
const std::string& headingMsgId,
const std::string& cssUrl)
: ContentResponseBlueprint(&server,
&request,
const std::string& cssUrl,
bool includeKiwixResponseData)
: ContentResponseBlueprint(&request,
httpStatusCode,
request.get_requested_format() == "html" ? "text/html; charset=utf-8" : "application/xml; charset=utf-8",
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml)
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml,
includeKiwixResponseData)
{
kainjow::mustache::list emptyList;
this->m_data = kainjow::mustache::object{
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) },
{"PAGE_TITLE", getMessage(pageTitleMsgId)},
{"PAGE_HEADING", getMessage(headingMsgId)},
Data::List emptyList;
*this->m_data = Data(Data::Object{
{"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) },
{"PAGE_TITLE", Data::from(nonParameterizedMessage(pageTitleMsgId))},
{"PAGE_HEADING", Data::from(nonParameterizedMessage(headingMsgId))},
{"details", emptyList}
};
});
}
HTTP404Response::HTTP404Response(const InternalServer& server,
const RequestContext& request)
: HTTPErrorResponse(server,
request,
HTTP404Response::HTTP404Response(const RequestContext& request)
: HTTPErrorResponse(request,
MHD_HTTP_NOT_FOUND,
"404-page-title",
"404-page-heading")
"404-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
{
}
HTTPErrorResponse& HTTP404Response::operator+(UrlNotFoundMsg /*unused*/)
UrlNotFoundResponse::UrlNotFoundResponse(const RequestContext& request)
: HTTP404Response(request)
{
const std::string requestUrl = urlDecode(m_request.get_full_url(), false);
return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}});
}
HTTPErrorResponse& HTTPErrorResponse::operator+(const std::string& msg)
{
m_data["details"].push_back({"p", msg});
return *this;
*this += ParameterizedMessage("url-not-found", {{"url", requestUrl}});
}
HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details)
{
return *this + details.getText(m_request.get_user_language());
(*m_data)["details"].push_back(Data::Object{{"p", Data::from(details)}});
return *this;
}
HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& details)
@@ -222,50 +419,36 @@ HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& det
}
HTTP400Response::HTTP400Response(const InternalServer& server,
const RequestContext& request)
: HTTPErrorResponse(server,
request,
HTTP400Response::HTTP400Response(const RequestContext& request)
: HTTPErrorResponse(request,
MHD_HTTP_BAD_REQUEST,
"400-page-title",
"400-page-heading")
{
}
HTTPErrorResponse& HTTP400Response::operator+(InvalidUrlMsg /*unused*/)
"400-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
{
std::string requestUrl = urlDecode(m_request.get_full_url(), false);
const auto query = m_request.get_query();
if (!query.empty()) {
requestUrl += "?" + encodeDiples(query);
}
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{{url}}}" is not a valid request.)");
return *this + msgTmpl.render({"url", requestUrl});
*this += ParameterizedMessage("invalid-request", {{"url", requestUrl}});
}
HTTP500Response::HTTP500Response(const InternalServer& server,
const RequestContext& request)
: HTTPErrorResponse(server,
request,
HTTP500Response::HTTP500Response(const RequestContext& request)
: HTTPErrorResponse(request,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"500-page-title",
"500-page-heading")
"500-page-heading",
std::string(),
/*includeKiwixResponseData=*/true)
{
// operator+() is a state-modifying operator (akin to operator+=)
*this + "An internal server error occured. We are sorry about that :/";
*this += nonParameterizedMessage("500-page-text");
}
std::unique_ptr<ContentResponse> HTTP500Response::generateResponseObject() const
std::unique_ptr<Response> Response::build_416(size_t resourceLength)
{
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;
}
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
{
auto response = Response::build(server);
auto response = Response::build();
// [FIXME] (compile with recent enough version of libmicrohttpd)
// response->set_code(MHD_HTTP_RANGE_NOT_SATISFIABLE);
response->set_code(416);
@@ -277,9 +460,9 @@ std::unique_ptr<Response> Response::build_416(const InternalServer& server, size
}
std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
std::unique_ptr<Response> Response::build_redirect(const std::string& redirectUrl)
{
auto response = Response::build(server);
auto response = Response::build();
response->m_returnCode = MHD_HTTP_FOUND;
response->add_header(MHD_HTTP_HEADER_LOCATION, redirectUrl);
return response;
@@ -374,7 +557,7 @@ ContentResponse::create_mhd_response(const RequestContext& request)
return response;
}
MHD_Result Response::send(const RequestContext& request, MHD_Connection* connection)
MHD_Result Response::send(const RequestContext& request, bool verbose, MHD_Connection* connection)
{
MHD_Response* response = create_mhd_response(request);
@@ -390,7 +573,7 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
if (m_verbose)
if (verbose)
print_response_info(m_returnCode, response);
auto ret = MHD_queue_response(connection, m_returnCode, response);
@@ -398,9 +581,8 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
return ret;
}
ContentResponse::ContentResponse(const std::string& root, bool verbose, const std::string& content, const std::string& mimetype) :
Response(verbose),
m_root(root),
ContentResponse::ContentResponse(const std::string& content, const std::string& mimetype) :
Response(),
m_content(content),
m_mimeType(mimetype)
{
@@ -408,29 +590,23 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, const st
}
std::unique_ptr<ContentResponse> ContentResponse::build(
const InternalServer& server,
const std::string& content,
const std::string& mimetype)
{
return std::unique_ptr<ContentResponse>(new ContentResponse(
server.m_root,
server.m_verbose.load(),
content,
mimetype));
return std::make_unique<ContentResponse>(content, mimetype);
}
std::unique_ptr<ContentResponse> ContentResponse::build(
const InternalServer& server,
const std::string& template_str,
kainjow::mustache::data data,
const std::string& mimetype)
{
auto content = render_template(template_str, data);
return ContentResponse::build(server, content, mimetype);
return ContentResponse::build(content, mimetype);
}
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
Response(verbose),
ItemResponse::ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
Response(),
m_item(item),
m_mimeType(mimetype)
{
@@ -439,30 +615,26 @@ ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::strin
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
}
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item)
std::unique_ptr<Response> ItemResponse::build(const 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);
auto response = ContentResponse::build(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());
auto response = Response::build_416(item.getSize());
response->set_kind(Response::ZIM_CONTENT);
return response;
}
return std::unique_ptr<Response>(new ItemResponse(
server.m_verbose.load(),
item,
mimetype,
byteRange));
return std::make_unique<ItemResponse>(item, mimetype, byteRange);
}
MHD_Response*

View File

@@ -41,7 +41,6 @@ class Archive;
namespace kiwix {
class InternalServer;
class RequestContext;
class Response {
@@ -54,15 +53,15 @@ class Response {
};
public:
Response(bool verbose);
Response();
virtual ~Response() = default;
static std::unique_ptr<Response> build(const InternalServer& server);
static std::unique_ptr<Response> build_304(const InternalServer& server, const ETag& etag);
static std::unique_ptr<Response> build_416(const InternalServer& server, size_t resourceLength);
static std::unique_ptr<Response> build_redirect(const InternalServer& server, const std::string& redirectUrl);
static std::unique_ptr<Response> build();
static std::unique_ptr<Response> build_304(const ETag& etag);
static std::unique_ptr<Response> build_416(size_t resourceLength);
static std::unique_ptr<Response> build_redirect(const std::string& redirectUrl);
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
MHD_Result send(const RequestContext& request, bool verbose, MHD_Connection* connection);
void set_code(int code) { m_returnCode = code; }
void set_kind(Kind k);
@@ -78,7 +77,6 @@ class Response {
protected: // data
Kind m_kind = DYNAMIC_CONTENT;
bool m_verbose;
int m_returnCode;
ByteRange m_byteRange;
ETag m_etag;
@@ -91,22 +89,21 @@ class Response {
class ContentResponse : public Response {
public:
ContentResponse(
const std::string& root,
bool verbose,
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);
static std::unique_ptr<ContentResponse> build(
const InternalServer& server,
const std::string& template_str,
kainjow::mustache::data data,
const std::string& mimetype);
const std::string& getContent() const { return m_content; }
const std::string& getMimeType() const { return m_mimeType; }
private:
MHD_Response* create_mhd_response(const RequestContext& request);
@@ -114,7 +111,6 @@ class ContentResponse : public Response {
private:
std::string m_root;
std::string m_content;
std::string m_mimeType;
};
@@ -122,99 +118,70 @@ class ContentResponse : public Response {
class ContentResponseBlueprint
{
public: // functions
ContentResponseBlueprint(const InternalServer* server,
const RequestContext* request,
ContentResponseBlueprint(const RequestContext* request,
int httpStatusCode,
const std::string& mimeType,
const std::string& templateStr)
: m_server(*server)
, m_request(*request)
, m_httpStatusCode(httpStatusCode)
, m_mimeType(mimeType)
, m_template(templateStr)
{}
const std::string& templateStr,
bool includeKiwixResponseData = false);
virtual ~ContentResponseBlueprint() = default;
~ContentResponseBlueprint();
operator std::unique_ptr<ContentResponse>() const
operator std::unique_ptr<Response>() const
{
return generateResponseObject();
}
operator std::unique_ptr<Response>() const
{
return operator std::unique_ptr<ContentResponse>();
}
std::unique_ptr<ContentResponse> generateResponseObject() const;
protected: // types
class Data;
protected: // functions
std::string getMessage(const std::string& msgId) const;
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
public: //data
const InternalServer& m_server;
protected: //data
const RequestContext& m_request;
const int m_httpStatusCode;
const std::string m_mimeType;
const std::string m_template;
kainjow::mustache::data m_data;
const bool m_includeKiwixResponseData;
std::unique_ptr<Data> m_data;
};
struct HTTPErrorResponse : ContentResponseBlueprint
{
HTTPErrorResponse(const InternalServer& server,
const RequestContext& request,
HTTPErrorResponse(const RequestContext& request,
int httpStatusCode,
const std::string& pageTitleMsgId,
const std::string& headingMsgId,
const std::string& cssUrl = "");
const std::string& cssUrl = "",
bool includeKiwixResponseData = false);
HTTPErrorResponse& operator+(const std::string& msg);
HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
};
class UrlNotFoundMsg {};
extern const UrlNotFoundMsg urlNotFoundMsg;
struct HTTP404Response : HTTPErrorResponse
{
HTTP404Response(const InternalServer& server,
const RequestContext& request);
using HTTPErrorResponse::operator+;
HTTPErrorResponse& operator+(UrlNotFoundMsg /*unused*/);
explicit HTTP404Response(const RequestContext& request);
};
class InvalidUrlMsg {};
extern const InvalidUrlMsg invalidUrlMsg;
struct UrlNotFoundResponse : HTTP404Response
{
explicit UrlNotFoundResponse(const RequestContext& request);
};
struct HTTP400Response : HTTPErrorResponse
{
HTTP400Response(const InternalServer& server,
const RequestContext& request);
using HTTPErrorResponse::operator+;
HTTPErrorResponse& operator+(InvalidUrlMsg /*unused*/);
explicit HTTP400Response(const RequestContext& request);
};
struct HTTP500Response : HTTPErrorResponse
{
HTTP500Response(const InternalServer& server,
const RequestContext& request);
private: // overrides
// generateResponseObject() is overriden in order to produce a minimal
// response without any need for additional resources from the server
std::unique_ptr<ContentResponse> generateResponseObject() const override;
explicit HTTP500Response(const RequestContext& request);
};
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);
ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
static std::unique_ptr<Response> build(const RequestContext& request, const zim::Item& item);
private:
MHD_Response* create_mhd_response(const RequestContext& request);

View File

@@ -43,6 +43,10 @@
#include <netdb.h>
#endif
#ifdef __HAIKU__
#include <sys/sockio.h>
#endif
size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdata)
{
auto str = static_cast<std::stringstream*>(userdata);

View File

@@ -327,17 +327,27 @@ std::string kiwix::render_template(const std::string& template_str, kainjow::mus
return ss.str();
}
namespace
{
// The escapeQuote parameter of escapeForJSON() defaults to true.
// This constant makes the calls to escapeForJSON() where the quote symbol
// should not be escaped (as it is later replaced with the HTML character entity
// &quot;) more readable.
static const bool DONT_ESCAPE_QUOTE = false;
std::string escapeForJSON(const std::string& s)
std::string kiwix::escapeForJSON(const std::string& s, bool escapeQuote)
{
std::ostringstream oss;
for (char c : s) {
if ( c == '\\' ) {
oss << "\\\\";
} else if ( unsigned(c) < 0x20U ) {
oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c);
switch ( c ) {
case '\n': oss << "\\n"; break;
case '\r': oss << "\\r"; break;
case '\t': oss << "\\t"; break;
default: oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c);
}
} else if ( c == '"' && escapeQuote ) {
oss << "\\\"";
} else {
oss << c;
}
@@ -345,6 +355,9 @@ std::string escapeForJSON(const std::string& s)
return oss.str();
}
namespace
{
std::string makeFulltextSearchSuggestion(const std::string& lang,
const std::string& queryString)
{
@@ -370,10 +383,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
? suggestion.getSnippet()
: suggestion.getTitle();
result.set("label", escapeForJSON(label));
result.set("value", escapeForJSON(suggestion.getTitle()));
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
result.set("kind", "path");
result.set("path", escapeForJSON(suggestion.getPath()));
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
result.set("first", m_data.is_empty_list());
m_data.push_back(result);
}
@@ -383,8 +396,8 @@ void kiwix::Suggestions::addFTSearchSuggestion(const std::string& uiLang,
{
kainjow::mustache::data result;
const std::string label = makeFulltextSearchSuggestion(uiLang, queryString);
result.set("label", escapeForJSON(label));
result.set("value", escapeForJSON(queryString + " "));
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
result.set("value", escapeForJSON(queryString + " ", DONT_ESCAPE_QUOTE));
result.set("kind", "pattern");
result.set("first", m_data.is_empty_list());
m_data.push_back(result);

View File

@@ -53,6 +53,7 @@ private:
const icu::Locale locale;
};
std::string escapeForJSON(const std::string& s, bool escapeQuote = true);
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
* difference that the slash (/) symbol is NOT encoded. */

View File

@@ -30,7 +30,10 @@ def get_translation_info(filepath):
with open(filepath, 'r', encoding="utf-8") as f:
content = json.load(f)
lang_name = content.get("name")
return lang_code, lang_name
translation_count = len(content)
return dict(iso_code=lang_code,
self_name=lang_name,
translation_count=translation_count)
language_list = []
json_files = translation_dir.glob("*.json")
@@ -40,14 +43,14 @@ with open(resource_file, 'w', encoding="utf-8") as f:
continue
print("Processing", i18n_file.name)
if i18n_file.name != "test.json":
lang_code, lang_name = get_translation_info(i18n_file)
translation_info = get_translation_info(i18n_file)
lang_name = translation_info["self_name"]
if lang_name:
language_list.append((lang_code, lang_name))
language_list.append(translation_info)
else:
print(f"Warning: missing 'name' in {i18n_file.name}")
f.write(str(i18n_file.relative_to(script_path.parent)) + '\n')
language_list = [{name: code} for code, name in sorted(language_list)]
language_list_jsobj_str = json.dumps(language_list,
indent=2,
ensure_ascii=False)

View File

@@ -1,6 +1,8 @@
skin/i18n/ar.json
skin/i18n/bn.json
skin/i18n/br.json
skin/i18n/cs.json
skin/i18n/dag.json
skin/i18n/de.json
skin/i18n/dga.json
skin/i18n/el.json
@@ -8,10 +10,12 @@ skin/i18n/en.json
skin/i18n/es.json
skin/i18n/fi.json
skin/i18n/fr.json
skin/i18n/ha.json
skin/i18n/he.json
skin/i18n/hi.json
skin/i18n/hy.json
skin/i18n/ia.json
skin/i18n/ig.json
skin/i18n/it.json
skin/i18n/ja.json
skin/i18n/ko.json

View File

@@ -17,6 +17,7 @@ skin/fonts/Poppins.ttf
skin/fonts/Roboto.ttf
skin/search_results.css
skin/blank.html
skin/polyfills.js
skin/viewer.js
skin/i18n.js
skin/languages.js

View File

@@ -69,15 +69,66 @@ function $t(msgId, params={}) {
}
}
const I18n = {
instantiateParameterizedMessages: function(data) {
if ( data.__proto__ == Array.prototype ) {
const result = [];
for ( const x of data ) {
result.push(this.instantiateParameterizedMessages(x));
}
return result;
} else if ( data.__proto__ == Object.prototype ) {
const msgId = data.msgid;
const msgParams = data.params;
if ( msgId && msgId.__proto__ == String.prototype && msgParams && msgParams.__proto__ == Object.prototype ) {
return $t(msgId, msgParams);
} else {
const result = {};
for ( const p in data ) {
result[p] = this.instantiateParameterizedMessages(data[p]);
}
return result;
}
} else {
return data;
}
},
render: function (template, params) {
params = this.instantiateParameterizedMessages(params);
return mustache.render(template, params);
}
}
const DEFAULT_UI_LANGUAGE = 'en';
Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true);
// Below function selects the most suitable UI language from the list
// of preferred languages in browser preferences and available translations.
// Since, unlike Accept-Language header, navigator.languages doesn't contain
// qvalues, they are computed using the same algorithm as in Firefox 121
function getDefaultUserLanguage() {
const mostSuitableLang = { code: DEFAULT_UI_LANGUAGE, score: 0 }
const n = navigator.languages.length;
for (const lang of uiLanguages ) {
const rank = navigator.languages.indexOf(lang.iso_code);
if ( rank >= 0 ) {
const qvalue = Math.round(10*(1 - rank/n))/10;
const score = qvalue * lang.translation_count;
if ( score > mostSuitableLang.score ) {
mostSuitableLang.code = lang.iso_code;
mostSuitableLang.score = score;
}
}
}
return mostSuitableLang.code;
}
function getUserLanguage() {
return new URLSearchParams(window.location.search).get('userlang')
|| window.localStorage.getItem('userlang')
|| viewerSettings.defaultUserLanguage
|| DEFAULT_UI_LANGUAGE;
|| getDefaultUserLanguage();
}
function setUserLanguage(lang, callback) {
@@ -133,10 +184,8 @@ function initUILanguageSelector(activeLanguage, languageChangeCallback) {
}
const languageSelector = document.getElementById("ui_language");
for (const lang of uiLanguages ) {
const lang_name = Object.getOwnPropertyNames(lang)[0];
const lang_code = lang[lang_name];
const is_selected = lang_code == activeLanguage;
languageSelector.appendChild(new Option(lang_name, lang_code, is_selected, is_selected));
const is_selected = lang.iso_code == activeLanguage;
languageSelector.appendChild(new Option(lang.self_name, lang.iso_code, is_selected, is_selected));
}
languageSelector.onchange = languageChangeCallback;
}
@@ -145,3 +194,4 @@ window.$t = $t;
window.getUserLanguage = getUserLanguage;
window.setUserLanguage = setUserLanguage;
window.initUILanguageSelector = initUILanguageSelector;
window.I18n = I18n;

View File

@@ -8,6 +8,8 @@
"404-page-heading": "পাওয়া যায়নি",
"500-page-title": "অভ্যন্তরীণ সার্ভার ত্রুটি",
"500-page-heading": "অভ্যন্তরীণ সার্ভার ত্রুটি",
"search-result-book-info": "{{BOOK_TITLE}} থেকে",
"word-count": "{{COUNT}}টি শব্দ",
"library-button-text": "স্বাগত পাতায় চলুন",
"home-button-text": "'{{BOOK_TITLE}}'-এর প্রধান পাতায় চলুন",
"searchbox-tooltip": "'{{BOOK_TITLE}}' অনুসন্ধান করুন",

42
static/skin/i18n/br.json Normal file
View File

@@ -0,0 +1,42 @@
{
"@metadata": {
"authors": [
"Adriendelucca",
"Y-M D"
]
},
"name": "brezhoneg",
"suggest-full-text-search": "E lec'h emañ \"{{{SEARCH_TERMS}}}\"...",
"no-such-book": "Neus ket eus al levr-mañ: {{BOOK_NAME}}",
"no-book-found": "Neus levr ebet a glot gant an dezverkoù-se",
"url-not-found": "Neo ket bet kavet an URL \"{{url}}\" goulennet war ar servijer-mañ.",
"random-article-failure": "Chaous! Nhon eus ket gellet dibab ur pennad dre ziouer evidoch :(",
"400-page-title": "Reked amwiriek",
"400-page-heading": "Reked amwiriek",
"404-page-heading": "N'eo ket bet kavet",
"500-page-title": "Fazi diabarzh ar servijer",
"500-page-heading": "Fazi diabarzh ar servijer",
"search-results-page-title": "Klask: {{SEARCH_PATTERN}}",
"search-results-page-header": "Disochoù <b>{{START}}-{{END}}</b> diwar <b>{{COUNT}}</b> evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Disoch ebet kavet evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "diouzh {{BOOK_TITLE}}",
"word-count": "{{COUNT}} a cherioù",
"library-button-text": "Mont dar bajenn degemer",
"home-button-text": "Mont da bajenn degemer \"{{BOOK_TITLE}}\"",
"random-page-button-text": "Mont dur bajenn dre zegouezh",
"searchbox-tooltip": "Klask '{{BOOK_TITLE}}'",
"powered-by-kiwix-html": "Lusket gant&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Klask",
"book-filtering-all-categories": "An holl rummadoù",
"book-filtering-all-languages": "An holl yezhoù",
"count-of-matching-books": "{{COUNT}} levr",
"download": "Pellgargañ",
"direct-download-link-text": "Eeun",
"filter-by-tag": "Silañ gant an dikedenn \"{{TAG}}\"",
"stop-filtering-by-tag": "Paouez da silañ gant an dikedenn \"{{TAG}}\"",
"welcome-to-kiwix-server": "Degemer mat er servijer Kiwix",
"download-links-heading": "Liammoù pellgargañ evit <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Pellgargañ al levr",
"preview-book": "Rakwelet",
"unknown-error": "Fazi dianav"
}

55
static/skin/i18n/dag.json Normal file
View File

@@ -0,0 +1,55 @@
{
"@metadata": {
"authors": [
"Kalakpagh",
"Ruky Wunpini"
]
},
"name": "Silimiinsili",
"suggest-full-text-search": "Gbubi la '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Lala buku kani:{{BOOK_NAME}}",
"too-many-books": "Buku nima pam ka bɛ daa suhi ({{NB_BOOKS}}) din ni ka tariga nyɛ {{LIMIT}}",
"no-book-found": "Buku kani lu zahim a ni piigi yaɣa shɛli",
"url-not-found": "URL \"{{url}}\" shɛli bɛ ni daa suhi daa kani n-ti tum tumda ŋɔ.",
"suggest-search": "Niŋmi lahabali pali vihigu zaŋ n-ti <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oops! Zaɣisiya ni di gahim piigi lahabali :(",
"invalid-raw-data-type": "{{DATATYPE}} nyɛla din suhibu bi niŋ viɛnyɛla zaŋ n-ti lahabali kahili.",
"invalid-request": "URL \"{{{url}}}\" shɛli bɛ ni daa suhi ŋɔ nyɛla din bi suhi viɛnyɛla.",
"no-value-for-arg": "Dariza shɛli bi ti zaŋ n-ti nangban'kpeeni {{ARGUMENT}}",
"no-query": "Yɛlshɛli bi yiina",
"raw-entry-not-found": "Ku tooi nya {{DATATYPE}} kpɛbu {{ENTRY}}",
"400-page-title": "Suhigu din bi niŋ viɛnyɛla",
"400-page-heading": "Suhigu din bi niŋ viɛnyɛla",
"404-page-title": "Lahabali kani",
"404-page-heading": "Kani",
"500-page-title": "Puuni tum tumda chiriŋ",
"500-page-heading": "Puuni tum tumda chiriŋ",
"500-page-text": "Puuni tum tumda chiriŋ niŋya. Ti niŋ yolitem zaŋ jɛndi li :/",
"fulltext-search-unavailable": "Lahabali pali vihigu kani",
"search-results-page-title": "Vihima:{{SEARCH_PATTERN}}",
"search-results-page-header": "Chaɣili nima <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Chaɣili daa kani zaŋ n-ti\n <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "yina {{BOOK_TITLE}}",
"word-count": "{{COUNT}} bachi nima",
"library-button-text": "Cham solɔɣu",
"home-button-text": "Cham yaɣili maŋmaŋ zaŋ n-ti\n'{{BOOK_TITLE}}'",
"random-page-button-text": "Cham gahim piigi yaɣili",
"searchbox-tooltip": "Vihima '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Buku nima ayi bee gari balli koŋkoba nyɛ din yɛn be vihigu ŋɔ ni ka di ni tooi chɛ ka di laasabu wali.",
"welcome-page-overzealous-filter": "Labisibu kani. A ni yu ni a\n<a href=\"{{URL}}\">reset filter</a>?",
"powered-by-kiwix-html": "Din niŋ li nyɛ &nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Vihima",
"book-filtering-all-categories": "Pubu zaa",
"book-filtering-all-languages": "Bala zaa",
"count-of-matching-books": "{{COUNT}} Buku(nima)",
"download": "Yihibu",
"direct-download-link-text": "Tibi",
"direct-download-alt-text": "Tibi deebu",
"hash-download-link-text": "Sha256 hash",
"hash-download-alt-text": "Deebu daliŋ",
"welcome-to-kiwix-server": "Maraba Kiwix tum tumda",
"download-links-heading": "Deemi soli zaŋ n-ti <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Yaa mi buku",
"preview-book": "Labi lihi",
"unknown-error": "Chiriŋ din bi tooi baŋ"
}

View File

@@ -3,6 +3,7 @@
"authors": [
"IMayBeABitShy",
"Lucas Werkmeister",
"Rofiatmustapha12",
"ThisCarthing"
]
},
@@ -15,6 +16,7 @@
"suggest-search": "Führe eine Volltextsuche nach <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> durch",
"random-article-failure": "Hoppla! Konnte keinen zufälligen Artikel auswählen :(",
"invalid-raw-data-type": "{{DATATYPE}} ist keine gültige Anfrage für unverarbeiteten Inhalt",
"invalid-request": "Die angeforderte URL „{{{url}}}“ ist keine gültige Anfrage.",
"no-value-for-arg": "Kein Wert für den Parameter {{ARGUMENT}} gegeben",
"no-query": "Keine Suchanfrage gegeben.",
"raw-entry-not-found": "Eintrag {{ENTRY}} des Typs {{DATATYPE}} konnte nicht gefunden werden.",
@@ -24,8 +26,14 @@
"404-page-heading": "Nicht gefunden",
"500-page-title": "Interner Server-Fehler",
"500-page-heading": "Interner Server-Fehler",
"500-page-text": "Es ist ein interner Serverfehler aufgetreten. Das tut uns leid :/",
"fulltext-search-unavailable": "Die Volltestsuche steht nicht zur Verfügung.",
"no-search-results": "Die Volltextsuche ist für diesen Inhalt nicht verfügbar.",
"search-results-page-title": "Suche: {{SEARCH_PATTERN}}",
"search-results-page-header": "Ergebnisse <b>{{START}}-{{END}}</b> von <b>{{COUNT}}</b> für <b>„{{{SEARCH_PATTERN}}}“</b>",
"empty-search-results-page-header": "Für <b>„{{{SEARCH_PATTERN}}}“</b> wurden keine Ergebnisse gefunden.",
"search-result-book-info": "von {{BOOK_TITLE}}",
"word-count": "{{COUNT}} Wörter",
"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",
@@ -53,5 +61,6 @@
"welcome-to-kiwix-server": "Wilkommen beim Kiwix Server",
"download-links-heading": "Download Links für <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Buch herunterladen",
"preview-book": "Vorschau"
"preview-book": "Vorschau",
"unknown-error": "Unbekannter Fehler"
}

View File

@@ -12,6 +12,7 @@
, "suggest-search" : "Make a full text search for <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>"
, "random-article-failure" : "Oops! Failed to pick a random article :("
, "invalid-raw-data-type" : "{{DATATYPE}} is not a valid request for raw content."
, "invalid-request" : "The requested URL \"{{{url}}}\" is not a valid request."
, "no-value-for-arg": "No value provided for argument {{ARGUMENT}}"
, "no-query" : "No query provided."
, "raw-entry-not-found" : "Cannot find {{DATATYPE}} entry {{ENTRY}}"
@@ -21,8 +22,14 @@
, "404-page-heading" : "Not Found"
, "500-page-title" : "Internal Server Error"
, "500-page-heading" : "Internal Server Error"
, "500-page-text": "An internal server error occured. We are sorry about that :/"
, "fulltext-search-unavailable" : "Fulltext search unavailable"
, "no-search-results": "The fulltext search engine is not available for this content."
, "search-results-page-title": "Search: {{SEARCH_PATTERN}}"
, "search-results-page-header": "Results <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
, "empty-search-results-page-header": "No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
, "search-result-book-info": "from {{BOOK_TITLE}}"
, "word-count": "{{COUNT}} words"
, "library-button-text": "Go to welcome page"
, "home-button-text": "Go to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "Go to a randomly selected page"
@@ -51,4 +58,6 @@
, "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>"
, "download-links-title": "Download book"
, "preview-book": "Preview"
, "non-translated-text": "{{MSG}}"
, "unknown-error": "Unknown error"
}

View File

@@ -1,10 +1,15 @@
{
"@metadata": {
"authors": [
"Adriendelucca",
"Gomoko",
"Melimeli",
"Stephane",
"Thibaut120094",
"Verdy p"
"Urhixidur",
"Verdy p",
"Vikoula5",
"Wladek92"
]
},
"name": "Français",
@@ -16,6 +21,7 @@
"suggest-search": "Faire une recherche en texte intégral de « <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> »",
"random-article-failure": "Oups! Échec de sélection dun article aléatoire :(",
"invalid-raw-data-type": "{{DATATYPE}} nest pas une requête valide pour du contenu brut.",
"invalid-request": "L'URL demandée \"{{{url}}}\" n'est pas une requête valide.",
"no-value-for-arg": "Aucune valeur fournie pour largument {{ARGUMENT}}",
"no-query": "Aucune requête fournie.",
"raw-entry-not-found": "Impossible de trouver lentrée « {{ENTRY}} » de type « {{DATATYPE}} »",
@@ -25,8 +31,14 @@
"404-page-heading": "Non trouvé",
"500-page-title": "Erreur interne du serveur",
"500-page-heading": "Erreur interne du serveur",
"500-page-text": "Une erreur de serveur interne s'est produite. Nous en sommes désolés :/",
"fulltext-search-unavailable": "Recherche en texte intégral non disponible",
"no-search-results": "Le moteur de recherche en texte intégral nest pas disponible pour ce contenu.",
"search-results-page-title": "Rechercher : {{SEARCH_PATTERN}}",
"search-results-page-header": "Résultats <b>{{START}}-{{END}}</b> sur<b> {{COUNT}}</b> pour <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Aucun résultat na été trouvé pour <b>« {{{SEARCH_PATTERN}}} »</b>",
"search-result-book-info": "à partir de {{BOOK_TITLE}}",
"word-count": "{{COUNT}} mots",
"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",
@@ -54,5 +66,6 @@
"welcome-to-kiwix-server": "Bienvenue sur le Serveur Kiwix",
"download-links-heading": "Liens de téléchargement pour <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Télécharger le livre",
"preview-book": "Aperçu"
"preview-book": "Aperçu",
"unknown-error": "Erreur inconnue"
}

67
static/skin/i18n/ha.json Normal file
View File

@@ -0,0 +1,67 @@
{
"@metadata": {
"authors": [
"Abelidokoo",
"El-hussain14",
"Rofiatmustapha12",
"Smshika",
"Yusuf Sa'adu"
]
},
"name": "Turanci",
"suggest-full-text-search": "dauke da ''{{{SEARCH_TERMS}}}''...",
"no-such-book": "Babu irin wannan littafin: {{BOOK_NAME}}",
"too-many-books": "An nemi littattafai da yawa ({{NB_BOOKS}}) inda iyaka shine {{LIMIT}}",
"no-book-found": "Babu wani littafi da ya dace da ma'aunin zaɓi",
"url-not-found": "Ba a sami URL ɗin da ake nema \"{{url}}\" akan wannan sabar ba.",
"suggest-search": "Yi cikakken bincike na rubutu don <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Kash! An kasa ɗaukar labarin bazuwar :(",
"invalid-raw-data-type": "{{DATATYPE}} ba ingantaccen buƙatun ɗanyen abun ciki bane.",
"invalid-request": "URL ɗin da ake nema \"{{{url}}}\" ba buƙatu mai inganci bane.",
"no-value-for-arg": "Babu ƙima da aka bayar don hujja {{ARGUMENT}}",
"no-query": "Ba a bayar da tambaya ba.",
"raw-entry-not-found": "Ba a iya samun shigarwar {{DATATYPE}} {{ENTRY}}",
"400-page-title": "nema mara inganci",
"400-page-heading": "nema mara inganci",
"404-page-title": "Ba a samo abun ciki ba",
"404-page-heading": "Ba a Samu ba",
"500-page-title": "Kuskuren na Cikin Saba",
"500-page-heading": "Kuskuren na Cikin Saba",
"500-page-text": "An sami kuskuren uwar garken ciki. Munyi nadama akan hakan :/",
"fulltext-search-unavailable": "Babu binciken cikakken rubutu",
"no-search-results": "Babu injin binciken cikakken rubutu don wannan abun ciki.",
"search-results-page-title": "Bincika: {{SEARCH_PATTERN}}",
"search-results-page-header": "Sakamako <b>{{START}}-{{END}}</b> na <b>{{COUNT}}</b> na <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Ba a sami sakamakon <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "daga {{BOOK_TITLE}}",
"word-count": "{{COUNT}} kalmomi",
"library-button-text": "Je zuwa shafin maraba",
"home-button-text": "Jeka babban shafin '{{BOOK_TITLE}}'",
"random-page-button-text": "Je zuwa shafin da aka zaɓa ba da gangan ba",
"searchbox-tooltip": "Bincika '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Littattafai biyu ko fiye a cikin harsuna daban-daban za su shiga cikin bincike, wanda zai iya haifar da sakamako mai ruɗani.",
"welcome-page-overzealous-filter": "Babu sakamako. Kuna so a <a href=\"{{URL}}\">sake saita tace</a>?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> ne ke ƙarfafa shi",
"search": "Nema",
"book-filtering-all-categories": "Dukkanin nau'o'in",
"book-filtering-all-languages": "Duka harsuna",
"count-of-matching-books": "{{COUNT}} littafi(s)",
"download": "Sauke",
"direct-download-link-text": "Kai tsaye",
"direct-download-alt-text": "saukewa kai tsaye",
"hash-download-link-text": "Sha256 hash",
"hash-download-alt-text": "sauke hash",
"magnet-link-text": "Magnet link",
"magnet-alt-text": "Magnet ɗin saukewa",
"torrent-download-link-text": "Torrent fayil",
"torrent-download-alt-text": "download torrent",
"library-opds-feed-all-entries": "Ciyarwar OPDS Library - Duk shigarwar",
"filter-by-tag": "Tace da alamar \"{{TAG}}\"",
"stop-filtering-by-tag": "Dakatar da tacewa ta hanyar \"{{TAG}}\"",
"library-opds-feed-parameterised": " OPDS ciyar wa OPDS- entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Barka da zowa manhajar Kiwix",
"download-links-heading": "Bring out ways through which people can join hands together .{{BOOK_TITLE}}",
"download-links-title": "Sauke littafin",
"preview-book": "Dubawa",
"unknown-error": "Kuskuren da ba a sani ba"
}

View File

@@ -14,6 +14,7 @@
"suggest-search": "לעשות חיפוש טקסט מלא עבור <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "אוי! לא עבדה בחירת ערך אקראי :(",
"invalid-raw-data-type": "{{DATATYPE}} הוא לא בקשה תקינה של תוכן גולמי.",
"invalid-request": "הכתובת המבוקשת \"{{{url}}}\" אינה בקשה תקינה.",
"no-value-for-arg": "לא סופק ערך לארגומנט {{ARGUMENT}}",
"no-query": "לא סופקה שאילתה.",
"raw-entry-not-found": "לא ניתן למצוא את רשומת ה־{{DATATYPE}} בשם {{ENTRY}}",
@@ -23,8 +24,14 @@
"404-page-heading": "לא נמצא",
"500-page-title": "שגיאת שרת פנימית",
"500-page-heading": "שגיאת שרת פנימית",
"500-page-text": "אירעה שגיאת שרת פנימית. אנחנו מצטערים על זה :/",
"fulltext-search-unavailable": "חיפוש בטקסט מלא אינו זמין",
"no-search-results": "מנוע החיפוש בטקסט מלא אינו זמין עבור התוכן הזה.",
"search-results-page-title": "חיפוש: {{SEARCH_PATTERN}}",
"search-results-page-header": "תוצאות <b>{{START}} עד {{END}}</b> מתוך <b>{{COUNT}}</b> עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "לא נמצאו תוצאות עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "מתוך {{BOOK_TITLE}}",
"word-count": "{{COUNT}} מילים",
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
"home-button-text": "מעבר לדף הראשי של \"{{BOOK_TITLE}}\"",
"random-page-button-text": "מעבר לדף שנבחר אקראית",
@@ -52,5 +59,6 @@
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "הורדת ספר",
"preview-book": "תצוגה מקדימה"
"preview-book": "תצוגה מקדימה",
"unknown-error": "שגיאה בלתי־ידועה"
}

65
static/skin/i18n/ig.json Normal file
View File

@@ -0,0 +1,65 @@
{
"@metadata": {
"authors": [
"Accuratecy051",
"Ngostary2k",
"Oby Ezeilo"
]
},
"name": "Bekee",
"suggest-full-text-search": "nwere {{{SEARCH_TERMS}}}'",
"no-such-book": "Enweghị akwụkwọ dị otú a: {{BOOK_NAME}}",
"too-many-books": "Arịrịọ ọtụtụ akwụkwọ ({{NB_BOOKS}}) ebe oke bụ {{LIMIT}}",
"no-book-found": "Ọ nweghị akwụkwọ dabara na nhọpụta nhọrọ",
"url-not-found": "Ahụghị URL a rịọrọ \"{{url}}\" na nkesa a.",
"suggest-search": "Mee ọchụchọ ederede zuru oke maka <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ee! Ịhọrọ akụkọ enweghị usoro :(",
"invalid-raw-data-type": "{{DATATYPE}} abụghị arịrịọ ziri ezi maka ọdịnaya raw.",
"invalid-request": "Arịrịọ gbasara URL \"{{{url}}}\" e zighi ezi.",
"no-value-for-arg": "Ọ nweghị uru enyere maka arụmụka {{ARGUMENT}}",
"no-query": "Ọnweghị ajụjụ enyere.",
"raw-entry-not-found": "Enweghị ike ịchọta ntinye {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Arịrịọ na-ezighi ezi",
"400-page-heading": "Arịrịọ na-ezighi ezi",
"404-page-title": "Ahụghị ọdịnaya",
"404-page-heading": "Ahụghị",
"500-page-title": "Mperi Sava Ime",
"500-page-heading": "Mperi Sava Ime",
"500-page-text": "Enwere mperi ihe nkesa dị n'ime. Ọ dị anyị nwute na nke ahụ :/",
"fulltext-search-unavailable": "Ọchịchọ ederede zuru ezu adịghị",
"no-search-results": "Igwe nchọta ederede zuru oke adịghị maka ọdịnaya a.",
"search-results-page-title": "Chọọ: {{SEARCH_PATTERN}}",
"search-results-page-header": "Rịzọlt ga <b>{{START}}-{{END}}</b> nke <b>{{COUNT}}</b> maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Ọnweghị rịzọlt ahụrụ maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "sitere na {{BOOK_TITLE}}",
"word-count": "Okwu {{COUNT}}",
"library-button-text": "Gaa na ibe nnabata",
"home-button-text": "Gaa na isi ibe nke '{{BOOK_TITLE}}'",
"random-page-button-text": "Gaa na ibe ahọpụtara enweghị usoro",
"searchbox-tooltip": "Chọọ '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Akwụkwọ abụọ ma ọ bụ karịa n'asụsụ dị iche iche ga-esonye na nchọ, nke nwere ike ibute nsonaazụ mgbagwoju anya.",
"welcome-page-overzealous-filter": "Enweghị nsonaazụ. Ọ ga-amasị gị <a href=\"{{URL}}\">ịtọgharịa nzacha</a> ?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> kwadoro ya",
"search": "Chọọ",
"book-filtering-all-categories": "Nkeji niile",
"book-filtering-all-languages": "Asụsụ niile",
"count-of-matching-books": "akwụkwọ {{COUNT}}",
"download": "Budata",
"direct-download-link-text": "Gosi",
"direct-download-alt-text": "nbudata ozugbo",
"hash-download-link-text": "Sha256 hash",
"hash-download-alt-text": "budata hash",
"magnet-link-text": "Njikọ magnet",
"magnet-alt-text": "ibudata magnet",
"torrent-download-link-text": " faịlụ nke Torrent",
"torrent-download-alt-text": "Budata torrent",
"library-opds-feed-all-entries": "Ọbá akwụkwọ OPDS Feed - Ihe niile",
"filter-by-tag": "Wepụta site na mkpado \"{{TAG}}\"",
"stop-filtering-by-tag": "Kwụsị nzacha site na mkpado \"{{TAG}}\"",
"library-opds-feed-parameterised": "Ọbá akwụkwọ OPDS nri - ndenye dabara na {{#LANG}}\nAsụsụ: {{LANG}} {{/LANG}}{{#CATEGORY}}\n Kategori: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\n Ajụjụ: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Nabata na Kiwix Server",
"download-links-heading": "Budata njikọ maka <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Budata akwụkwọ",
"preview-book": "Ziwe nkirimaàtụ̀",
"unknown-error": "amaghị m njehie"
}

View File

@@ -3,6 +3,7 @@
"authors": [
"Albano",
"Beta16",
"Luca.favorido",
"McDutchie"
]
},
@@ -14,6 +15,7 @@
"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 :(",
"invalid-request": "L'URL richiesto \"{{{url}}}\" non è una richiesta valida.",
"no-value-for-arg": "Nessun valore fornito per l'argomento {{ARGUMENT}}",
"400-page-title": "Richiesta non valida",
"400-page-heading": "Richiesta non valida",
@@ -21,6 +23,12 @@
"404-page-heading": "Non trovato",
"500-page-title": "Errore interno del server",
"500-page-heading": "Errore interno del server",
"500-page-text": "Si è verificato un errore interno del server. Ci dispiace :/",
"search-results-page-title": "Cerca: {{SEARCH_PATTERN}}",
"search-results-page-header": "Risultati <b>{{START}}-{{END}}</b> di <b>{{COUNT}}</b> per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Non è stato trovato alcun risultato per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "da {{BOOK_TITLE}}",
"word-count": "{{COUNT}} parole",
"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",
@@ -30,5 +38,6 @@
"count-of-matching-books": "{{COUNT}} libro/i",
"download": "Scarica",
"download-links-title": "Scarica libro",
"preview-book": "Anteprima"
"preview-book": "Anteprima",
"unknown-error": "Errore sconosciuto"
}

View File

@@ -13,6 +13,7 @@
"404-page-heading": "Net fonnt",
"500-page-title": "Interne Feeler um Server",
"500-page-heading": "Interne Feeler um Server",
"500-page-text": "Et ass en interne Serverfeeler opgetrueden. Mir entschëllegen eis dofir :/",
"fulltext-search-unavailable": "Volltext-Sich net verfügbar",
"home-button-text": "Gitt op d'Haaptsäit vun '{{BOOK_TITLE}}'",
"random-page-button-text": "Gitt op eng zoufälleg gewielte Säit",
@@ -24,5 +25,6 @@
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
"download": "Eroflueden",
"direct-download-link-text": "Direkt",
"download-links-title": "Buch eroflueden"
"download-links-title": "Buch eroflueden",
"unknown-error": "Onbekannte Feeler"
}

View File

@@ -14,6 +14,7 @@
"suggest-search": "Побарајте го <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> по целиот текст",
"random-article-failure": "Упс! Не успеав да изберам случајна статија :(",
"invalid-raw-data-type": "{{DATATYPE}} не претставува важечко барање за сирова содржина.",
"invalid-request": "Побараната URL „{{{url}}}“ не претставува важечко барање.",
"no-value-for-arg": "Нема укажано вредност за аргументот {{ARGUMENT}}",
"no-query": "Не е укажано барање.",
"raw-entry-not-found": "Не можам да ја најдам {{DATATYPE}}-ставката {{ENTRY}}",
@@ -23,8 +24,14 @@
"404-page-heading": "Не е најдено",
"500-page-title": "Внатрешна грешка во опслужувачот",
"500-page-heading": "Внатрешна грешка во опслужувачот",
"500-page-text": "Настана внатрешна грешка во опслужувачот. Жал ни е :/",
"fulltext-search-unavailable": "Целотекстното пребарување е недостапно",
"no-search-results": "Погонот за целотекстно пребарување не е достапен за оваа содржина.",
"search-results-page-title": "Пребарување: {{SEARCH_PATTERN}}",
"search-results-page-header": "Исход <b>{{START}}-{{END}}</b> од <b>{{COUNT}}</b> за <b>„{{{SEARCH_PATTERN}}}“</b>",
"empty-search-results-page-header": "Не најдов ништо за <b>„{{{SEARCH_PATTERN}}}“</b>",
"search-result-book-info": "од {{BOOK_TITLE}}",
"word-count": "{{COUNT}} зборови",
"library-button-text": "Оди на воведната страница",
"home-button-text": "Оди на главната страница на „{{BOOK_TITLE}}“",
"random-page-button-text": "Оди на случајно избрана страница",
@@ -52,5 +59,6 @@
"welcome-to-kiwix-server": "Добре дојдовте на Опслужувачот на Кивикс",
"download-links-heading": "Врски за преземање на <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Преземи книга",
"preview-book": "Преглед"
"preview-book": "Преглед",
"unknown-error": "Непозната грешка"
}

View File

@@ -25,6 +25,13 @@
"home-button-text": "Przejdź do głównej strony '{{BOOK_TITLE}}'",
"random-page-button-text": "Przejdź do losowo wybranej strony",
"searchbox-tooltip": "Szukaj '{{BOOK_TITLE}}'",
"search": "Szukaj",
"book-filtering-all-categories": "Wszystkie Kategorie",
"book-filtering-all-languages": "Wszystkie języki",
"download": "Pobierz",
"direct-download-link-text": "Bezpośrednio",
"direct-download-alt-text": "bezpośrednie pobieranie",
"torrent-download-link-text": "Plik torrent",
"welcome-to-kiwix-server": "Witamy na serwerze Kiwix",
"download-links-title": "Pobierz książkę",
"preview-book": "Podgląd"

View File

@@ -1,6 +1,7 @@
{
"@metadata": {
"authors": [
"Amire80",
"Matthieu Gautier",
"Veloman Yunkan",
"Verdy p"
@@ -15,6 +16,7 @@
"suggest-search": "Suggest a search when the URL points to a non existing article",
"random-article-failure": "Failure of the random article selection procedure",
"invalid-raw-data-type": "Invalid DATATYPE was used with the /raw endpoint (/raw/<book>/DATATYPE/...); allowed values are 'meta' and 'content'",
"invalid-request": "Error text for malformed URLs.",
"no-value-for-arg": "Error text when no value has been provided for ARGUMENT in the request's query string",
"no-query": "Error text when no query has been provided for fulltext search",
"raw-entry-not-found": "Entry requested via the /raw endpoint was not found",
@@ -24,8 +26,14 @@
"404-page-heading": "Heading of the 404 error page",
"500-page-title": "Title of the 500 error page",
"500-page-heading": "Heading of the 500 error page",
"500-page-text": "Text of the 500 error page",
"fulltext-search-unavailable": "Title of the error page returned when search is attempted in a book without fulltext search database",
"no-search-results": "Text of the error page returned when search is attempted in a book without fulltext search database",
"search-results-page-title": "Title of the search results page",
"search-results-page-header": "Header of the search results page",
"empty-search-results-page-header": "Header of the empty search results page",
"search-result-book-info": "Reference to the book where the search result belongs (this is displayed AFTER the search result)",
"word-count": "Word count information",
"library-button-text": "Tooltip of the button leading to the welcome page",
"home-button-text": "Tooltip of the button leading to the main page of a book",
"random-page-button-text": "Tooltip of the button opening a randomly selected page",
@@ -52,5 +60,7 @@
"welcome-to-kiwix-server": "Title shown in browser's title bar/page tab",
"download-links-heading": "Heading for no-js download page",
"download-links-title": "Title for no-js download page",
"preview-book": "Tooltip of book-tile leading to the book"
"preview-book": "Tooltip of book-tile leading to the book",
"non-translated-text": "{{ignored}}\nUsed to display text that is generated at runtime and cannot be translated. Nothing to translate about this one.",
"unknown-error": "Unknown error"
}

View File

@@ -6,6 +6,7 @@
"Okras",
"Pacha Tchernof",
"Razno0",
"Rofiatmustapha12",
"Smavrina"
]
},
@@ -18,6 +19,7 @@
"suggest-search": "Выполните полнотекстовый поиск для <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ой! Не удалось выбрать случайную статью :(",
"invalid-raw-data-type": "{{DATATYPE}} не является допустимым запросом необработанного контента.",
"invalid-request": "Запрошенный URL-адрес «{{{url}}}» не является допустимым запросом.",
"no-value-for-arg": "Не указано значение для аргумента {{ARGUMENT}}",
"no-query": "Не предоставлен запрос.",
"raw-entry-not-found": "Не удаётся найти запись {{ENTRY}} типа {{DATATYPE}}",
@@ -27,30 +29,41 @@
"404-page-heading": "Не найдено",
"500-page-title": "Внутренняя ошибка сервера",
"500-page-heading": "Внутренняя ошибка сервера",
"500-page-text": "Произошла внутренняя ошибка сервера. Мы сожалеем об этом :/",
"fulltext-search-unavailable": "Полнотекстовый поиск недоступен",
"no-search-results": "Полнотекстовая поисковая система недоступна для этого содержания.",
"search-results-page-title": "Поиск: {{SEARCH_PATTERN}}",
"search-results-page-header": "Результаты <b>{{START}}-{{END}}</b> из <b>{{COUNT}}</b> для <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "По запросу <b>\"{{{SEARCH_PATTERN}}}\"</b> результатов не найдено.",
"search-result-book-info": "из {{BOOK_TITLE}}",
"word-count": "{{COUNT}} слов",
"library-button-text": "Перейти на страницу-приветствие",
"home-button-text": "Перейти на главную страницу '{{BOOK_TITLE}}'",
"random-page-button-text": "Перейти на случайно выбранную страницу",
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'",
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам.",
"welcome-page-overzealous-filter": "Безрезультатно. Хотите <a href=\"{{URL}}\">сбросить фильтр</a> ?",
"powered-by-kiwix-html": "При поддержке&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Найти",
"book-filtering-all-categories": "Все категории",
"book-filtering-all-languages": "Все языки",
"count-of-matching-books": "{{COUNT}} книг(и)",
"download": "Скачать",
"direct-download-link-text": "Прямой",
"direct-download-alt-text": "прямая загрузка",
"hash-download-link-text": "Хэш Sha256",
"hash-download-alt-text": "скачать хэш",
"magnet-link-text": "Магнитная ссылка",
"magnet-alt-text": "скачать магнит",
"torrent-download-link-text": "Торрент-файл",
"torrent-download-alt-text": "скачать торрент",
"library-opds-feed-all-entries": "Канал библиотеки OPDS  все записи",
"filter-by-tag": "Фильтровать по тегу \"{{TAG}}\"",
"stop-filtering-by-tag": "Прекратить фильтрацию по тегу \"{{TAG}}\"",
"library-opds-feed-parameterised": "Канал OPDS библиотеки – записи, соответствующие {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nЗапрос: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Добро пожаловать на сервер Kiwix",
"download-links-heading": "Ссылки для скачивания <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Скачать книгу",
"preview-book": "Предпросмотр"
"preview-book": "Предпросмотр",
"unknown-error": "Неизвестная ошибка"
}

View File

@@ -2,7 +2,8 @@
"@metadata": {
"authors": [
"Eleassar",
"Kelson"
"Kelson",
"Rofiatmustapha12"
]
},
"name": "slovenščina",
@@ -14,6 +15,7 @@
"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.",
"invalid-request": "Zahtevani URL »{{{url}}}« ni veljaven zahtevek.",
"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}} tipa {{DATATYPE}}",
@@ -23,8 +25,14 @@
"404-page-heading": "Ni najdeno",
"500-page-title": "Notranja napaka strežnika",
"500-page-heading": "Notranja napaka strežnika",
"500-page-text": "Prišlo je do notranje napake strežnika. Žal nam je za to. :/",
"fulltext-search-unavailable": "Iskanje po celotnem besedilu ni na voljo",
"no-search-results": "Iskalnik po celotnem besedilu za to vsebino ni na voljo.",
"search-results-page-title": "Iskanje: {{SEARCH_PATTERN}}",
"search-results-page-header": "Zadetki <b>{{START}}{{END}}</b> od <b>{{COUNT}}</b> za »<b>{{{SEARCH_PATTERN}}}</b>«",
"empty-search-results-page-header": "Ni zadetkov za »<b>{{{SEARCH_PATTERN}}}</b>«",
"search-result-book-info": "iz {{BOOK_TITLE}}",
"word-count": "{{COUNT}} besed",
"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",
@@ -52,5 +60,6 @@
"welcome-to-kiwix-server": "Pozdravljeni na strežniku Kiwix",
"download-links-heading": "Povezave za prenos za <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Prenesi knjigo",
"preview-book": "Predogled"
"preview-book": "Predogled",
"unknown-error": "Neznana napaka"
}

View File

@@ -2,6 +2,7 @@
"@metadata": {
"authors": [
"Jopparn",
"Rofiatmustapha12",
"Sabelöga",
"WikiPhoenix"
]
@@ -15,6 +16,7 @@
"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.",
"invalid-request": "Den begärda webbadressen \"{{{url}}}\" är inte en giltig begäran.",
"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}}",
@@ -24,8 +26,14 @@
"404-page-heading": "Hittades inte",
"500-page-title": "Internt serverfel",
"500-page-heading": "Internt serverfel",
"500-page-text": "Ett internt serverfel uppstod. Vi ber om ursäkt för det :/",
"fulltext-search-unavailable": "Fulltextsökning är inte tillgänglig",
"no-search-results": "Sökmaskinen för fulltext är inte tillgänglig för detta innehåll.",
"search-results-page-title": "Sök: {{SEARCH_PATTERN}}",
"search-results-page-header": "Resultat <b>{{START}}-{{END}}</b> av <b>{{COUNT}}</b> för <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"empty-search-results-page-header": "Inga resultat hittades för <b>\"{{{SEARCH_PATTERN}}}\"</b>",
"search-result-book-info": "från {{BOOK_TITLE}}",
"word-count": "{{COUNT}} ord",
"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",
@@ -53,5 +61,6 @@
"welcome-to-kiwix-server": "Välkommen till Kiwix Server",
"download-links-heading": "Nedladdningslänkar för <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Ladda ned bok",
"preview-book": "Förhandsgranska"
"preview-book": "Förhandsgranska",
"unknown-error": "Okänt fel"
}

View File

@@ -40,4 +40,11 @@
, "download-links-heading": "[I18N] Download links for <b><i>{{BOOK_TITLE}}</i></b> [TESTING]"
, "download-links-title": "[I18N TESTING]Download book"
, "preview-book": "[I18N] Preview [TESTING]"
, "no-query" : "[I18N TESTING] Kiwix can read your thoughts but it is against GDPR. Please provide your query explicitly."
, "invalid-request" : "[I18N TESTING] Invalid URL: \"{{{url}}}\""
, "search-results-page-title": "[I18N TESTING] Search: {{SEARCH_PATTERN}}"
, "search-results-page-header": "[I18N TESTING] Results <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
, "empty-search-results-page-header": "[I18N TESTING] No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
, "search-result-book-info": "from [I18N TESTING] {{BOOK_TITLE}}"
, "word-count": "{{COUNT}} [I18N TESTING] words"
}

View File

@@ -1,7 +1,8 @@
{
"@metadata": {
"authors": [
"Hedda"
"Hedda",
"Rofiatmustapha12"
]
},
"name": "Türkçe",
@@ -13,6 +14,7 @@
"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.",
"invalid-request": "İstenen \"{{{url}}}\" URL'si 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ı",
@@ -22,10 +24,41 @@
"404-page-heading": "Bulunamadı",
"500-page-title": "İç Sunucu Hatası",
"500-page-heading": "İç Sunucu Hatası",
"500-page-text": "Dahili bir sunucu hatası oluştu. Bunun için üzgünüz :/",
"fulltext-search-unavailable": "Tam metin araması kullanılamıyor",
"no-search-results": "Tam metin arama motoru bu içerik için kullanılamaz.",
"search-results-page-title": "Arama: {{SEARCH_PATTERN}}",
"search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b> için <b>{{COUNT}}</b> sonuçtan <b>{{START}}-{{END}}</b> arası sonuçlar",
"empty-search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b> için sonuç bulunamadı",
"search-result-book-info": "{{BOOK_TITLE}} adlı kitaptan",
"word-count": "{{COUNT}} kelime",
"library-button-text": "Karşılama sayfasına git",
"home-button-text": "'{{BOOK_TITLE}}' anasayfasına gidin",
"random-page-button-text": "Rastgele seçilen bir sayfaya git",
"searchbox-tooltip": "'{{BOOK_TITLE}}' ara"
"searchbox-tooltip": "'{{BOOK_TITLE}}' ara",
"confusion-of-tongues": "Aramaya farklı dillerde iki veya daha fazla kitap katılacak ve bu da kafa karıştırıcı sonuçlara yol açabilecektir.",
"welcome-page-overzealous-filter": "Sonuç yok. <a href=\"{{URL}}\">Filtreyi sıfırlamak</a> ister misiniz?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> tarafından desteklenmektedir",
"search": "Ara",
"book-filtering-all-categories": "Tüm kategoriler",
"book-filtering-all-languages": "Tüm diller",
"count-of-matching-books": "{{COUNT}} kitap",
"download": "İndir",
"direct-download-link-text": "Doğrudan",
"direct-download-alt-text": "direkt indirme",
"hash-download-link-text": "Sha256 haşesi",
"hash-download-alt-text": "csv indir",
"magnet-link-text": "Mıknatıs bağlantısı",
"magnet-alt-text": "mıknatısı indir",
"torrent-download-link-text": "Hedef dosya",
"torrent-download-alt-text": "torrenti indir",
"library-opds-feed-all-entries": "Kütüphane OPDS Akışı - Tüm girişler",
"filter-by-tag": "\"{{TAG}}\" etiketine göre filtrele",
"stop-filtering-by-tag": "\"{{TAG}}\" etiketine göre filtrelemeyi durdur",
"library-opds-feed-parameterised": "Kütüphane OPDS Özet Akışı - {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} ile eşleşen girişler {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Kiwix Sunucusuna Hoş Geldiniz",
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> için indirme bağlantıları",
"download-links-title": "Kitapları indir",
"preview-book": "Önizleme",
"unknown-error": "Bilinmeyen hata"
}

View File

@@ -2,24 +2,63 @@
"@metadata": {
"authors": [
"GuoPC",
"IceButBin",
"StarrySky",
"Sunai",
"XtexChooser"
]
},
"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}} 对原请求无效。",
"invalid-request": "请求的URL无效{{{url}}}",
"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": "内部服务器错误",
"500-page-text": "内部服务器出现错误。真的十分抱歉 (;⁠ŏ⁠﹏⁠ŏ⁠)",
"fulltext-search-unavailable": "全文搜索不可用",
"no-search-results": "全文搜索引擎不适用于该内容。",
"search-results-page-title": "搜索:{{SEARCH_PATTERN}}",
"search-results-page-header": "<b>“{{{SEARCH_PATTERN}}}”</b>的<b>第 {{START}}-{{END}}</b>个结果(共<b>{{COUNT}}</b>个)",
"empty-search-results-page-header": "未找到<b>“{{{SEARCH_PATTERN}}}”</b>的结果",
"search-result-book-info": "来自{{BOOK_TITLE}}",
"word-count": "{{COUNT}} 个字",
"library-button-text": "前往欢迎页面",
"home-button-text": "转到“{{BOOK_TITLE}}”的主页",
"random-page-button-text": "前往随机选择的页面",
"searchbox-tooltip": "搜索“{{BOOK_TITLE}}”",
"confusion-of-tongues": "两本或多本不同语言的图书将同时被搜索,这可能会导致搜索结果混乱。",
"welcome-page-overzealous-filter": "没有结果。您想<a href=\"{{URL}}\">重置过滤器</a>吗?",
"powered-by-kiwix-html": "由<a href=\"https://kiwix.org\">Kiwix</a>提供技术支持",
"search": "搜索",
"book-filtering-all-categories": "所有分类",
"book-filtering-all-languages": "所有语言",
"count-of-matching-books": "{{COUNT}} 本书",
"download": "下载",
"direct-download-link-text": "直接",
"direct-download-alt-text": "直接下載",
"hash-download-link-text": "Sha256 哈希值",
"hash-download-alt-text": "下载哈希值",
"magnet-link-text": "磁力链接",
"magnet-alt-text": "下载磁力链接",
"torrent-download-link-text": "种子文件",
"torrent-download-alt-text": "下载种子文件",
"library-opds-feed-all-entries": "图书馆 OPDS Feed - 所有条目",
"filter-by-tag": "按标签“{{TAG}}”过滤",
"stop-filtering-by-tag": "停止按标签“{{TAG}}”过滤",
"library-opds-feed-parameterised": "图书馆 OPDS Feed - 匹配的项目 {{#LANG}}\n语言{{LANG}} {{/LANG}}{{#CATEGORY}}\n分类{{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n标签{{TAG}} {{/TAG}}{{#Q}}\n查询{{Q}} {{/Q}}",
"welcome-to-kiwix-server": "欢迎来到 Kiwix 服务器",
"preview-book": "预览"
}

View File

@@ -15,6 +15,7 @@
"suggest-search": "建立 <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> 使用的全文搜尋",
"random-article-failure": "哎呀!隨機挑選條目失敗 :(",
"invalid-raw-data-type": "{{DATATYPE}}不是原始內容的有效請求。",
"invalid-request": "請求的 URL「{{{url}}}」不是有效的請求。",
"no-value-for-arg": "沒有為引數 {{ARGUMENT}} 提供內容",
"no-query": "未提供查詢。",
"raw-entry-not-found": "找不到{{DATATYPE}}項目{{ENTRY}}",
@@ -24,8 +25,14 @@
"404-page-heading": "查無頁面",
"500-page-title": "內部伺服器錯誤",
"500-page-heading": "內部伺服器錯誤",
"500-page-text": "內部伺服器發生錯誤。對此我們深感抱歉:/",
"fulltext-search-unavailable": "全文搜尋無效",
"no-search-results": "全文搜尋引擎不適用此內容。",
"search-results-page-title": "搜尋:{{SEARCH_PATTERN}}",
"search-results-page-header": "<b>「{{{SEARCH_PATTERN}}}」</b>的<b>第{{START}}-{{END}}筆</b>結果(共<b>{{COUNT}}</b>筆)",
"empty-search-results-page-header": "未找到<b>「{{{SEARCH_PATTERN}}}」</b>的結果",
"search-result-book-info": "來自{{BOOK_TITLE}}",
"word-count": "{{COUNT}}個字",
"library-button-text": "前往歡迎首頁",
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
"random-page-button-text": "前往隨機選取頁面",
@@ -53,5 +60,6 @@
"welcome-to-kiwix-server": "歡迎來到 Kiwix 伺服器",
"download-links-heading": "下載<b><i>{{BOOK_TITLE}}</i></b>的連結",
"download-links-title": "下載書籍",
"preview-book": "預覽"
"preview-book": "預覽",
"unknown-error": "不明錯誤"
}

View File

@@ -1,104 +1,192 @@
const uiLanguages = [
{
"الإنجليزية": "ar"
"iso_code": "ar",
"self_name": "الإنجليزية",
"translation_count": 25
},
{
"বাংলা": "bn"
"iso_code": "bn",
"self_name": "বাংলা",
"translation_count": 14
},
{
"Čeština": "cs"
"iso_code": "br",
"self_name": "brezhoneg",
"translation_count": 35
},
{
"Deutsch": "de"
"iso_code": "cs",
"self_name": "Čeština",
"translation_count": 25
},
{
"English": "en"
"iso_code": "dag",
"self_name": "Silimiinsili",
"translation_count": 24
},
{
"español": "es"
"iso_code": "de",
"self_name": "Deutsch",
"translation_count": 57
},
{
"suomi": "fi"
"iso_code": "en",
"self_name": "English",
"translation_count": 58
},
{
"Français": "fr"
"iso_code": "es",
"self_name": "español",
"translation_count": 48
},
{
"עברית": "he"
"iso_code": "fi",
"self_name": "suomi",
"translation_count": 22
},
{
"हिन्दी": "hi"
"iso_code": "fr",
"self_name": "Français",
"translation_count": 57
},
{
"Հայերեն": "hy"
"iso_code": "ha",
"self_name": "Turanci",
"translation_count": 57
},
{
"interlingua": "ia"
"iso_code": "he",
"self_name": "עברית",
"translation_count": 57
},
{
"italiano": "it"
"iso_code": "hi",
"self_name": "हिन्दी",
"translation_count": 49
},
{
"日本語": "ja"
"iso_code": "hy",
"self_name": "Հայերեն",
"translation_count": 15
},
{
"한국어": "ko"
"iso_code": "ia",
"self_name": "interlingua",
"translation_count": 49
},
{
"kurdî": "ku-latn"
"iso_code": "ig",
"self_name": "Bekee",
"translation_count": 57
},
{
"Lëtzebuergesch": "lb"
"iso_code": "it",
"self_name": "italiano",
"translation_count": 34
},
{
"македонски": "mk"
"iso_code": "ja",
"self_name": "日本語",
"translation_count": 26
},
{
"Bahasa Melayu": "ms"
"iso_code": "ko",
"self_name": "한국어",
"translation_count": 13
},
{
"Nederlands": "nl"
"iso_code": "ku-latn",
"self_name": "kurdî",
"translation_count": 26
},
{
"ߒߞߏ": "nqo"
"iso_code": "lb",
"self_name": "Lëtzebuergesch",
"translation_count": 22
},
{
"ଓଡ଼ିଆ": "or"
"iso_code": "mk",
"self_name": "македонски",
"translation_count": 57
},
{
"Polski": "pl"
"iso_code": "ms",
"self_name": "Bahasa Melayu",
"translation_count": 14
},
{
"русский": "ru"
"iso_code": "nl",
"self_name": "Nederlands",
"translation_count": 49
},
{
"Sardu": "sc"
"iso_code": "nqo",
"self_name": "ߒߞߏ",
"translation_count": 43
},
{
"slovenčina": "sk"
"iso_code": "or",
"self_name": "ଓଡ଼ିଆ",
"translation_count": 49
},
{
"سرائیکی": "skr-arab"
"iso_code": "pl",
"self_name": "Polski",
"translation_count": 31
},
{
"slovenščina": "sl"
"iso_code": "ru",
"self_name": "русский",
"translation_count": 57
},
{
"Shqip": "sq"
"iso_code": "sc",
"self_name": "Sardu",
"translation_count": 49
},
{
"Svenska": "sv"
"iso_code": "sk",
"self_name": "slovenčina",
"translation_count": 25
},
{
"ఇంగ్లీషు": "te"
"iso_code": "skr-arab",
"self_name": "سرائیکی",
"translation_count": 20
},
{
"Türkçe": "tr"
"iso_code": "sl",
"self_name": "slovenščina",
"translation_count": 57
},
{
"英语": "zh-hans"
"iso_code": "sq",
"self_name": "Shqip",
"translation_count": 49
},
{
"繁體中文": "zh-hant"
"iso_code": "sv",
"self_name": "Svenska",
"translation_count": 57
},
{
"iso_code": "te",
"self_name": "ఇంగ్లీషు",
"translation_count": 49
},
{
"iso_code": "tr",
"self_name": "Türkçe",
"translation_count": 57
},
{
"iso_code": "zh-hans",
"self_name": "英语",
"translation_count": 54
},
{
"iso_code": "zh-hant",
"self_name": "繁體中文",
"translation_count": 57
}
]

21
static/skin/polyfills.js Normal file
View File

@@ -0,0 +1,21 @@
// A few browsers do not support the use of String.prototype.replaceAll method.
// Hence we define it once we verify that it isn't supported. For documentation
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (pattern, replacement) {
// verify parameter: It must either be a string or a RegExp with a global flag.
if (typeof pattern[Symbol.replace] === 'function') {
// the pattern is a RegExp check for the presence of g flag.
if (pattern.global) {
return this.replace(pattern, replacement);
} else {
throw new TypeError('Global flag for regular expressions')
}
}
// the pattern is not a RegExp, hence it must be a string.
if (typeof pattern !== 'string') {
throw new TypeError('pattern must either be a string or a RegExp with a global (g) flag.')
}
return this.replace(new RegExp(pattern, 'g'), replacement);
}
}

View File

@@ -10,13 +10,22 @@ let viewerState = {
uiLanguage: 'en',
};
function dropUserLang(query) {
const q = new URLSearchParams(query);
q.delete('userlang');
const pre = (query.startsWith('?') && q.size != 0 ? '?' : '');
return pre + q.toString();
}
function userUrl2IframeUrl(url) {
if ( url == '' ) {
return blankPageUrl;
}
if ( url.startsWith('search?') ) {
return `${root}/${url}`;
const q = new URLSearchParams(url.slice("search?".length));
q.set('userlang', viewerState.uiLanguage);
return `${root}/search?${q.toString()}`;
}
return `${root}/content/${url}`;
@@ -73,7 +82,7 @@ function quasiUriEncode(s, specialSymbols) {
function performSearch() {
const searchbox = document.getElementById('kiwixsearchbox');
const q = encodeURIComponent(searchbox.value);
gotoUrl(`/search?books.name=${currentBook}&pattern=${q}`);
gotoUrl(`/search?books.name=${currentBook}&pattern=${q}&userlang=${viewerState.uiLanguage}`);
}
function makeJSLink(jsCodeString, linkText, linkAttr="") {
@@ -148,7 +157,7 @@ function iframeUrl2UserUrl(url, query) {
}
if ( url == `${root}/search` ) {
return `search${query}`;
return `search${dropUserLang(query)}`;
}
url = url.slice(root.length);
@@ -249,6 +258,25 @@ function handle_location_hash_change() {
history.replaceState(viewerState, null);
}
function translateErrorPageIfNeeded() {
const cw = contentIframe.contentWindow;
if ( cw.KIWIX_RESPONSE_TEMPLATE && cw.KIWIX_RESPONSE_DATA ) {
const template = htmlDecode(cw.KIWIX_RESPONSE_TEMPLATE);
// cw.KIWIX_RESPONSE_DATA belongs to the iframe context and running
// I18n.render() on it directly in the top context doesn't work correctly
// because the type checks (obj.__proto__ == ???.prototype) in
// I18n.instantiateParameterizedMessages() always fail (String.prototype
// refers to different objects in different contexts).
// Work arround that issue by copying the object into our context.
const params = JSON.parse(JSON.stringify(cw.KIWIX_RESPONSE_DATA));
const html = I18n.render(template, params);
const htmlDoc = new DOMParser().parseFromString(html, "text/html");
cw.document.documentElement.innerHTML = htmlDoc.documentElement.innerHTML;
}
}
function handle_content_url_change() {
const iframeLocation = contentIframe.contentWindow.location;
console.log('handle_content_url_change: ' + iframeLocation.href);
@@ -258,6 +286,7 @@ function handle_content_url_change() {
const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery);
history.replaceState(viewerState, null, makeURL(location.search, newHash));
updateCurrentBookIfNeeded(newHash);
translateErrorPageIfNeeded();
};
////////////////////////////////////////////////////////////////////////////////
@@ -290,12 +319,23 @@ function isExternalUrl(url) {
|| url.startsWith("https:");
}
function getRealHref(target) {
// In case of wombat in the middle, wombat will rewrite the href value to the original url (external link)
// This is not what we want. Let's ask wombat to not rewrite href
const old_no_rewrite = target._no_rewrite;
target._no_rewrite = true;
const target_href = target.href;
target._no_rewrite = old_no_rewrite;
return target_href;
}
function onClickEvent(e) {
const iframeDocument = contentIframe.contentDocument;
const target = matchingAncestorElement(e.target, iframeDocument, "a");
if (target !== null && "href" in target) {
if ( isExternalUrl(target.href) ) {
const possiblyBlockedLink = blockLink(target.href);
const target_href = getRealHref(target);
if (isExternalUrl(target_href)) {
const possiblyBlockedLink = blockLink(target_href);
if ( e.ctrlKey || e.shiftKey ) {
// The link will be loaded in a new tab/window - update the link
// and let the browser handle the rest.
@@ -485,6 +525,7 @@ function changeUILanguage() {
viewerState.uiLanguage = lang;
setUserLanguage(lang, () => {
updateUIText();
translateErrorPageIfNeeded();
history.pushState(viewerState, null);
});
}
@@ -505,9 +546,8 @@ function setupViewer() {
const lang = getUserLanguage();
setUserLanguage(lang, finishViewerSetupOnceTranslationsAreLoaded);
viewerState.uiLanguage = lang;
const q = new URLSearchParams(window.location.search);
q.delete('userlang');
const rewrittenURL = makeURL(q.toString(), location.hash);
const cleanedUpQuery = dropUserLang(window.location.search);
const rewrittenURL = makeURL(cleanedUpQuery, location.hash);
history.replaceState(viewerState, null, rewrittenURL);
kiwixToolBarWrapper.style.display = 'block';

View File

@@ -5,7 +5,10 @@
<title>{{PAGE_TITLE}}</title>
{{#CSS_URL}}
<link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />
{{/CSS_URL}}
{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} <script>
window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";
window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};
</script>{{/KIWIX_RESPONSE_DATA}}
</head>
<body>
<h1>{{PAGE_HEADING}}</h1>

View File

@@ -31,6 +31,7 @@
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{{root}}/skin/favicon/browserconfig.xml?KIWIXCACHEID">
<meta name="theme-color" content="#ffffff">
<script type="text/javascript" src="./skin/polyfills.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="./viewer_settings.js"></script>
<script type="module" src="{{root}}/skin/i18n.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="{{root}}/skin/languages.js?KIWIXCACHEID" defer></script>

View File

@@ -102,23 +102,11 @@
}
</style>
<title>Search: {{query.pattern}}</title>
<title>{{PAGE_TITLE}}</title>
</head>
<body bgcolor="white">
<div class="header">
{{#results.hasResults}}
Results
<b>
{{results.start}}-{{results.end}}
</b> of <b>
{{results.count}}
</b> for <b>
"{{{query.pattern}}}"
</b>
{{/results.hasResults}}
{{^results.hasResults}}
No results were found for <b>"{{{query.pattern}}}"</b>
{{/results.hasResults}}
{{{PAGE_HEADER}}}
</div>
<div class="results">
@@ -131,12 +119,12 @@
{{#snippet}}
<cite>{{>snippet}}...</cite>
{{/snippet}}
{{#bookTitle}}
<div class="book-title">from {{bookTitle}}</div>
{{/bookTitle}}
{{#wordCount}}
<div class="informations">{{wordCount}} words</div>
{{/wordCount}}
{{#bookInfo}}
<div class="book-title">{{bookInfo}}</div>
{{/bookInfo}}
{{#wordCountInfo}}
<div class="informations">{{wordCountInfo}}</div>
{{/wordCountInfo}}
</li>
{{/results.items}}
</ul>

View File

@@ -1,6 +1,5 @@
const viewerSettings = {
toolbarEnabled: {{enable_toolbar}},
linkBlockingEnabled: {{enable_link_blocking}},
libraryButtonEnabled: {{enable_library_button}},
defaultUserLanguage: "{{default_user_language}}"
libraryButtonEnabled: {{enable_library_button}}
}

View File

@@ -11,6 +11,7 @@
<link type="text/css" href="./skin/kiwix.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="./skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
<script type="text/javascript" src="./skin/polyfills.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="./viewer_settings.js"></script>
<script type="module" src="./skin/i18n.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="./skin/languages.js?KIWIXCACHEID" defer></script>

View File

@@ -1516,7 +1516,7 @@ inline bool bind_ip_address(socket_t sock, const char *host) {
}
inline std::string if2ip(const std::string &ifn) {
#ifndef _WIN32
#if !defined(_WIN32) && !defined(__HAIKU__)
struct ifaddrs *ifap;
getifaddrs(&ifap);
for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) {

50
test/i18n.cpp Normal file
View File

@@ -0,0 +1,50 @@
#include "../src/server/i18n.h"
#include "gtest/gtest.h"
using namespace kiwix;
TEST(ParameterizedMessage, parameterlessMessages)
{
{
const ParameterizedMessage msg("404-page-title", {});
EXPECT_EQ(msg.getText("en"), "Content not found");
EXPECT_EQ(msg.getText("test"), "[I18N TESTING] Not Found - Try Again");
}
{
// Make sure that msgId influences the result of getText()
const ParameterizedMessage msg("random-page-button-text", {});
EXPECT_EQ(msg.getText("en"), "Go to a randomly selected page");
EXPECT_EQ(msg.getText("test"), "[I18N TESTING] I am tired of determinism");
}
{
// Demonstrate that unwanted parameters are silently ignored
const ParameterizedMessage msg("404-page-title", {{"abc", "xyz"}});
EXPECT_EQ(msg.getText("en"), "Content not found");
EXPECT_EQ(msg.getText("test"), "[I18N TESTING] Not Found - Try Again");
}
}
TEST(ParameterizedMessage, messagesWithParameters)
{
{
const ParameterizedMessage msg("filter-by-tag",
{{"TAG", "scifi"}}
);
EXPECT_EQ(msg.getText("en"), "Filter by tag \"scifi\"");
EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"scifi\"");
}
{
// Omitting expected parameters amounts to using empty values for them
const ParameterizedMessage msg("filter-by-tag", {});
EXPECT_EQ(msg.getText("en"), "Filter by tag \"\"");
EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"\"");
}
}

View File

@@ -20,7 +20,6 @@
#include "gtest/gtest.h"
#include <string>
const char * sampleOpdsStream = R"(
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
@@ -28,12 +27,12 @@ const char * sampleOpdsStream = R"(
<id>00000000-0000-0000-0000-000000000000</id>
<entry>
<title>Encyclopédie de la Tunisie</title>
<name>wikipedia_fr_tunisie_novid_2018-10</name>
<flavour>unforgettable</flavour>
<name>wikipedia_fr_tunisie</name>
<flavour>novid</flavour>
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd</id>
<icon>/meta?name=favicon&amp;content=wikipedia_fr_tunisie_novid_2018-10</icon>
<updated>2018-10-08T00:00::00:Z</updated>
<dc:issued>8 Oct 2018</dc:issued>
<dc:issued>2018-10-08T00:00::00:Z</dc:issued>
<language>fra</language>
<summary>Le meilleur de Wikipédia sur la Tunisie</summary>
<tags>wikipedia;novid;_ftindex</tags>
@@ -49,9 +48,53 @@ const char * sampleOpdsStream = R"(
<mediaCount>1100</mediaCount>
<articleCount>172</articleCount>
</entry>
<entry>
<title>Encyclopédie de la Tunisie</title>
<name>wikipedia_fr_tunisie</name>
<flavour>novid</flavour>
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater</id>
<updated>2019-10-08T00:00::00:Z</updated>
<dc:issued>2019-10-08T00:00::00:Z</dc:issued>
<language>fra</language>
<summary>Le meilleur de Wikipédia sur la Tunisie. Updated in 2019</summary>
<author>
<name>Wikipedia</name>
</author>
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4" length="90030080" />
</entry>
<entry>
<title>Encyclopédie de la Tunisie</title>
<name>wikipedia_fr_tunisie</name>
<flavour>other_flavour</flavour>
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd_flavour</id>
<updated>2018-10-08T00:00::00:Z</updated>
<dc:issued>2018-10-08T00:00::00:Z</dc:issued>
<language>fra</language>
<summary>Le meilleur de Wikipédia sur la Tunisie. With another flavour</summary>
<author>
<name>Wikipedia</name>
</author>
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4" length="90030080" />
</entry>
<entry>
<title>Encyclopédie de la Tunisie</title>
<name>wikipedia_fr_tunisie</name>
<flavour>other_flavour</flavour>
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater_flavour</id>
<updated>2019-10-08T00:00::00:Z</updated>
<dc:issued>2019-10-08T00:00::00:Z</dc:issued>
<language>fra</language>
<summary>Le meilleur de Wikipédia sur la Tunisie. Updated in 2019, and other flavour</summary>
<author>
<name>Wikipedia</name>
</author>
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4" length="90030080" />
</entry>
<entry>
<title>Tania Louis</title>
<id>urn:uuid:0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2</id>
<name>biologie-tout-compris_fr_all</name>
<flavour>full</flavour>
<icon>/meta?name=favicon&amp;content=biologie-tout-compris_fr_all_2018-06</icon>
<updated>2018-06-23T00:00::00:Z</updated>
<language>fra</language>
@@ -67,6 +110,8 @@ const char * sampleOpdsStream = R"(
<entry>
<title>Wikiquote</title>
<id>urn:uuid:0ea1cde6-441d-6c58-f2c7-21c2838e659f</id>
<name>wikiquote_fr_all</name>
<flavour>full</flavour>
<icon>/meta?name=favicon&amp;content=wikiquote_fr_all_nopic_2019-06</icon>
<updated>2019-06-05T00:00::00:Z</updated>
<language>fra,ita</language>
@@ -83,6 +128,8 @@ const char * sampleOpdsStream = R"(
<entry>
<title>Géographie par Wikipédia</title>
<id>urn:uuid:1123e574-6eef-6d54-28fc-13e4caeae474</id>
<name>wikipedia_fr_geography</name>
<flavour>full</flavour>
<icon>/meta?name=favicon&amp;content=wikipedia_fr_geography_nopic_2019-06</icon>
<updated>2019-06-02T00:00::00:Z</updated>
<summary>Une sélection d'articles de Wikipédia sur la géographie</summary>
@@ -99,6 +146,8 @@ const char * sampleOpdsStream = R"(
<entry>
<title>Mathématiques</title>
<id>urn:uuid:14829621-c490-c376-0792-9de558b57efa</id>
<name>wikipedia_fr_mathematics</name>
<flavour>novid</flavour>
<icon>/meta?name=favicon&amp;content=wikipedia_fr_mathematics_nopic_2019-05</icon>
<updated>2019-05-13T00:00::00:Z</updated>
<language>fra</language>
@@ -115,6 +164,8 @@ const char * sampleOpdsStream = R"(
<entry>
<title>Granblue Fantasy Wiki</title>
<id>urn:uuid:006cbd1b-16d8-b00d-a584-c1ae110a94ed</id>
<name>grandbluefantasy_en_all</name>
<flavour>novid</flavour>
<icon>/meta?name=favicon&amp;content=granbluefantasy_en_all_all_nopic_2018-10</icon>
<updated>2018-10-14T00:00::00:Z</updated>
<language>eng</language>
@@ -130,6 +181,8 @@ const char * sampleOpdsStream = R"(
<entry>
<title>Movies &amp; TV Stack Exchange</title>
<id>urn:uuid:00f37b00-f4da-0675-995a-770f9c72903e</id>
<name>movies.stackexchange.com_en_all</name>
<flavour>novid</flavour>
<icon>/meta?name=favicon&amp;content=movies.stackexchange.com_en_all_2019-02</icon>
<updated>2019-02-03T00:00::00:Z</updated>
<language>eng</language>
@@ -143,8 +196,10 @@ const char * sampleOpdsStream = R"(
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&amp;content=movies.stackexchange.com_en_all_2019-02" />
</entry>
<entry>
<title>TED talks - Business</title>
<title>TED"talks" - Business</title>
<id>urn:uuid:0189d9be-2fd0-b4b6-7300-20fab0b5cdc8</id>
<name>ted_en_business</name>
<flavour>nodet</flavour>
<icon>/meta?name=favicon&amp;content=ted_en_business_2018-07</icon>
<updated>2018-07-23T00:00::00:Z</updated>
<language>eng</language>
@@ -157,9 +212,28 @@ const char * sampleOpdsStream = R"(
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/ted/ted_en_business_2018-07.zim.meta4" length="8855827456" />
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&amp;content=ted_en_business_2018-07" />
</entry>
<entry>
<title>Business talks about TED</title>
<id>Dummy id </id>
<name>speak_business</name>
<flavour>nodet</flavour>
<icon>/meta?name=favicon&amp;content=ted_en_business_2018-07</icon>
<updated>2018-08-23T00:00::00:Z</updated>
<language>eng</language>
<summary>Ideas worth spreading</summary>
<tags></tags>
<link type="text/html" href="/ted_en_business_2018-07" />
<author>
<name>TED</name>
</author>
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/ted/ted_en_business_2018-07.zim.meta4" length="8855827456" />
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&amp;content=ted_en_business_2018-07" />
</entry>
<entry>
<title>Mythology &amp; Folklore Stack Exchange</title>
<id>urn:uuid:028055ac-4acc-1d54-65e0-a96de45e1b22</id>
<name>mythology.stackexchange.com_en_all</name>
<flavour>novid</flavour>
<icon>/meta?name=favicon&amp;content=mythology.stackexchange.com_en_all_2019-02</icon>
<updated>2019-02-03T00:00::00:Z</updated>
<language>eng</language>
@@ -175,6 +249,8 @@ const char * sampleOpdsStream = R"(
<entry>
<title>Islam Stack Exchange</title>
<id>urn:uuid:02e9c7ff-36fc-9c6e-6ac7-cd7085989029</id>
<name>islam.stackexchange.com_en_all</name>
<flavour>novid</flavour>
<icon>/meta?name=favicon&amp;content=islam.stackexchange.com_en_all_2019-01</icon>
<updated>2019-01-31T00:00::00:Z</updated>
<language>eng</language>
@@ -229,6 +305,7 @@ const char sampleLibraryXML[] = R"(
#include "../include/library.h"
#include "../include/manager.h"
#include "../include/book.h"
#include "../include/bookmark.h"
namespace
@@ -242,17 +319,17 @@ TEST(LibraryOpdsImportTest, allInOne)
kiwix::Manager manager(lib);
manager.readOpds(sampleOpdsStream, "library-opds-import.unittests.dev");
EXPECT_EQ(10U, lib->getBookCount(true, true));
EXPECT_EQ(14U, lib->getBookCount(true, true));
{
const kiwix::Book& book1 = lib->getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd");
EXPECT_EQ(book1.getTitle(), "Encyclopédie de la Tunisie");
EXPECT_EQ(book1.getName(), "wikipedia_fr_tunisie_novid_2018-10");
EXPECT_EQ(book1.getFlavour(), "unforgettable");
EXPECT_EQ(book1.getName(), "wikipedia_fr_tunisie");
EXPECT_EQ(book1.getFlavour(), "novid");
EXPECT_EQ(book1.getLanguages(), Langs{ "fra" });
EXPECT_EQ(book1.getCommaSeparatedLanguages(), "fra");
EXPECT_EQ(book1.getDate(), "8 Oct 2018");
EXPECT_EQ(book1.getDate(), "2018-10-08");
EXPECT_EQ(book1.getDescription(), "Le meilleur de Wikipédia sur la Tunisie");
EXPECT_EQ(book1.getCreator(), "Wikipedia");
EXPECT_EQ(book1.getPublisher(), "Wikipedia Publishing House");
@@ -272,9 +349,9 @@ TEST(LibraryOpdsImportTest, allInOne)
{
const kiwix::Book& book2 = lib->getBookById("0189d9be-2fd0-b4b6-7300-20fab0b5cdc8");
EXPECT_EQ(book2.getTitle(), "TED talks - Business");
EXPECT_EQ(book2.getName(), "");
EXPECT_EQ(book2.getFlavour(), "");
EXPECT_EQ(book2.getTitle(), "TED\"talks\" - Business");
EXPECT_EQ(book2.getName(), "ted_en_business");
EXPECT_EQ(book2.getFlavour(), "nodet");
EXPECT_EQ(book2.getLanguages(), Langs{ "eng" });
EXPECT_EQ(book2.getCommaSeparatedLanguages(), "eng");
EXPECT_EQ(book2.getDate(), "2018-07-23");
@@ -309,11 +386,18 @@ class LibraryTest : public ::testing::Test {
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
}
kiwix::Bookmark createBookmark(const std::string &id) {
kiwix::Bookmark bookmark;
bookmark.setBookId(id);
return bookmark;
};
kiwix::Bookmark createBookmark(const std::string &id, const std::string& url="", const std::string& title="") {
kiwix::Bookmark bookmark;
bookmark.setBookId(id);
bookmark.setUrl(url);
bookmark.setTitle(title);
return bookmark;
};
kiwix::Bookmark createBookmark(const kiwix::Book& book, const std::string& url="", const std::string& title="") {
kiwix::Bookmark bookmark(book, url, title);
return bookmark;
};
TitleCollection ids2Titles(const BookIdCollection& ids) {
TitleCollection titles;
@@ -327,14 +411,35 @@ class LibraryTest : public ::testing::Test {
std::shared_ptr<kiwix::Library> lib;
};
TEST_F(LibraryTest, createBookMark)
{
auto bookId = "0c45160e-f917-760a-9159-dfe3c53cdcdd";
auto book = lib->getBookById(bookId);
auto bookmark = createBookmark(book, "/a/url", "A title");
EXPECT_EQ(bookmark.getUrl(), "/a/url");
EXPECT_EQ(bookmark.getTitle(), "A title");
EXPECT_EQ(bookmark.getBookId(), bookId);
EXPECT_EQ(bookmark.getBookName(), book.getName());
EXPECT_EQ(bookmark.getBookName(), "wikipedia_fr_tunisie");
EXPECT_EQ(bookmark.getBookTitle(), book.getTitle());
EXPECT_EQ(bookmark.getDate(), book.getDate());
EXPECT_EQ(bookmark.getBookFlavour(), book.getFlavour());
EXPECT_EQ(bookmark.getLanguage(), book.getCommaSeparatedLanguages());
}
TEST_F(LibraryTest, getBookMarksTest)
{
auto bookId1 = lib->getBooksIds()[0];
auto bookId2 = lib->getBooksIds()[1];
auto bookId1 = "0c45160e-f917-760a-9159-dfe3c53cdcdd";
auto bookId2 = "0189d9be-2fd0-b4b6-7300-20fab0b5cdc8";
lib->addBookmark(createBookmark(bookId1));
lib->addBookmark(createBookmark("invalid-bookmark-id"));
lib->addBookmark(createBookmark(bookId2));
auto book1 = lib->getBookById(bookId1);
auto book2 = lib->getBookById(bookId2);
lib->addBookmark(createBookmark(book1));
lib->addBookmark(createBookmark("invalid-book-id"));
lib->addBookmark(createBookmark(book2));
auto onlyValidBookmarks = lib->getBookmarks();
auto allBookmarks = lib->getBookmarks(false);
@@ -342,13 +447,284 @@ TEST_F(LibraryTest, getBookMarksTest)
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1);
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-bookmark-id");
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
}
TEST_F(LibraryTest, bookmarksSerializationTest)
{
auto bookId1 = lib->getBooksIds()[0];
auto bookId2 = lib->getBooksIds()[1];
auto book1 = lib->getBookById(bookId1);
auto book2 = lib->getBookById(bookId2);
// Create bookmarks using three different ways.
lib->addBookmark(createBookmark(bookId1, "a/url", "Article title1"));
lib->addBookmark(createBookmark("invalid-book-id", "another/url", "Unknown title"));
lib->addBookmark(createBookmark(book2, "a/url/2", "Article title2"));
lib->writeBookmarksToFile("__test__bookmarks.xml");
// Build a new library
auto new_lib = kiwix::Library::create();
{
kiwix::Manager manager(new_lib);
manager.readOpds(sampleOpdsStream, "foo.urlHost");
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
manager.readBookmarkFile("__test__bookmarks.xml");
}
std::remove("__test__bookmarks.xml");
auto onlyValidBookmarks = new_lib->getBookmarks();
auto allBookmarks = new_lib->getBookmarks(false);
ASSERT_EQ(onlyValidBookmarks.size(), 2U);
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
ASSERT_EQ(allBookmarks.size(), 3U);
auto bookmark1 = allBookmarks[0];
EXPECT_EQ(bookmark1.getBookId(), bookId1);
EXPECT_EQ(bookmark1.getBookTitle(), book1.getTitle());
EXPECT_EQ(bookmark1.getBookName(), book1.getName());
EXPECT_EQ(bookmark1.getBookFlavour(), book1.getFlavour());
EXPECT_EQ(bookmark1.getUrl(), "a/url");
EXPECT_EQ(bookmark1.getTitle(), "Article title1");
EXPECT_EQ(bookmark1.getLanguage(), book1.getCommaSeparatedLanguages());
EXPECT_EQ(bookmark1.getDate(), book1.getDate());
auto bookmark2 = allBookmarks[1];
EXPECT_EQ(bookmark2.getBookId(), "invalid-book-id");
EXPECT_EQ(bookmark2.getBookTitle(), "");
EXPECT_EQ(bookmark2.getBookName(), "");
EXPECT_EQ(bookmark2.getBookFlavour(), "");
EXPECT_EQ(bookmark2.getUrl(), "another/url");
EXPECT_EQ(bookmark2.getTitle(), "Unknown title");
EXPECT_EQ(bookmark2.getLanguage(), "");
EXPECT_EQ(bookmark2.getDate(), "");
auto bookmark3 = allBookmarks[2];
EXPECT_EQ(bookmark3.getBookId(), bookId2);
EXPECT_EQ(bookmark3.getBookTitle(), book2.getTitle());
EXPECT_EQ(bookmark3.getBookName(), book2.getName());
EXPECT_EQ(bookmark3.getBookFlavour(), book2.getFlavour());
EXPECT_EQ(bookmark3.getUrl(), "a/url/2");
EXPECT_EQ(bookmark3.getTitle(), "Article title2");
EXPECT_EQ(bookmark3.getLanguage(), book2.getCommaSeparatedLanguages());
EXPECT_EQ(bookmark3.getDate(), book2.getDate());
}
TEST_F(LibraryTest, MigrateBookmark)
{
std::string bookId1 = "0c45160e-f917-760a-9159-dfe3c53cdcdd";
std::string bookId2 = "0189d9be-2fd0-b4b6-7300-20fab0b5cdc8";
auto book1 = lib->getBookById(bookId1);
auto book1Flavour = lib->getBookById(bookId1+"_flavour");
auto book2 = lib->getBookById(bookId2);
lib->addBookmark(createBookmark(book1));
lib->addBookmark(createBookmark("invalid-book-id"));
lib->addBookmark(createBookmark(book2));
auto wrongIdBookmark = createBookmark(book1);
wrongIdBookmark.setBookId("wrong-book-id");
lib->addBookmark(wrongIdBookmark);
auto wrongIdBookmarkNoName = createBookmark(book2);
wrongIdBookmarkNoName.setBookId("wrong-book-id-noname");
wrongIdBookmarkNoName.setBookName("");
lib->addBookmark(wrongIdBookmarkNoName);
auto wrongIdFlavourBookmark = createBookmark(book1Flavour);
wrongIdFlavourBookmark.setBookId("wrong-book-flavour-id");
lib->addBookmark(wrongIdFlavourBookmark);
auto onlyValidBookmarks = lib->getBookmarks();
auto allBookmarks = lib->getBookmarks(false);
ASSERT_EQ(onlyValidBookmarks.size(), 2U);
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
ASSERT_EQ(allBookmarks.size(), 6U);
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1);
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[3].getBookId(), "wrong-book-id");
EXPECT_EQ(allBookmarks[4].getBookId(), "wrong-book-id-noname");
EXPECT_EQ(allBookmarks[5].getBookId(), "wrong-book-flavour-id");
ASSERT_EQ(lib->migrateBookmarks("no-existant-book"), 0);
ASSERT_EQ(lib->migrateBookmarks(), std::make_tuple(3, 4));
onlyValidBookmarks = lib->getBookmarks();
allBookmarks = lib->getBookmarks(false);
ASSERT_EQ(onlyValidBookmarks.size(), 5U);
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[2].getBookId(), bookId1+"_updated1yearlater");
EXPECT_EQ(onlyValidBookmarks[3].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[4].getBookId(), bookId1+"_updated1yearlater_flavour");
ASSERT_EQ(allBookmarks.size(), 6U);
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1);
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[3].getBookId(), bookId1+"_updated1yearlater");
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
ASSERT_EQ(lib->migrateBookmarks(), std::make_tuple(0, 1));
ASSERT_EQ(lib->migrateBookmarks(bookId1), 1);
allBookmarks = lib->getBookmarks(false);
ASSERT_EQ(allBookmarks.size(), 6U);
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1+"_updated1yearlater");
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[3].getBookId(), bookId1+"_updated1yearlater");
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
ASSERT_EQ(lib->migrateBookmarks(bookId1, bookId2), 0); // No more bookId1 bookmark
ASSERT_EQ(lib->migrateBookmarks(bookId1+"_updated1yearlater", bookId2), 2);
onlyValidBookmarks = lib->getBookmarks();
allBookmarks = lib->getBookmarks(false);
ASSERT_EQ(onlyValidBookmarks.size(), 5U);
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[2].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[3].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[4].getBookId(), bookId1+"_updated1yearlater_flavour");
ASSERT_EQ(allBookmarks.size(), 6U);
EXPECT_EQ(allBookmarks[0].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[3].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
ASSERT_EQ(lib->migrateBookmarks("invalid-book-id", bookId1), 1);
onlyValidBookmarks = lib->getBookmarks();
allBookmarks = lib->getBookmarks(false);
ASSERT_EQ(onlyValidBookmarks.size(), 6U);
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId1);
EXPECT_EQ(onlyValidBookmarks[2].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[3].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[4].getBookId(), bookId2);
EXPECT_EQ(onlyValidBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
ASSERT_EQ(allBookmarks.size(), 6U);
EXPECT_EQ(allBookmarks[0].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[1].getBookId(), bookId1);
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[3].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
}
TEST_F(LibraryTest, GetBestTargetBookIdOlder)
{
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd");
auto book = lib->getBookById(bookId);
auto validBookmark = createBookmark(book);
lib->addBookmark(validBookmark);
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::UPGRADE_ONLY), bookId+"_updated1yearlater");
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::ALLOW_DOWNGRADE), bookId+"_updated1yearlater");
}
TEST_F(LibraryTest, GetBestTargetBookIdNewer)
{
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater");
auto book = lib->getBookById(bookId);
EXPECT_EQ(book.getDate(), "2019-10-08");
auto validBookmark = createBookmark(book);
// Make the bookmark more recent than any books in the library.
// (But still pointing to existing book)
validBookmark.setDate("2020-10-08");
lib->addBookmark(validBookmark);
// The best book for the bookmark is bookId...
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::UPGRADE_ONLY), bookId);
// but there is not migration to do as the bookmark already point to it.
ASSERT_EQ(lib->migrateBookmarks(bookId, kiwix::UPGRADE_ONLY), 0);
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::ALLOW_DOWNGRADE), bookId);
}
TEST_F(LibraryTest, GetBestTargetBookIdInvalidOlder)
{
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd");
auto book = lib->getBookById(bookId);
auto invalidBookmark = createBookmark(book);
invalidBookmark.setBookId("invalid-book-id");
lib->addBookmark(invalidBookmark);
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::UPGRADE_ONLY), bookId+"_updated1yearlater");
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::ALLOW_DOWNGRADE), bookId+"_updated1yearlater");
}
TEST_F(LibraryTest, GetBestTargetBookIdInvalidNewer)
{
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd");
auto book = lib->getBookById(bookId);
EXPECT_EQ(book.getDate(), "2018-10-08");
auto invalidBookmark = createBookmark(book);
invalidBookmark.setBookId("invalid-book-id");
invalidBookmark.setDate("2020-10-08");
lib->addBookmark(invalidBookmark);
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::UPGRADE_ONLY), "");
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::ALLOW_DOWNGRADE), bookId+"_updated1yearlater");
}
TEST_F(LibraryTest, GetBestTargetBookIdFlavour)
{
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd_flavour");
auto book = lib->getBookById(bookId);
EXPECT_EQ(book.getDate(), "2018-10-08");
auto invalidBookmark = createBookmark(book);
invalidBookmark.setBookId("invalid-book-id");
invalidBookmark.setDate("2020-10-08");
lib->addBookmark(invalidBookmark);
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::UPGRADE_ONLY), "");
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::ALLOW_DOWNGRADE), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater_flavour");
}
TEST_F(LibraryTest, GetBestTargetBookIdName)
{
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie"), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater");
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie", "novid"), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater");
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie", "other_flavour"), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater_flavour");
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie", "other_flavour", "2020-12-12"), "");
}
TEST_F(LibraryTest, sanityCheck)
{
EXPECT_EQ(lib->getBookCount(true, true), 12U);
EXPECT_EQ(lib->getBookCount(true, true), 16U);
EXPECT_EQ(lib->getBooksLanguages(),
std::vector<std::string>({"deu", "eng", "fra", "ita", "spa"})
);
@@ -400,6 +776,10 @@ TEST_F(LibraryTest, filterLocal)
);
EXPECT_FILTER_RESULTS(kiwix::Filter().local(false),
"Business talks about TED",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Granblue Fantasy Wiki",
"Géographie par Wikipédia",
@@ -407,7 +787,7 @@ TEST_F(LibraryTest, filterLocal)
"Mathématiques",
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
"TED talks - Business",
"TED\"talks\" - Business",
"Tania Louis",
"Wikiquote"
);
@@ -416,6 +796,10 @@ TEST_F(LibraryTest, filterLocal)
TEST_F(LibraryTest, filterRemote)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().remote(true),
"Business talks about TED",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Granblue Fantasy Wiki",
"Géographie par Wikipédia",
@@ -424,7 +808,7 @@ TEST_F(LibraryTest, filterRemote)
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
"Ray Charles",
"TED talks - Business",
"TED\"talks\" - Business",
"Tania Louis",
"Wikiquote"
);
@@ -437,21 +821,23 @@ TEST_F(LibraryTest, filterRemote)
TEST_F(LibraryTest, filterByLanguage)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().lang("eng"),
"Business talks about TED",
"Granblue Fantasy Wiki",
"Islam Stack Exchange",
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
"Ray Charles",
"TED talks - Business"
"TED\"talks\" - Business"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("lang:eng"),
"Business talks about TED",
"Granblue Fantasy Wiki",
"Islam Stack Exchange",
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
"Ray Charles",
"TED talks - Business"
"TED\"talks\" - Business"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("eng"),
@@ -459,6 +845,25 @@ TEST_F(LibraryTest, filterByLanguage)
);
}
TEST_F(LibraryTest, filterByFlavour)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().flavour("full"),
"Géographie par Wikipédia",
"Tania Louis",
"Wikiquote"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("flavour:full"),
"Géographie par Wikipédia",
"Tania Louis",
"Wikiquote"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("full"),
/* no results */
);
}
TEST_F(LibraryTest, filterByTags)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"stackexchange"}),
@@ -558,6 +963,9 @@ TEST_F(LibraryTest, filterByQuery)
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki"),
"An example ZIM archive", // due to the "wikibooks" tag
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Granblue Fantasy Wiki",
"Géographie par Wikipédia",
"Mathématiques", // due to the "wikipedia" tag
@@ -576,6 +984,10 @@ TEST_F(LibraryTest, filteringByEmptyQueryReturnsAllEntries)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().query(""),
"An example ZIM archive",
"Business talks about TED",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Granblue Fantasy Wiki",
"Géographie par Wikipédia",
@@ -584,7 +996,7 @@ TEST_F(LibraryTest, filteringByEmptyQueryReturnsAllEntries)
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
"Ray Charles",
"TED talks - Business",
"TED\"talks\" - Business",
"Tania Louis",
"Wikiquote"
);
@@ -593,6 +1005,9 @@ TEST_F(LibraryTest, filteringByEmptyQueryReturnsAllEntries)
TEST_F(LibraryTest, filterByCreator)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Wikipedia"),
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Géographie par Wikipédia",
"Mathématiques",
@@ -634,6 +1049,9 @@ TEST_F(LibraryTest, filterByCreator)
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("creator:Wikipedia"),
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Géographie par Wikipédia",
"Mathématiques",
@@ -741,6 +1159,9 @@ TEST_F(LibraryTest, filterByMaxSize)
TEST_F(LibraryTest, filterByMultipleCriteria)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia"),
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Géographie par Wikipédia",
"Mathématiques", // due to the "wikipedia" tag
@@ -748,11 +1169,17 @@ TEST_F(LibraryTest, filterByMultipleCriteria)
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia").maxSize(100000000UL),
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Ray Charles"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia").maxSize(100000000UL).local(false),
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie"
);
}
@@ -810,6 +1237,10 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
{
EXPECT_FILTER_RESULTS(kiwix::Filter(),
"An example ZIM archive",
"Business talks about TED",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Encyclopédie de la Tunisie",
"Granblue Fantasy Wiki",
"Géographie par Wikipédia",
@@ -818,7 +1249,7 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
"Ray Charles",
"TED talks - Business",
"TED\"talks\" - Business",
"Tania Louis",
"Wikiquote"
);
@@ -832,7 +1263,7 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
const uint64_t rev2 = lib->getRevision();
EXPECT_EQ(9u, lib->removeBooksNotUpdatedSince(rev));
EXPECT_EQ(13u, lib->removeBooksNotUpdatedSince(rev));
EXPECT_GT(lib->getRevision(), rev2);

View File

@@ -13,7 +13,9 @@ tests = [
'name_mapper',
'opds_catalog',
'server_helper',
'lrucache'
'lrucache',
'i18n',
'response'
]
if build_machine.system() != 'windows'

View File

@@ -110,10 +110,10 @@ TEST(Suggestions, specialCharHandling)
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "Title with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippet with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"value" : "Title with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippet with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"kind" : "path"
, "path" : "Path with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
, "path" : "Path with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
}
]
)EXPECTEDJSON"
@@ -128,10 +128,10 @@ R"EXPECTEDJSON([
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "Snippetless title with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippetless title with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"value" : "Snippetless title with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippetless title with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"kind" : "path"
, "path" : "Path with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
, "path" : "Path with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?"
}
]
)EXPECTEDJSON"
@@ -145,8 +145,8 @@ R"EXPECTEDJSON([
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "text with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.? ",
"label" : "containing &apos;text with \u0009\u0010\u0013\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?&apos;...",
"value" : "text with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.? ",
"label" : "containing &apos;text with \t\n\r\\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}

101
test/response.cpp Normal file
View File

@@ -0,0 +1,101 @@
#include "../src/server/response.h"
#include "gtest/gtest.h"
#include "../src/server/request_context.h"
namespace
{
using namespace kiwix;
RequestContext makeHttpGetRequest(const std::string& url,
const RequestContext::NameValuePairs& headers,
const RequestContext::NameValuePairs& queryArgs)
{
return RequestContext("", url, "GET", "1.1", headers, queryArgs);
}
std::string getResponseContent(const ContentResponseBlueprint& crb)
{
return crb.generateResponseObject()->getContent();
}
} // unnamed namespace
TEST(HTTPErrorResponse, shouldBeInEnglishByDefault) {
const RequestContext req = makeHttpGetRequest("/asdf", {}, {});
HTTPErrorResponse errResp(req, MHD_HTTP_NOT_FOUND,
"404-page-title",
"404-page-heading",
"/css/error.css",
/*includeKiwixResponseData=*/true);
errResp += ParameterizedMessage("suggest-search",
{
{ "PATTERN", "asdf" },
{ "SEARCH_URL", "/search?q=asdf" }
});
EXPECT_EQ(getResponseContent(errResp),
R"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Content not found</title>
<link type="text/css" href="/css/error.css" rel="Stylesheet" />
<script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;\n &lt;head&gt;\n &lt;meta content=&quot;text/html;charset=UTF-8&quot; http-equiv=&quot;content-type&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n{{#CSS_URL}}\n &lt;link type=&quot;text/css&quot; href=&quot;{{{CSS_URL}}}&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;{{/KIWIX_RESPONSE_DATA}}\n &lt;/head&gt;\n &lt;body&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n{{#details}}\n &lt;p&gt;\n {{{p}}}\n &lt;/p&gt;\n{{/details}}\n &lt;/body&gt;\n&lt;/html&gt;\n";
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/css/error.css", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "asdf", "SEARCH_URL" : "/search?q=asdf" } } } ] };
</script>
</head>
<body>
<h1>Not Found</h1>
<p>
Make a full text search for <a href="/search?q=asdf">asdf</a>
</p>
</body>
</html>
)");
}
TEST(HTTPErrorResponse, shouldBeTranslatable) {
const RequestContext req = makeHttpGetRequest("/asdf",
/* headers */ {},
/* query args */ {{"userlang", "test"}}
);
HTTPErrorResponse errResp(req, MHD_HTTP_NOT_FOUND,
"404-page-title",
"404-page-heading",
"/css/error.css",
/*includeKiwixResponseData=*/true);
errResp += ParameterizedMessage("suggest-search",
{
{ "PATTERN", "asdf" },
{ "SEARCH_URL", "/search?q=asdf" }
});
EXPECT_EQ(getResponseContent(errResp),
R"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>[I18N TESTING] Not Found - Try Again</title>
<link type="text/css" href="/css/error.css" rel="Stylesheet" />
<script>
window.KIWIX_RESPONSE_TEMPLATE = "&lt;!DOCTYPE html&gt;\n&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;\n &lt;head&gt;\n &lt;meta content=&quot;text/html;charset=UTF-8&quot; http-equiv=&quot;content-type&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n{{#CSS_URL}}\n &lt;link type=&quot;text/css&quot; href=&quot;{{{CSS_URL}}}&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;{{/KIWIX_RESPONSE_DATA}}\n &lt;/head&gt;\n &lt;body&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n{{#details}}\n &lt;p&gt;\n {{{p}}}\n &lt;/p&gt;\n{{/details}}\n &lt;/body&gt;\n&lt;/html&gt;\n";
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/css/error.css", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "asdf", "SEARCH_URL" : "/search?q=asdf" } } } ] };
</script>
</head>
<body>
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
[I18N TESTING] Make a full text search for <a href="/search?q=asdf">asdf</a>
</p>
</body>
</html>
)");
}

View File

@@ -59,7 +59,7 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=6a8c6fb2" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
@@ -75,7 +75,7 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=e014a885" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=201653b8" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=5fc4badf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" },
@@ -83,6 +83,8 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json" },
// TODO: implement cache management of i18n resources
//{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json?cacheid=unknown" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=5be77f5c" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
@@ -144,12 +146,12 @@ const ResourceCollection resources200Uncompressible{
{ STATIC_CONTENT, "/ROOT%23%3F/skin/hash.png?cacheid=f836e872" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/magnet.png" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/magnet.png?cacheid=73b6bddf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/polyfills.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/polyfills.js?cacheid=a0e0343d" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/search-icon.svg" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/search-icon.svg?cacheid=b10ae7ed" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/search_results.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=96f2cf73" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Description" },
@@ -285,8 +287,9 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
<link rel="mask-icon" href="/ROOT%23%3F/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95" color="#5bbad5">
<link rel="shortcut icon" href="/ROOT%23%3F/skin/favicon/favicon.ico?cacheid=92663314">
<meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=6a8c6fb2" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=96f2cf73" defer></script>
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=5be77f5c" defer></script>
<script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
<script src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=ce19da2a" defer></script>
@@ -318,9 +321,10 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fad9" rel="Stylesheet" />
<link type="text/css" href="./skin/taskbar.css?cacheid=e014a885" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" rel="Stylesheet" />
<script type="module" src="./skin/i18n.js?cacheid=6a8c6fb2" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=96f2cf73" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=201653b8" defer></script>
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
<script type="module" src="./skin/i18n.js?cacheid=071abc9a" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=5be77f5c" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=5fc4badf" defer></script>
<script type="text/javascript" src="./skin/autoComplete/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>
@@ -337,6 +341,7 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fa
// a page rendered from static/templates/no_search_result_html
/* url */ "/ROOT%23%3F/search?content=poor&pattern=whatever",
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" rel="Stylesheet" />
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] };
)EXPECTEDRESULT"
},
};
@@ -535,6 +540,7 @@ struct ExpectedResponseData
{
const std::string expectedPageTitle;
const std::string expectedCssUrl;
const std::string expectedKiwixResponseData;
const std::string bookName;
const std::string bookTitle;
const std::string expectedBody;
@@ -544,6 +550,7 @@ enum ExpectedResponseDataType
{
expected_page_title,
expected_css_url,
expected_kiwix_response_data,
book_name,
book_title,
expected_body
@@ -556,11 +563,13 @@ ExpectedResponseData operator==(ExpectedResponseDataType t, std::string s)
{
switch (t)
{
case expected_page_title: return ExpectedResponseData{s, "", "", "", ""};
case expected_css_url: return ExpectedResponseData{"", s, "", "", ""};
case book_name: return ExpectedResponseData{"", "", s, "", ""};
case book_title: return ExpectedResponseData{"", "", "", s, ""};
case expected_body: return ExpectedResponseData{"", "", "", "", s};
case expected_page_title: return ExpectedResponseData{s, "", "", "", "", ""};
case expected_css_url: return ExpectedResponseData{"", s, "", "", "", ""};
case expected_kiwix_response_data:
return ExpectedResponseData{"", "", s, "", "", ""};
case book_name: return ExpectedResponseData{"", "", "", s, "", ""};
case book_title: return ExpectedResponseData{"", "", "", "", s, ""};
case expected_body: return ExpectedResponseData{"", "", "", "", "", s};
default: assert(false); return ExpectedResponseData{};
}
}
@@ -579,6 +588,7 @@ ExpectedResponseData operator&&(const ExpectedResponseData& a,
return ExpectedResponseData{
selectNonEmpty(a.expectedPageTitle, b.expectedPageTitle),
selectNonEmpty(a.expectedCssUrl, b.expectedCssUrl),
selectNonEmpty(a.expectedKiwixResponseData, b.expectedKiwixResponseData),
selectNonEmpty(a.bookName, b.bookName),
selectNonEmpty(a.bookTitle, b.bookTitle),
selectNonEmpty(a.expectedBody, b.expectedBody)
@@ -607,19 +617,29 @@ private:
std::string TestContentIn404HtmlResponse::expectedResponse() const
{
const std::string frag[] = {
// frag[0]
R"FRAG(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>)FRAG",
// frag[1]
R"FRAG(</title>
)FRAG",
R"FRAG(
// frag[2]
R"( <script>
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
window.KIWIX_RESPONSE_DATA = )",
// frag[3]
R"FRAG(;
</script>
</head>
<body>)FRAG",
// frag[4]
R"FRAG( </body>
</html>
)FRAG"
@@ -630,8 +650,10 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
+ frag[1]
+ pageCssLink()
+ frag[2]
+ expectedKiwixResponseData
+ frag[3]
+ expectedBody
+ frag[3];
+ frag[4];
}
std::string TestContentIn404HtmlResponse::pageTitle() const
@@ -648,7 +670,8 @@ std::string TestContentIn404HtmlResponse::pageCssLink() const
return R"( <link type="text/css" href=")"
+ expectedCssUrl
+ R"(" rel="Stylesheet" />)";
+ R"(" rel="Stylesheet" />)"
+ "\n";
}
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
@@ -676,6 +699,7 @@ TEST_F(ServerTest, Http404HtmlError)
using namespace TestingOfHtmlResponses;
const std::vector<TestContentIn404HtmlResponse> testData{
{ /* url */ "/ROOT%23%3F/random?content=non-existent-book",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -685,6 +709,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/random?content=non-existent-book&userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
@@ -693,6 +718,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/suggest?content=no-such-book&term=whatever",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -701,6 +727,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/catalog/",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -710,6 +737,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/catalog/?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/" } } } ] })" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
@@ -718,6 +746,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/catalog/invalid_endpoint",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/invalid_endpoint" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -727,6 +756,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/catalog/invalid_endpoint?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/invalid_endpoint" } } } ] })" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
@@ -735,6 +765,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/content/invalid-book/whatever",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/invalid-book/whatever" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "whatever", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=whatever" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -748,6 +779,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -759,6 +791,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ R"(/ROOT%23%3F/content/"><svg onload=alert(1)>)",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -772,6 +805,7 @@ TEST_F(ServerTest, Http404HtmlError)
{ /* url */ R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -782,10 +816,27 @@ TEST_F(ServerTest, Http404HtmlError)
</p>
)" },
// XXX: This test case is against a "</script>" string appearing inside
// XXX: javascript code that will confuse the HTML parser
{ /* url */ R"(/ROOT%23%3F/content/zimfile/</script>)",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/</scr\ipt>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "script>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=script%3E" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT%23%3F/content/zimfile/&lt;/script&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=script%3E">script&gt;</a>
</p>
)" },
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
expected_body==R"(
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
<p>
@@ -797,6 +848,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/raw/no-such-book/meta/Title",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/no-such-book/meta/Title" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -808,6 +860,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/raw/zimfile/XYZ",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/XYZ" } } }, { "p" : { "msgid" : "invalid-raw-data-type", "params" : { "DATATYPE" : "XYZ" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -819,6 +872,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "meta", "ENTRY" : "invalid-metadata" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -830,6 +884,7 @@ TEST_F(ServerTest, Http404HtmlError)
)" },
{ /* url */ "/ROOT%23%3F/raw/zimfile/content/invalid-article",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/content/invalid-article" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "content", "ENTRY" : "invalid-article" } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -845,6 +900,7 @@ TEST_F(ServerTest, Http404HtmlError)
expected_css_url=="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" &&
book_name=="poor" &&
book_title=="poor" &&
expected_kiwix_response_data==R"({ "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] })" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
@@ -866,6 +922,7 @@ TEST_F(ServerTest, Http400HtmlError)
using namespace TestingOfHtmlResponses;
const std::vector<TestContentIn400HtmlResponse> testData{
{ /* url */ "/ROOT%23%3F/search",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search" } } }, { "p" : { "msgid" : "too-many-books", "params" : { "LIMIT" : "3", "NB_BOOKS" : "4" } } } ] })" &&
expected_body== R"(
<h1>Invalid request</h1>
<p>
@@ -876,6 +933,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p>
)" },
{ /* url */ "/ROOT%23%3F/search?content=zimfile",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=zimfile" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
expected_body==R"(
<h1>Invalid request</h1>
<p>
@@ -886,6 +944,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p>
)" },
{ /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existing-book" } } } ] })" &&
expected_body==R"(
<h1>Invalid request</h1>
<p>
@@ -896,6 +955,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p>
)" },
{ /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=a\"<script foo>",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=non-existing-book&pattern=a%22%3Cscript%20foo%3E" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existing-book" } } } ] })" &&
expected_body==R"(
<h1>Invalid request</h1>
<p>
@@ -908,6 +968,7 @@ TEST_F(ServerTest, Http400HtmlError)
// There is a flaw in our way to handle query string, we cannot differenciate
// between `pattern` and `pattern=`
{ /* url */ "/ROOT%23%3F/search?books.filter.lang=eng&pattern",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?books.filter.lang=eng&pattern" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
expected_body==R"(
<h1>Invalid request</h1>
<p>
@@ -918,6 +979,7 @@ TEST_F(ServerTest, Http400HtmlError)
</p>
)" },
{ /* url */ "/ROOT%23%3F/search?pattern=foo",
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?pattern=foo" } } }, { "p" : { "msgid" : "too-many-books", "params" : { "LIMIT" : "3", "NB_BOOKS" : "4" } } } ] })" &&
expected_body==R"(
<h1>Invalid request</h1>
<p>
@@ -927,6 +989,20 @@ TEST_F(ServerTest, Http400HtmlError)
Too many books requested (4) where limit is 3
</p>
)" },
// Testing of translation
{ /* url */ "/ROOT%23%3F/search?content=zimfile&userlang=test",
expected_page_title=="[I18N TESTING] Invalid request ($400 fine must be paid)" &&
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=zimfile&userlang=test" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
expected_body==R"(
<h1>[I18N TESTING] -400 karma for an invalid request</h1>
<p>
[I18N TESTING] Invalid URL: "/ROOT%23%3F/search?content=zimfile&userlang=test"
</p>
<p>
[I18N TESTING] Kiwix can read your thoughts but it is against GDPR. Please provide your query explicitly.
</p>
)" },
};
for ( const auto& t : testData ) {
@@ -1023,7 +1099,10 @@ TEST_F(ServerTest, 500)
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Internal Server Error</title>
<script>
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "500-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "500-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "500-page-text", "params" : { } } }, { "p" : { "msgid" : "non-translated-text", "params" : { "MSG" : "Entry redirect_loop.html is a redirect entry." } } } ] };
</script>
</head>
<body>
<h1>Internal Server Error</h1>
@@ -1041,6 +1120,7 @@ TEST_F(ServerTest, 500)
const auto r = zfs1_->GET("/ROOT%23%3F/content/poor/A/redirect_loop.html");
EXPECT_EQ(r->status, 500);
EXPECT_EQ(r->body, expectedBody);
EXPECT_EQ(r->get_header_value("Content-Type"), "text/html; charset=utf-8");
}
}
@@ -1050,106 +1130,194 @@ TEST_F(ServerTest, UserLanguageList)
EXPECT_EQ(r->body,
R"EXPECTEDRESPONSE(const uiLanguages = [
{
"الإنجليزية": "ar"
"iso_code": "ar",
"self_name": "الإنجليزية",
"translation_count": 25
},
{
"বাংলা": "bn"
"iso_code": "bn",
"self_name": "বাংলা",
"translation_count": 14
},
{
"Čeština": "cs"
"iso_code": "br",
"self_name": "brezhoneg",
"translation_count": 35
},
{
"Deutsch": "de"
"iso_code": "cs",
"self_name": "Čeština",
"translation_count": 25
},
{
"English": "en"
"iso_code": "dag",
"self_name": "Silimiinsili",
"translation_count": 24
},
{
"español": "es"
"iso_code": "de",
"self_name": "Deutsch",
"translation_count": 57
},
{
"suomi": "fi"
"iso_code": "en",
"self_name": "English",
"translation_count": 58
},
{
"Français": "fr"
"iso_code": "es",
"self_name": "español",
"translation_count": 48
},
{
"עברית": "he"
"iso_code": "fi",
"self_name": "suomi",
"translation_count": 22
},
{
"हिन्दी": "hi"
"iso_code": "fr",
"self_name": "Français",
"translation_count": 57
},
{
"Հայերեն": "hy"
"iso_code": "ha",
"self_name": "Turanci",
"translation_count": 57
},
{
"interlingua": "ia"
"iso_code": "he",
"self_name": "עברית",
"translation_count": 57
},
{
"italiano": "it"
"iso_code": "hi",
"self_name": "हिन्दी",
"translation_count": 49
},
{
"日本語": "ja"
"iso_code": "hy",
"self_name": "Հայերեն",
"translation_count": 15
},
{
"한국어": "ko"
"iso_code": "ia",
"self_name": "interlingua",
"translation_count": 49
},
{
"kurdî": "ku-latn"
"iso_code": "ig",
"self_name": "Bekee",
"translation_count": 57
},
{
"Lëtzebuergesch": "lb"
"iso_code": "it",
"self_name": "italiano",
"translation_count": 34
},
{
"македонски": "mk"
"iso_code": "ja",
"self_name": "日本語",
"translation_count": 26
},
{
"Bahasa Melayu": "ms"
"iso_code": "ko",
"self_name": "한국어",
"translation_count": 13
},
{
"Nederlands": "nl"
"iso_code": "ku-latn",
"self_name": "kurdî",
"translation_count": 26
},
{
"ߒߞߏ": "nqo"
"iso_code": "lb",
"self_name": "Lëtzebuergesch",
"translation_count": 22
},
{
"ଓଡ଼ିଆ": "or"
"iso_code": "mk",
"self_name": "македонски",
"translation_count": 57
},
{
"Polski": "pl"
"iso_code": "ms",
"self_name": "Bahasa Melayu",
"translation_count": 14
},
{
"русский": "ru"
"iso_code": "nl",
"self_name": "Nederlands",
"translation_count": 49
},
{
"Sardu": "sc"
"iso_code": "nqo",
"self_name": "ߒߞߏ",
"translation_count": 43
},
{
"slovenčina": "sk"
"iso_code": "or",
"self_name": "ଓଡ଼ିଆ",
"translation_count": 49
},
{
"سرائیکی": "skr-arab"
"iso_code": "pl",
"self_name": "Polski",
"translation_count": 31
},
{
"slovenščina": "sl"
"iso_code": "ru",
"self_name": "русский",
"translation_count": 57
},
{
"Shqip": "sq"
"iso_code": "sc",
"self_name": "Sardu",
"translation_count": 49
},
{
"Svenska": "sv"
"iso_code": "sk",
"self_name": "slovenčina",
"translation_count": 25
},
{
"ఇంగ్లీషు": "te"
"iso_code": "skr-arab",
"self_name": "سرائیکی",
"translation_count": 20
},
{
"Türkçe": "tr"
"iso_code": "sl",
"self_name": "slovenščina",
"translation_count": 57
},
{
"英语": "zh-hans"
"iso_code": "sq",
"self_name": "Shqip",
"translation_count": 49
},
{
"繁體中文": "zh-hant"
"iso_code": "sv",
"self_name": "Svenska",
"translation_count": 57
},
{
"iso_code": "te",
"self_name": "ఇంగ్లీషు",
"translation_count": 49
},
{
"iso_code": "tr",
"self_name": "Türkçe",
"translation_count": 57
},
{
"iso_code": "zh-hans",
"self_name": "英语",
"translation_count": 54
},
{
"iso_code": "zh-hant",
"self_name": "繁體中文",
"translation_count": 57
}
])EXPECTEDRESPONSE");
}
@@ -1161,7 +1329,6 @@ TEST_F(ServerTest, UserLanguageControl)
const std::string description;
const std::string url;
const std::string acceptLanguageHeader;
const char* const requestCookie; // Cookie: header of the request
const std::string expectedH1;
operator TestContext() const
@@ -1172,64 +1339,45 @@ TEST_F(ServerTest, UserLanguageControl)
{"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%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "Not Found"
},
{
"userlang URL query parameter is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "Not Found"
},
{
"userlang URL query parameter is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
/*Accept-Language:*/ "",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"'Accept-Language: *' is handled",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "*",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "Not Found"
},
{
"Accept-Language: header is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"userlang cookie is ignored",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/*Request Cookie:*/ "userlang=test",
/* expected <h1> */ "Not Found"
},
{
"userlang query parameter takes precedence over Accept-Language",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "test",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "Not Found"
},
{
@@ -1238,7 +1386,6 @@ TEST_F(ServerTest, UserLanguageControl)
// with quality values) the most suitable language is selected.
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test;q=0.9, en;q=0.2",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
@@ -1247,7 +1394,6 @@ TEST_F(ServerTest, UserLanguageControl)
// with quality values) the most suitable language is selected.
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test;q=0.2, en;q=0.9",
/*Request Cookie:*/ NO_COOKIE,
/* expected <h1> */ "Not Found"
},
};
@@ -1259,9 +1405,6 @@ 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);
EXPECT_FALSE(r->has_header("Set-Cookie"));
std::regex_search(r->body, h1Match, h1Regex);
@@ -1958,8 +2101,7 @@ TEST_F(ServerTest, viewerSettings)
R"(const viewerSettings = {
toolbarEnabled: false,
linkBlockingEnabled: false,
libraryButtonEnabled: false,
defaultUserLanguage: "en"
libraryButtonEnabled: false
}
)");
}
@@ -1970,8 +2112,7 @@ R"(const viewerSettings = {
R"(const viewerSettings = {
toolbarEnabled: false,
linkBlockingEnabled: true,
libraryButtonEnabled: false,
defaultUserLanguage: "en"
libraryButtonEnabled: false
}
)");
}
@@ -1982,8 +2123,7 @@ R"(const viewerSettings = {
R"(const viewerSettings = {
toolbarEnabled: true,
linkBlockingEnabled: false,
libraryButtonEnabled: false,
defaultUserLanguage: "en"
libraryButtonEnabled: false
}
)");
}
@@ -1994,47 +2134,7 @@ R"(const viewerSettings = {
R"(const viewerSettings = {
toolbarEnabled: true,
linkBlockingEnabled: false,
libraryButtonEnabled: true,
defaultUserLanguage: "en"
}
)");
}
{
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
const Headers headers{ {"Accept-Language", "fr"} };
ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js", headers)->body,
R"(const viewerSettings = {
toolbarEnabled: true,
linkBlockingEnabled: false,
libraryButtonEnabled: true,
defaultUserLanguage: "fr"
}
)");
}
{
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
const Headers headers{ {"Accept-Language", "test;q=0.2, en;q=0.9"} };
ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js", headers)->body,
R"(const viewerSettings = {
toolbarEnabled: true,
linkBlockingEnabled: false,
libraryButtonEnabled: true,
defaultUserLanguage: "en"
}
)");
}
{
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
const Headers headers{ {"Accept-Language", "test;q=0.9, en;q=0.2"} };
ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js", headers)->body,
R"(const viewerSettings = {
toolbarEnabled: true,
linkBlockingEnabled: false,
libraryButtonEnabled: true,
defaultUserLanguage: "test"
libraryButtonEnabled: true
}
)");
}

View File

@@ -113,7 +113,7 @@ std::string makeSearchResultsHtml(const std::string& pattern,
}
</style>
<title>Search: %PATTERN%</title>
<title>%USERLANGMARKER%Search: %PATTERN%</title>
</head>
<body bgcolor="white">
<div class="header">
@@ -173,8 +173,8 @@ struct SearchResult
+ " " + title + "\n"
+ " </a>\n"
+ " <cite>" + snippet + "</cite>\n"
+ " <div class=\"book-title\">from " + bookTitle + "</div>\n"
+ " <div class=\"informations\">" + wordCount + " words</div>\n";
+ " <div class=\"book-title\">from %USERLANGMARKER%" + bookTitle + "</div>\n"
+ " <div class=\"informations\">" + wordCount + " %USERLANGMARKER%words</div>\n";
}
std::string getXml() const
@@ -737,26 +737,16 @@ struct TestData
std::string expectedHtmlHeader() const
{
if ( totalResultCount == 0 ) {
return "\n No results were found for <b>\"" + getPattern() + "\"</b>";
}
std::string header = R"( Results
<b>
FIRSTRESULT-LASTRESULT
</b> of <b>
RESULTCOUNT
</b> for <b>
"PATTERN"
</b>
)";
std::string header = totalResultCount == 0
? R"(No results were found for <b>"PATTERN"</b>)"
: R"(Results <b>FIRSTRESULT-LASTRESULT</b> of <b>RESULTCOUNT</b> for <b>"PATTERN"</b>)";
const size_t lastResultIndex = std::min(totalResultCount, firstResultIndex + results.size() - 1);
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
header = replace(header, "LASTRESULT", std::to_string(lastResultIndex));
header = replace(header, "RESULTCOUNT", std::to_string(totalResultCount));
header = replace(header, "PATTERN", getPattern());
return header;
return "%USERLANGMARKER%" + header;
}
std::string expectedHtmlResultsString() const
@@ -800,12 +790,18 @@ struct TestData
std::string expectedHtml() const
{
return makeSearchResultsHtml(
getPattern(),
expectedHtmlHeader(),
expectedHtmlResultsString(),
expectedHtmlFooter()
const std::string html = makeSearchResultsHtml(
getPattern(),
expectedHtmlHeader(),
expectedHtmlResultsString(),
expectedHtmlFooter()
);
const std::string userlangMarker = extractQueryValue("userlang") == "test"
? "[I18N TESTING] "
: "";
return replace(html, "%USERLANGMARKER%", userlangMarker);
}
std::string expectedXmlHeader() const
@@ -824,7 +820,8 @@ struct TestData
/>)";
const auto realResultsPerPage = resultsPerPage?resultsPerPage:25;
const auto url = makeUrl(query + "&format=xml", firstResultIndex, realResultsPerPage);
const auto cleanedUpQuery = replace(query, "&userlang=test", "");
const auto url = makeUrl(cleanedUpQuery + "&format=xml", firstResultIndex, realResultsPerPage);
header = replace(header, "URL", replace(url, "&", "&amp;"));
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
header = replace(header, "ITEMCOUNT", std::to_string(realResultsPerPage));
@@ -931,6 +928,17 @@ TEST(ServerSearchTest, searchResults)
/* pagination */ {}
},
{
/* query */ "pattern=velomanyunkan&books.id=" RAYCHARLESZIMID
"&userlang=test",
/* start */ -1,
/* resultsPerPage */ 0,
/* totalResultCount */ 0,
/* firstResultIndex */ 1,
/* results */ {},
/* pagination */ {}
},
{
/* query */ "pattern=razaf&books.id=" RAYCHARLESZIMID,
/* start */ -1,
@@ -1037,6 +1045,17 @@ TEST(ServerSearchTest, searchResults)
/* pagination */ {}
},
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID
"&userlang=test",
/* start */ -1,
/* resultsPerPage */ 100,
/* totalResultCount */ 44,
/* firstResultIndex */ 1,
/* results */ LARGE_SEARCH_RESULTS,
/* pagination */ {}
},
{
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
/* start */ -1,
@@ -1509,7 +1528,10 @@ std::string expectedConfusionOfTonguesErrorHtml(std::string url)
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Invalid request</title>
<script>
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : ")" + url + R"(" } } }, { "p" : { "msgid" : "confusion-of-tongues", "params" : { } } } ] };
</script>
</head>
<body>
<h1>Invalid request</h1>

View File

@@ -190,3 +190,5 @@ protected:
zfs1_.reset();
}
};
static const std::string ERROR_HTML_TEMPLATE_JS_STRING = R"("&lt;!DOCTYPE html&gt;\n&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;\n &lt;head&gt;\n &lt;meta content=&quot;text/html;charset=UTF-8&quot; http-equiv=&quot;content-type&quot; /&gt;\n &lt;title&gt;{{PAGE_TITLE}}&lt;/title&gt;\n{{#CSS_URL}}\n &lt;link type=&quot;text/css&quot; href=&quot;{{{CSS_URL}}}&quot; rel=&quot;Stylesheet&quot; /&gt;\n{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} &lt;script&gt;\n window.KIWIX_RESPONSE_TEMPLATE = &quot;{{KIWIX_RESPONSE_TEMPLATE}}&quot;;\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n &lt;/script&gt;{{/KIWIX_RESPONSE_DATA}}\n &lt;/head&gt;\n &lt;body&gt;\n &lt;h1&gt;{{PAGE_HEADING}}&lt;/h1&gt;\n{{#details}}\n &lt;p&gt;\n {{{p}}}\n &lt;/p&gt;\n{{/details}}\n &lt;/body&gt;\n&lt;/html&gt;\n")";