Compare commits

..

161 Commits

Author SHA1 Message Date
Veloman Yunkan
ebf0fe8b8f More predictable Downloader::startDownload()
Before this change `Downloader::startDownload()` might avoid starting a
new download when a download with the specified URI was already present
in its cache. This might be confusing for the following reasons:

1. uri is not the only parameter of `Downloader::startDownload()` - a
   target download directory may also be specified through the second
   `options` parameter. Thus calling `Downloader::startDownload()` twice
   with the same URI but different download directories would not save
   files into the second directory.

2. Files of a completed download may be removed, whereupon downloading the same
   files again won't be a no-op. However in such a situation
   `Downloader` refuses to actually repeat a previous download.
2024-02-13 18:42:15 +04: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
Kelson
24faf84163 Merge pull request #1023 from kiwix/suggestions_with_control_characters
Control characters are escaped in suggestions JSON
2023-11-17 15:12:13 +01:00
Veloman Yunkan
571c09e00a Control characters are escaped in suggestions JSON
According to the JSON spec, control characters from U+0000 through U+001F
must NOT appear in strings unescaped.
2023-11-17 14:55:01 +01:00
Kelson
a959800173 Merge pull request #1024 from kiwix/release-13.0.0
Release 13.0.0
2023-11-17 14:13:31 +01:00
Emmanuel Engelhart
b2196ee7a9 13.0.0 Changelog 2023-11-17 14:11:20 +01:00
Emmanuel Engelhart
aea51c21ff Bump-up version to 13.0.0 2023-11-17 13:52:03 +01:00
Kelson
95d627afa1 Merge pull request #1022 from kiwix/viewer_toolbar_tweaks
Viewer toolbar improvements
2023-11-15 21:41:55 +01:00
Veloman Yunkan
183bdcf2c0 Updated tests depending on kiwix-serve resources 2023-11-15 16:35:06 +04:00
Veloman Yunkan
e1cf16ddea Better behavior on narrow screens
On media (screens) narrower than 420 pixels, the toolbar buttons
are hidden. Before this change, when made visible they were laid out
in two rows. This change places them in a single row and provides
some vertical spacing from the search-box.
2023-11-15 16:08:41 +04:00
Veloman Yunkan
a74df86fcf Continuity in responsive layout of the toolbar
Without this change, in the media width range [416, 420) the searchbox is
narrow while the toolbars button space is empty which doesn't look nice.
2023-11-15 15:45:28 +04:00
Veloman Yunkan
605c7f71e0 Right-aligned UI language selector button 2023-11-15 15:31:22 +04:00
Veloman Yunkan
f58d4a93e1 Viewer toolbar controls are now of the same height 2023-11-15 13:00:12 +04:00
Kelson
00032adce2 Merge pull request #1017 from kiwix/macos_13
Switch to macos-13.
2023-11-11 19:33:53 +01:00
Matthieu Gautier
f5e6502e04 Switch to macos-13. 2023-11-09 17:53:33 +01:00
Kelson
37274f7882 Merge pull request #1016 from kiwix/fix_query_with_dot
Do not index book's name as a phrase.
2023-11-08 17:31:15 +01:00
Matthieu Gautier
07ff4eab43 Do not index book's name as a phrase.
Fix #1004
2023-11-08 10:29:31 +01:00
Kelson
e89f4e2ac7 Merge pull request #1008 from kiwix/autocomplete_no_min
Add a non minified version of autoComplete.js
2023-11-07 20:41:59 +01:00
Matthieu Gautier
bcbdce6a9a Add a small comment on autoComplete.css telling where it comes from. 2023-11-07 11:13:09 +01:00
Matthieu Gautier
0effcdb23f Add unminified autoComplete.js and LICENSE file.
- LICENSE is the copy of LICENSE file in TarekRaafat/autoComplete.js
- autoComplete.js is
  `https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.6/dist/autoComplete.js`
2023-11-07 11:07:56 +01:00
Matthieu Gautier
5c8dd0e8d3 Move autoComplete.min.js and autoComplete.css in a subdirectory.
This way we can easily identify which files is part of other project.
2023-11-07 11:04:27 +01:00
Matthieu Gautier
d2c031e047 Merge pull request #1013 from kiwix/add-fon-language-support 2023-11-07 08:29:25 +01:00
Emmanuel Engelhart
733b027c2f Add support of Fon language (no supported in libicu) 2023-11-04 15:34:42 +01:00
Kelson
e8b8c18297 Merge pull request #1009 from kiwix/kiwix_frontend_style_cleanup
Kiwix frontend style cleanup
2023-11-04 15:20:45 +01:00
Veloman Yunkan
29c33a7ad6 More economic use of vertical space on the library page 2023-10-28 21:20:33 +04:00
Veloman Yunkan
fd504c1166 Matched viewer toolbar color to that of the library page
Attempts to use the same color for buttons yielded poor results: viewer
toolbar buttons don't look nice on the dark background used for the
filter controls on the library page, whereas the light background of the
viewer toolbar buttons doesn't play well with the filters on the library
page which seem to be designed around the contrast effect.
2023-10-28 21:20:33 +04:00
Veloman Yunkan
0c05af658d Deduplicated styling of UI language selector
There was a slight difference (between index.css and taskbar.css) in the
margin values of the UI language selector button, however the values
taken from taskbar.css don't seem to have any visible impact on the
welcome/library page (controlled by index.css).
2023-10-28 21:20:33 +04:00
Veloman Yunkan
0c0b1f5971 Moved to kiwix.css some CSS with global effect
Moved from index.css into kiwix.css some CSS with global effect thus
making it apply to the viewer too.

Extra font-size directives in taskbar.css are needed to undo the effect
of 'font-size: 62.5%' now applied to the 'html' element type.
2023-10-28 21:20:33 +04:00
Veloman Yunkan
a65681d6f4 Shared styling of modal dialogs goes into kiwix.css 2023-10-28 21:20:33 +04:00
Veloman Yunkan
af27141320 Enter kiwix.css
The new file kiwix.css is intended to host the intersection of index.css
and taskbar.css. In this commit only font definitions have been moved
into it.
2023-10-28 21:20:33 +04:00
Veloman Yunkan
d2bb3d198c Moved font definition from template to CSS 2023-10-28 21:20:33 +04:00
Matthieu Gautier
a5db4a1fd5 Merge pull request #1006 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2023-10-24 15:33:21 +02:00
Matthieu Gautier
59f0070ecc Add new translations to resource files. 2023-10-24 15:13:07 +02:00
translatewiki.net
bd818d33af Localisation updates from https://translatewiki.net. 2023-10-23 13:10:04 +02:00
Kelson
16fbf15938 Merge pull request #1007 from computerscienceiscool/patch-1 2023-10-20 10:21:51 +02:00
JJ
8383265ac4 Update README.md
Fixed a couple of spelling errors
2023-10-20 00:58:52 -07:00
Kelson
0eb9a06736 Merge pull request #1003 from kiwix/nodiscard_aarch64
Do not use `[[nodiscard]]` attribute on compiler not supporting it.
2023-10-16 15:19:53 +02:00
Matthieu Gautier
01aa190c38 Do not use [[nodiscard]] attribute on compiler not supporting it.
On aarch64, we use gcc version 6.3.0 which doesn't support the
`[[nodiscard]]` attribute
(see https://en.cppreference.com/w/cpp/compiler_support/17)

So don't set the attribute if the attribute is not present.
2023-10-16 14:41:30 +02:00
Kelson
da891699ac Merge pull request #1005 from kiwix/language_selector_font_fix
Fixed the fonts in the viewer UI language selector
2023-10-16 11:34:38 +02:00
Veloman Yunkan
f9be9f98ce Fixed the fonts in the viewer UI language selector 2023-10-15 16:37:28 +04:00
Veloman Yunkan
22b55d36c6 Merge pull request #990 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2023-10-15 15:04:00 +04:00
Veloman Yunkan
2d86927e17 Registered new translations in the resource list 2023-10-15 14:47:32 +04:00
translatewiki.net
86be66a2d8 Localisation updates from https://translatewiki.net. 2023-10-12 13:09:48 +02:00
Matthieu Gautier
4425cd2122 Merge pull request #1001 from kiwix/magnet 2023-10-09 18:16:39 +02:00
renaud gaudin
ab0d7b6e80 updated index.js cacheid 2023-10-09 16:08:24 +00:00
renaud gaudin
cfc91b0967 Fixed #938: added hack for mirrobrain magnet URIs
Missing parameters are added to the magnet link for it to work properly
given it is mainly based on webseeds.
Each mirror serving the file is added as a webseed.
Params that requires URIEncoding are now encoded.

Introduces `makeURLSearchString()` to turn SearchParams into a string but only
URIEncoding some specific params.
2023-10-08 16:52:02 +02:00
Kelson
2650cdd7da Merge pull request #983 from kiwix/multiple-ci-cd-fixes
Multiple CI/CD fixes
2023-10-08 16:47:29 +02:00
Emmanuel Engelhart
efdb596561 Add Debian package building in CI 2023-10-08 16:43:10 +02:00
Emmanuel Engelhart
177e1d5da6 Use latest base image_variant v38 2023-10-08 16:43:10 +02:00
Emmanuel Engelhart
b861dfc9dd Use pinned version of Ubuntu for workflow 2023-10-08 16:43:10 +02:00
Emmanuel Engelhart
3fdbb5a990 Push on 'release' PPA triggered by 'release' event 2023-10-08 16:43:10 +02:00
Matthieu Gautier
e49abc1df1 Merge pull request #991 from kiwix/no_raw_pointer 2023-10-05 17:47:44 +02:00
Matthieu Gautier
9166b67c47 Do not allow SearchRendered to work on a delete nameMapper/Library.
By moving the nameMapper/library arguments in `getHtml`/`getXml` we avoid
any potential "use after free" of name mapper or library as they are not
stored.
2023-10-05 16:37:22 +02:00
Matthieu Gautier
1dc9705597 Introduce LibraryPtr and ConstLibraryPtr.
As we enforce the use of Library through a shared_ptr, let's simplify
user life (and code) with new "type".
2023-10-05 16:37:22 +02:00
Matthieu Gautier
5292f06fff Move back Library::Impl in Library.
As we now always use Library through a shared_ptr, we can make `Library`
not copiable and not movable.
So we can move back the data in `Library`, we don't care about move
constructor.
2023-10-05 16:37:22 +02:00
Matthieu Gautier
f8e7c3d476 Move the Library mutex in Library::Impl.
The why of this mutex is in `Library` is a bit complex.
It has been introduced in c2927ce when there was only `Library` and no
`std::unique_ptr<Impl>`.
As introducing the mutex imply implementing the move constructor, we have
split all data in `LibraryBase` (and keep a default move constructor here)
and add the mutex in `Library` (and implement a simple move constructor).

Later, in 090c2fd, we have move the `LibraryBase` to `Library::Impl`
(which should have been `Library::Data`).

So at the end, `Library::Impl` is never moved. We can move the `mutex` in
it and still simply implement move constructor for `Library`.
2023-10-05 16:37:22 +02:00
Matthieu Gautier
ead1474ead Make SearchRendered taking a const pointer. 2023-10-05 16:37:22 +02:00
Matthieu Gautier
1316dec37c Make the Server keep a shared_ptr instead of a raw NameMapper pointer.
Same as for `Library`, we want to be sure that the `NameMapper`
actually exists when the server is using it.
2023-10-05 16:37:22 +02:00
Matthieu Gautier
a5557eeb25 Make the Server keep a shared_ptr instead of a raw Library pointer.
We want to be sure that `Library` actually exists when we use it.
While it is not a silver bullet (user can still create a shared_ptr on
a raw pointer), making the `Server` keep `shared_ptr` on the library
help us a lot here.
2023-10-05 16:36:18 +02:00
Matthieu Gautier
efcbf6ef1e Make the UpdatableNameMapper keep a shared_ptr.
Same as `Manager`, we want to be sure that `Library` actually exists
when we use it.
2023-09-25 16:31:55 +02:00
Matthieu Gautier
139b561253 Make the Manager keep a shared_ptr instead of a raw Library reference.
We want to be sure that `Library` actually exists when we modify it.
While it is not a silver bullet (user can still create a shared_ptr on
a raw pointer), making the `Manager` keep `shared_ptr` on the library
help us a lot here.
2023-09-25 16:30:56 +02:00
Matthieu Gautier
c203e07ee9 Make the library creatable only within a shared_ptr. 2023-09-25 16:28:25 +02:00
Matthieu Gautier
49e99e7c22 Remove dumpers from the public API.
All those dumper were not used by any of our other projects.
They are only used internally, either by `Library::writeToFile` or the
server.
2023-09-19 16:46:58 +02:00
Kelson
e13324fbba Merge pull request #996 from kiwix/cpp17
Move to c++17.
2023-09-14 17:21:18 +02:00
Matthieu Gautier
c38ab3e5d7 Move to c++17.
All our compilers should handle c++17. Let's move on.
2023-09-14 17:21:02 +02:00
Matthieu Gautier
bde737f63b Merge pull request #997 from kiwix/cookieless_user_language_control 2023-09-11 14:04:48 +02:00
Veloman Yunkan
cc6aa9b162 Fixed userlang control on the library page too
This fix contains a small hack - in order to detect the default language
from browser language preference during the first visit, the library
page has to load /viewer_settings.js which contains that information.
2023-09-09 19:39:16 +04:00
Veloman Yunkan
9063450b5a Fixed userlang control in the viewer
Now the viewer stores the userlang preference in window.localStorage.
2023-09-09 19:39:16 +04:00
Veloman Yunkan
f8c3a1fd2e Added default user language to viewer_settings.js
The default user language determined from the value of "Accept-Language"
header is communicated to the client via the /viewer_settings.js
endpoint.
2023-09-09 19:37:49 +04:00
Veloman Yunkan
b5b98e7a61 RIP userlang cookie
This commit drops the usage of the userlang cookie in the backend but
not in the frontend. UI language control should be broken at this point
and will be fixed in the next few commits.
2023-09-09 19:37:49 +04:00
Veloman Yunkan
e7e8275a31 Made the language selector button visible
After upgrading my OS to Ubuntu 22.04 the language selector button
didn't show up in the viewer taskbar. Investigation shows that the id
used in the CSS was applied to the wrong HTML element (the enclosing
<a> rather than <img>).
2023-09-09 19:37:49 +04:00
Kelson
c6456cac42 Merge pull request #993 from kiwix/no_kinetic_package
Remove Ubuntu Kinetic from CI/CD (deprecated)
2023-08-24 17:15:22 +07:00
Emmanuel Engelhart
f0c0400485 Remove Ubuntu Kinetic from CI/CD (deprecated) 2023-08-24 11:15:19 +02:00
Matthieu Gautier
ccbeb154a5 Merge pull request #992 from kiwix/fix_rtd 2023-08-24 10:55:57 +02:00
Matthieu Gautier
0e8a2952d5 Always set html_theme in doc configuration.
Lat version of read the doc do not set a html_theme for us.
So we have to always set it.

See readthedocs/readthedocs.org#10638
2023-08-24 10:46:46 +02:00
Kelson
fe5e6c451d Merge pull request #977 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2023-08-18 10:37:54 +08:00
translatewiki.net
3966e8544b Localisation updates from https://translatewiki.net. 2023-08-17 13:10:24 +02:00
Matthieu Gautier
09476ededb Merge pull request #974 from kiwix/multipleCategories 2023-07-26 14:57:29 +02:00
Nikhil Tanwar
d47c4fa72f Unit tests for OPDS filtering by category
Added tests for multiple category filtering for zims
Added new test: catalog_v2_entries_filtered_by_category for entry filtering by category.
2023-07-26 18:15:47 +05:30
Nikhil Tanwar
c938101c70 Allow multiple category support
Created a generic function multipleQuery which takes:
1. string (representing a comma separated list)
2. param (the value to query on using Xapian).

Category and language query will use this function.
2023-07-26 18:15:45 +05:30
Matthieu Gautier
9c91fc7369 Merge pull request #967 from kiwix/opdsFilters 2023-07-26 14:00:06 +02:00
Nikhil Tanwar
385931f229 Move getLanguageSelfName to tools.h
This is a general utility which other ports can get use of.
Added tests
2023-07-26 16:02:32 +05:30
Nikhil Tanwar
8726de494c Tests for readLanguagesFromFeed and readCategoriesFromFeed
Added tests on a sample OPDS language and categories stream
2023-07-26 16:02:32 +05:30
Nikhil Tanwar
94d6bef402 Introduce readCategoriesFromFeed()
Added a function to load categories stored in an OPDS stream
2023-07-26 16:02:32 +05:30
Nikhil Tanwar
a28c2973e9 Introduce readLanguagesFromFeed()
Added a new function to read languages stored in an OPDS feed
2023-07-26 16:02:32 +05:30
Nikhil Tanwar
7feb89c30e Add remaining include files to meson.build
These files were overlooked during a merge of another PR
2023-07-26 16:02:32 +05:30
Matthieu Gautier
903dcd46d6 Merge pull request #978 from kiwix/fix_missing_includes 2023-07-25 16:32:26 +02:00
Matthieu Gautier
1be5424711 Add missing include.
`uint64_t` type (used by `beautifyFileSize`) need to be declared.
2023-07-25 13:37:31 +02:00
Matthieu Gautier
de517330f6 Merge pull request #971 from kiwix/beautifyPublic
Make beautifyFileSize public
2023-07-21 19:10:26 +02:00
Nikhil Tanwar
5c3a997de4 make beautifyFileSize public
This general function will be useful in other kiwix apps
2023-07-21 22:19:45 +05:30
96 changed files with 3562 additions and 1264 deletions

View File

@@ -8,7 +8,17 @@ on:
jobs:
macOS:
runs-on: macos-11
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:
@@ -22,22 +32,31 @@ jobs:
# 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
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
@@ -83,15 +102,14 @@ jobs:
HOME: /home/runner
runs-on: ubuntu-20.04
container:
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:37"
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:38"
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

@@ -5,15 +5,17 @@ on:
push:
branches:
- main
release:
types: [published]
jobs:
build-deb:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
distro:
- ubuntu-kinetic
- debian-unstable
- ubuntu-jammy
- ubuntu-focal
steps:
@@ -38,13 +40,12 @@ jobs:
email: release+launchpad@kiwix.org
distro: ${{ matrix.distro }}
- uses: legoktm/gh-action-build-deb@ubuntu-kinetic
if: matrix.distro == 'ubuntu-kinetic'
name: Build package for ubuntu-kinetic
id: build-ubuntu-kinetic
- uses: legoktm/gh-action-build-deb@debian-unstable
if: matrix.distro == 'debian-unstable'
name: Build package for debian-unstable
id: build-debian-unstable
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
if: matrix.distro == 'ubuntu-jammy'
@@ -69,7 +70,7 @@ jobs:
- uses: legoktm/gh-action-dput@master
name: Upload dev package
# Only upload on pushes to git default branch
# Only upload on pushes to main
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
with:
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
@@ -78,10 +79,8 @@ jobs:
- uses: legoktm/gh-action-dput@master
name: Upload release package
# Only upload on pushes to master or tag
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && startswith(matrix.distro, 'ubuntu-')
if: github.event_name == 'release' && startswith(matrix.distro, 'ubuntu-')
with:
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
repository: ppa:kiwixteam/release
packages: output/*_source.changes

View File

@@ -1,7 +1,22 @@
libkiwix 12.1.1
libkiwix 13.0.0
===============
* Revert API break introduced in libkiwix 12.1.0
* Server:
- Improved look & feel of kiwix-serve UI (@veloman-yunkan #917 #1021)
- Increase tolerance to malformed (control characters) ZIM entry titles (@veloman-yunkan #1023)
- API allowing to filter many categories at once (@juuz0 #974)
- Cookie-less user language control (@veloman-yumkan #997)
- Hack to fix Mirrorbrain based broken magnet URLs (@rgaudin #1001)
* Fix handling of books with 'Name' metadata with dots (@mgautier #1016)
* New method beautifyFileSize() to provide nice-looking book sizes (@vuuz0 #971)
* Fix a few missing includes (@mgautierfr #978)
* New functions to read - kiwix-serve - languages and categories streams (@juuz0 #967)
* Add support of Fon language (@kelson42 #1013)
* C++17 code base compliancy (@mgautierfr #996)
* Use everywhere std::shared_ptr in place of raw pointer (@mgautierfr #991)
* Do not use [[nodiscard]] attribute on compiler not supporting it (@mgautierfr #1003)
* Add a non minified version of autoComplete.js (@mgautierfr #1008)
* Multiple CI/CD improvements (@kelson42 #982)
libkiwix 12.1.0
===============

View File

@@ -24,9 +24,9 @@ with the Libkiwix compilation itself, we recommend to have a look to
Preamble
--------
Although the Libkiwix can be (cross-)compiled on/for many sytems, the
Although the Libkiwix can be (cross-)compiled on/for many systems, the
following documentation explains how to do it on POSIX ones. It is
primarly thought for GNU/Linux systems and has been tested on recent
primarily thought for GNU/Linux systems and has been tested on recent
releases of Ubuntu and Fedora.
Dependencies
@@ -54,7 +54,7 @@ The following dependency needs to be available at runtime:
These dependencies may or may not be packaged by your operating
system. They may also be packaged but only in an older version. The
compilation script will tell you if one of them is missing or too old.
In the worse case, you will have to download and compile bleeding edge
In the worst case, you will have to download and compile bleeding edge
version by hand.
If you want to install these dependencies locally, then use the
@@ -201,7 +201,7 @@ To use JS provided by kiwix-serve you can use the following template to start wi
If you compile manually Libmicrohttpd, you might need to compile it
without GNU TLS, a bug here will empeach further compilation
without GNU TLS, a bug here will impeach further compilation
otherwise.
If the compilation still fails, you might need to get a more recent

View File

@@ -24,8 +24,6 @@ author = 'libkiwix-team'
# -- General configuration ---------------------------------------------------
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
@@ -42,9 +40,7 @@ templates_path = ['_templates']
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
if not on_rtd:
html_theme = 'sphinx_rtd_theme'
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,

View File

@@ -1,2 +1,3 @@
breathe
exhale
sphinx_rtd_theme

View File

@@ -184,7 +184,7 @@ class Downloader
* @param options: A series of pair <option_name, option_value> to pass to aria.
* @return: The newly created Download.
*/
Download* startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
std::shared_ptr<Download> startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
/**
* Get a download corrsponding to a download id (did)
@@ -194,7 +194,7 @@ class Downloader
* @return: The Download corresponding to did.
* @throw: Throw std::out_of_range if did is not found.
*/
Download* getDownload(const std::string& did);
std::shared_ptr<Download> getDownload(const std::string& did);
/**
* Get the number of downloads currently managed.
@@ -204,11 +204,11 @@ class Downloader
/**
* Get the ids of the managed downloads.
*/
std::vector<std::string> getDownloadIds();
std::vector<std::string> getDownloadIds() const;
private:
mutable std::mutex m_lock;
std::map<std::string, std::unique_ptr<Download>> m_knownDownloads;
std::map<std::string, std::shared_ptr<Download>> m_knownDownloads;
std::shared_ptr<Aria2> mp_aria;
};
}

View File

@@ -34,6 +34,10 @@
#define KIWIX_LIBRARY_VERSION "20110515"
namespace Xapian {
class WritableDatabase;
};
namespace kiwix
{
@@ -105,6 +109,12 @@ class Filter {
Filter& acceptTags(const Tags& tags);
Filter& rejectTags(const Tags& tags);
/**
* Set the filter to only accept books in the specified category.
*
* Multiple categories can be specified as a comma-separated list (in
* which case a book in any of those categories will match).
*/
Filter& category(std::string category);
/**
@@ -167,31 +177,53 @@ class ZimSearcher : public zim::Searcher
std::mutex m_mutex;
};
template<typename, typename>
class ConcurrentCache;
template<typename, typename>
class MultiKeyCache;
using LibraryPtr = std::shared_ptr<Library>;
using ConstLibraryPtr = std::shared_ptr<const Library>;
// Some compiler we use don't have [[nodiscard]] attribute.
// We don't want to declare `create` with it in this case.
#define LIBKIWIX_NODISCARD
#if defined __has_cpp_attribute
#if __has_cpp_attribute (nodiscard)
#undef LIBKIWIX_NODISCARD
#define LIBKIWIX_NODISCARD [[nodiscard]]
#endif
#endif
/**
* A Library store several books.
*/
class Library
class Library: public std::enable_shared_from_this<Library>
{
// all data fields must be added in LibraryBase
mutable std::mutex m_mutex;
public:
typedef uint64_t Revision;
typedef std::vector<std::string> BookIdCollection;
typedef std::map<std::string, int> AttributeCounts;
typedef std::set<std::string> BookIdSet;
public:
private:
Library();
public:
LIBKIWIX_NODISCARD static LibraryPtr create() {
return LibraryPtr(new Library());
}
~Library();
/**
* Library is not a copiable object. However it can be moved.
*/
Library(const Library& ) = delete;
Library(Library&& );
Library(Library&& ) = delete;
void operator=(const Library& ) = delete;
Library& operator=(Library&& );
Library& operator=(Library&& ) = delete;
/**
* Add a book to the library.
@@ -362,19 +394,36 @@ class Library
private: // types
typedef const std::string& (Book::*BookStrPropMemFn)() const;
struct Impl;
struct Entry : Book
{
Library::Revision lastUpdatedRevision = 0;
};
private: // functions
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
BookIdCollection filterViaBookDB(const Filter& filter) 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
std::unique_ptr<Impl> mp_impl;
mutable std::mutex m_mutex;
Library::Revision m_revision;
std::map<std::string, Entry> m_books;
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
std::unique_ptr<ArchiveCache> mp_archiveCache;
using SearcherCache = MultiKeyCache<std::string, std::shared_ptr<ZimSearcher>>;
std::unique_ptr<SearcherCache> mp_searcherCache;
std::vector<kiwix::Bookmark> m_bookmarks;
std::unique_ptr<Xapian::WritableDatabase> m_bookDB;
};
// We don't need it anymore and we don't want to polute any other potential usage
// of `LIBKIWIX_NODISCARD` token.
#undef LIBKIWIX_NODISCARD
}
#endif

View File

@@ -37,10 +37,10 @@ namespace kiwix
class LibraryManipulator
{
public: // functions
explicit LibraryManipulator(Library* library);
explicit LibraryManipulator(LibraryPtr library);
virtual ~LibraryManipulator();
Library& getLibrary() const { return library; }
LibraryPtr getLibrary() const { return library; }
bool addBookToLibrary(const Book& book);
void addBookmarkToLibrary(const Bookmark& bookmark);
@@ -52,7 +52,7 @@ class LibraryManipulator
virtual void booksWereRemovedFromLibrary();
private: // data
kiwix::Library& library;
LibraryPtr library;
};
/**
@@ -64,8 +64,8 @@ class Manager
typedef std::vector<std::string> Paths;
public: // functions
explicit Manager(LibraryManipulator* manipulator);
explicit Manager(Library* library);
explicit Manager(LibraryManipulator manipulator);
explicit Manager(LibraryPtr library);
/**
* Read a `library.xml` and add book in the file to the library.
@@ -163,7 +163,7 @@ class Manager
uint64_t m_itemsPerPage = 0;
protected:
std::shared_ptr<kiwix::LibraryManipulator> manipulator;
kiwix::LibraryManipulator manipulator;
bool readBookFromPath(const std::string& path, Book* book);
bool parseXmlDom(const pugi::xml_document& doc,

View File

@@ -4,8 +4,6 @@ headers = [
'common.h',
'library.h',
'manager.h',
'libxml_dumper.h',
'opds_dumper.h',
'downloader.h',
'search_renderer.h',
'server.h',

View File

@@ -59,7 +59,7 @@ class HumanReadableNameMapper : public NameMapper {
class UpdatableNameMapper : public NameMapper {
typedef std::shared_ptr<NameMapper> NameMapperHandle;
public:
UpdatableNameMapper(Library& library, bool withAlias);
UpdatableNameMapper(std::shared_ptr<Library> library, bool withAlias);
virtual std::string getNameForId(const std::string& id) const;
virtual std::string getIdForName(const std::string& name) const;
@@ -71,7 +71,7 @@ class UpdatableNameMapper : public NameMapper {
private:
mutable std::mutex mutex;
Library& library;
std::shared_ptr<Library> library;
NameMapperHandle nameMapper;
const bool withAlias;
};

View File

@@ -37,29 +37,11 @@ class SearchRenderer
/**
* Construct a SearchRenderer from a SearchResultSet.
*
* The constructed version of the SearchRenderer will not introduce
* the book name for each result. It is better to use the other constructor
* with a Library pointer to have a better html page.
*
* @param srs The `SearchResultSet` to render.
* @param mapper The `NameMapper` to use to do the rendering.
* @param start The start offset used for the srs.
* @param estimatedResultCount The estimatedResultCount of the whole search
*/
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount);
/**
* Construct a SearchRenderer from a SearchResultSet.
*
* @param srs The `SearchResultSet` to render.
* @param mapper The `NameMapper` to use to do the rendering.
* @param library The `Library` to use to look up book details for search results.
* @param start The start offset used for the srs.
* @param estimatedResultCount The estimatedResultCount of the whole search
*/
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
unsigned int start, unsigned int estimatedResultCount);
SearchRenderer(zim::SearchResultSet srs, unsigned int start, unsigned int estimatedResultCount);
~SearchRenderer();
@@ -90,24 +72,39 @@ class SearchRenderer
this->pageLength = pageLength;
}
std::string renderTemplate(const std::string& tmpl_str);
/**
* set user language
*/
void setUserLang(const std::string& lang){
this->userlang = lang;
}
/**
* Generate the html page with the resutls of the search.
*
* @param mapper The `NameMapper` to use to do the rendering.
* @param library The `Library` to use to look up book details for search results.
May be nullptr. In this case, bookName is not set in the rendered string.
* @return The html string
*/
std::string getHtml();
std::string getHtml(const NameMapper& mapper, const Library* library);
/**
/**
* Generate the xml page with the resutls of the search.
*
* @param mapper The `NameMapper` to use to do the rendering.
* @param library The `Library` to use to look up book details for search results.
May be nullptr. In this case, bookName is not set in the rendered string.
* @return The xml string
*/
std::string getXml();
std::string getXml(const NameMapper& mapper, const Library* library);
protected: // function
std::string renderTemplate(const std::string& tmpl_str, const NameMapper& mapper, const Library *library);
protected:
std::string beautifyInteger(const unsigned int number);
zim::SearchResultSet m_srs;
NameMapper* mp_nameMapper;
Library* mp_library;
std::string searchBookQuery;
std::string searchPattern;
std::string protocolPrefix;
@@ -115,6 +112,7 @@ class SearchRenderer
unsigned int pageLength;
unsigned int estimatedResultCount;
unsigned int resultStart;
std::string userlang = "en";
};

View File

@@ -36,7 +36,7 @@ namespace kiwix
*
* @param library The library to serve.
*/
Server(Library* library, NameMapper* nameMapper=nullptr);
Server(std::shared_ptr<Library> library, std::shared_ptr<NameMapper> nameMapper=nullptr);
virtual ~Server();
@@ -66,8 +66,8 @@ namespace kiwix
std::string getAddress();
protected:
Library* mp_library;
NameMapper* mp_nameMapper;
std::shared_ptr<Library> mp_library;
std::shared_ptr<NameMapper> mp_nameMapper;
std::string m_root = "";
std::string m_addr = "";
std::string m_indexTemplateString = "";

View File

@@ -23,8 +23,12 @@
#include <string>
#include <vector>
#include <map>
#include <cstdint>
namespace kiwix {
typedef std::pair<std::string, std::string> LangNameCodePair;
typedef std::vector<LangNameCodePair> FeedLanguages;
typedef std::vector<std::string> FeedCategories;
/**
* Return the current directory.
@@ -216,5 +220,37 @@ std::map<std::string, std::string> getNetworkInterfaces();
*/
std::string getBestPublicIp();
/** Converts file size to human readable format.
*
* This function will convert a number to its equivalent size using units.
*
* @param number file size in bytes.
* @return a human-readable string representation of the size, e.g., "2.3 KB", "1.8 MB", "5.2 GB".
*/
std::string beautifyFileSize(uint64_t number);
/**
* Load languages stored in an OPDS stream.
*
* @param content the OPDS stream.
* @return vector containing pairs of language code and their corresponding full language name.
*/
FeedLanguages readLanguagesFromFeed(const std::string& content);
/**
* Load categories stored in an OPDS stream .
*
* @param content the OPDS stream.
* @return vector containing category strings.
*/
FeedCategories readCategoriesFromFeed(const std::string& content);
/**
* Retrieve the full language name associated with a given ISO 639-3 language code.
*
* @param lang ISO 639-3 language code.
* @return full language name.
*/
std::string getLanguageSelfName(const std::string& lang);
}
#endif // KIWIX_TOOLS_H

View File

@@ -1,7 +1,7 @@
project('libkiwix', 'cpp',
version : '12.1.1',
version : '13.0.0',
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])
compiler = meson.get_compiler('cpp')

View File

@@ -157,7 +157,7 @@ void Downloader::close()
mp_aria->close();
}
std::vector<std::string> Downloader::getDownloadIds() {
std::vector<std::string> Downloader::getDownloadIds() const {
std::unique_lock<std::mutex> lock(m_lock);
std::vector<std::string> ret;
for(auto& p:m_knownDownloads) {
@@ -166,37 +166,31 @@ std::vector<std::string> Downloader::getDownloadIds() {
return ret;
}
Download* Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
{
std::unique_lock<std::mutex> lock(m_lock);
for (auto& p: m_knownDownloads) {
auto& d = p.second;
auto& uris = d->getUris();
if (std::find(uris.begin(), uris.end(), uri) != uris.end())
return d.get();
}
std::vector<std::string> uris = {uri};
auto gid = mp_aria->addUri(uris, options);
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
return m_knownDownloads[gid].get();
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
return m_knownDownloads[gid];
}
Download* Downloader::getDownload(const std::string& did)
std::shared_ptr<Download> Downloader::getDownload(const std::string& did)
{
std::unique_lock<std::mutex> lock(m_lock);
try {
return m_knownDownloads.at(did).get();
return m_knownDownloads.at(did);
} catch(std::exception& e) {
for (auto gid : mp_aria->tellWaiting()) {
if (gid == did) {
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
return m_knownDownloads[gid].get();
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
return m_knownDownloads[gid];
}
}
for (auto gid : mp_aria->tellActive()) {
if (gid == did) {
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
return m_knownDownloads[gid].get();
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
return m_knownDownloads[gid];
}
}
throw e;

View File

View File

@@ -39,6 +39,8 @@
namespace kiwix
{
namespace
{
@@ -58,6 +60,8 @@ bool booksReferToTheSameArchive(const Book& book1, const Book& book2)
&& book1.getPath() == book2.getPath();
}
} // unnamed namespace
template<typename Key, typename Value>
class MultiKeyCache: public ConcurrentCache<std::set<Key>, Value>
{
@@ -79,49 +83,8 @@ class MultiKeyCache: public ConcurrentCache<std::set<Key>, Value>
}
};
} // unnamed namespace
struct Library::Impl
{
struct Entry : Book
{
Library::Revision lastUpdatedRevision = 0;
};
Library::Revision m_revision;
std::map<std::string, Entry> m_books;
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
std::unique_ptr<ArchiveCache> mp_archiveCache;
using SearcherCache = MultiKeyCache<std::string, std::shared_ptr<ZimSearcher>>;
std::unique_ptr<SearcherCache> mp_searcherCache;
std::vector<kiwix::Bookmark> m_bookmarks;
Xapian::WritableDatabase m_bookDB;
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;
Impl();
~Impl();
Impl(Impl&& );
Impl& operator=(Impl&& );
};
Library::Impl::Impl()
: mp_archiveCache(new ArchiveCache(std::max(getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", 1), 1))),
mp_searcherCache(new SearcherCache(std::max(getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", 1), 1))),
m_bookDB("", Xapian::DB_BACKEND_INMEMORY)
{
}
Library::Impl::~Impl()
{
}
Library::Impl::Impl(Library::Impl&& ) = default;
Library::Impl& Library::Impl::operator=(Library::Impl&& ) = default;
unsigned int
Library::Impl::getBookCount(const bool localBooks, const bool remoteBooks) const
Library::getBookCount_not_protected(const bool localBooks, const bool remoteBooks) const
{
unsigned int result = 0;
for (auto& pair: m_books) {
@@ -136,50 +99,41 @@ Library::Impl::getBookCount(const bool localBooks, const bool remoteBooks) const
/* Constructor */
Library::Library()
: mp_impl(new Library::Impl)
: mp_archiveCache(new ArchiveCache(std::max(getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", 1), 1))),
mp_searcherCache(new SearcherCache(std::max(getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", 1), 1))),
m_bookDB(new Xapian::WritableDatabase("", Xapian::DB_BACKEND_INMEMORY))
{
}
Library::Library(Library&& other)
: mp_impl(std::move(other.mp_impl))
{
}
Library& Library::operator=(Library&& other)
{
mp_impl = std::move(other.mp_impl);
return *this;
}
/* Destructor */
Library::~Library() = default;
bool Library::addBook(const Book& book)
{
std::lock_guard<std::mutex> lock(m_mutex);
++mp_impl->m_revision;
++m_revision;
/* Try to find it */
updateBookDB(book);
try {
auto& oldbook = mp_impl->m_books.at(book.getId());
auto& oldbook = m_books.at(book.getId());
if ( ! booksReferToTheSameArchive(oldbook, book) ) {
dropCache(book.getId());
}
oldbook.update(book); // XXX: This may have no effect if oldbook is readonly
// XXX: Then m_bookDB will become out-of-sync with
// XXX: the real contents of the library.
oldbook.lastUpdatedRevision = mp_impl->m_revision;
oldbook.lastUpdatedRevision = m_revision;
return false;
} catch (std::out_of_range&) {
auto& newEntry = mp_impl->m_books[book.getId()];
auto& newEntry = m_books[book.getId()];
static_cast<Book&>(newEntry) = book;
newEntry.lastUpdatedRevision = mp_impl->m_revision;
size_t new_cache_size = static_cast<size_t>(std::ceil(mp_impl->getBookCount(true, true)*0.1));
newEntry.lastUpdatedRevision = m_revision;
size_t new_cache_size = static_cast<size_t>(std::ceil(getBookCount_not_protected(true, true)*0.1));
if (getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", -1) <= 0) {
mp_impl->mp_archiveCache->setMaxSize(new_cache_size);
mp_archiveCache->setMaxSize(new_cache_size);
}
if (getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", -1) <= 0) {
mp_impl->mp_searcherCache->setMaxSize(new_cache_size);
mp_searcherCache->setMaxSize(new_cache_size);
}
return true;
}
@@ -188,15 +142,15 @@ bool Library::addBook(const Book& book)
void Library::addBookmark(const Bookmark& bookmark)
{
std::lock_guard<std::mutex> lock(m_mutex);
mp_impl->m_bookmarks.push_back(bookmark);
m_bookmarks.push_back(bookmark);
}
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
{
std::lock_guard<std::mutex> lock(m_mutex);
for(auto it=mp_impl->m_bookmarks.begin(); it!=mp_impl->m_bookmarks.end(); it++) {
for(auto it=m_bookmarks.begin(); it!=m_bookmarks.end(); it++) {
if (it->getBookId() == zimId && it->getUrl() == url) {
mp_impl->m_bookmarks.erase(it);
m_bookmarks.erase(it);
return true;
}
}
@@ -206,14 +160,14 @@ bool Library::removeBookmark(const std::string& zimId, const std::string& url)
void Library::dropCache(const std::string& id)
{
mp_impl->mp_archiveCache->drop(id);
mp_impl->mp_searcherCache->drop(id);
mp_archiveCache->drop(id);
mp_searcherCache->drop(id);
}
bool Library::removeBookById(const std::string& id)
{
std::lock_guard<std::mutex> lock(m_mutex);
mp_impl->m_bookDB.delete_document("Q" + id);
m_bookDB->delete_document("Q" + id);
dropCache(id);
// We do not change the cache size here
// Most of the time, the book is remove in case of library refresh, it is
@@ -221,9 +175,9 @@ bool Library::removeBookById(const std::string& id)
// Having a too big cache is not a problem here (or it would have been before)
// (And setMaxSize doesn't actually reduce the cache size, extra cached items
// will be removed in put or getOrPut).
const bool bookWasRemoved = mp_impl->m_books.erase(id) == 1;
const bool bookWasRemoved = m_books.erase(id) == 1;
if ( bookWasRemoved ) {
++mp_impl->m_revision;
++m_revision;
}
return bookWasRemoved;
}
@@ -231,7 +185,7 @@ bool Library::removeBookById(const std::string& id)
Library::Revision Library::getRevision() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return mp_impl->m_revision;
return m_revision;
}
uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
@@ -239,7 +193,7 @@ uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
BookIdCollection booksToRemove;
{
std::lock_guard<std::mutex> lock(m_mutex);
for ( const auto& entry : mp_impl->m_books) {
for ( const auto& entry : m_books) {
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
booksToRemove.push_back(entry.first);
}
@@ -258,7 +212,7 @@ const Book& Library::getBookById(const std::string& id) const
{
// XXX: Doesn't make sense to lock this operation since it cannot
// XXX: guarantee thread-safety because of its return type
return mp_impl->m_books.at(id);
return m_books.at(id);
}
Book Library::getBookByIdThreadSafe(const std::string& id) const
@@ -271,7 +225,7 @@ const Book& Library::getBookByPath(const std::string& path) const
{
// XXX: Doesn't make sense to lock this operation since it cannot
// XXX: guarantee thread-safety because of its return type
for(auto& it: mp_impl->m_books) {
for(auto& it: m_books) {
auto& book = it.second;
if (book.getPath() == path)
return book;
@@ -284,7 +238,7 @@ const Book& Library::getBookByPath(const std::string& path) const
std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
{
try {
return mp_impl->mp_archiveCache->getOrPut(id,
return mp_archiveCache->getOrPut(id,
[&](){
auto book = getBookById(id);
if (!book.isPathValid()) {
@@ -301,7 +255,7 @@ std::shared_ptr<ZimSearcher> Library::getSearcherByIds(const BookIdSet& ids)
{
assert(!ids.empty());
try {
return mp_impl->mp_searcherCache->getOrPut(ids,
return mp_searcherCache->getOrPut(ids,
[&](){
std::vector<zim::Archive> archives;
for(auto& id:ids) {
@@ -322,7 +276,7 @@ unsigned int Library::getBookCount(const bool localBooks,
const bool remoteBooks) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return mp_impl->getBookCount(localBooks, remoteBooks);
return getBookCount_not_protected(localBooks, remoteBooks);
}
bool Library::writeToFile(const std::string& path) const
@@ -353,7 +307,7 @@ Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) con
std::lock_guard<std::mutex> lock(m_mutex);
AttributeCounts propValueCounts;
for (const auto& pair: mp_impl->m_books) {
for (const auto& pair: m_books) {
const auto& book = pair.second;
if (book.getOrigId().empty()) {
propValueCounts[(book.*p)()] += 1;
@@ -385,7 +339,7 @@ Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
std::lock_guard<std::mutex> lock(m_mutex);
AttributeCounts langsWithCounts;
for (const auto& pair: mp_impl->m_books) {
for (const auto& pair: m_books) {
const auto& book = pair.second;
if (book.getOrigId().empty()) {
for ( const auto& lang : book.getLanguages() ) {
@@ -401,7 +355,7 @@ std::vector<std::string> Library::getBooksCategories() const
std::lock_guard<std::mutex> lock(m_mutex);
std::set<std::string> categories;
for (const auto& pair: mp_impl->m_books) {
for (const auto& pair: m_books) {
const auto& book = pair.second;
const auto& c = book.getCategory();
if ( !c.empty() ) {
@@ -425,12 +379,12 @@ std::vector<std::string> Library::getBooksPublishers() const
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const
{
if (!onlyValidBookmarks) {
return mp_impl->m_bookmarks;
return m_bookmarks;
}
std::vector<kiwix::Bookmark> validBookmarks;
auto booksId = getBooksIds();
std::lock_guard<std::mutex> lock(m_mutex);
for(auto& bookmark:mp_impl->m_bookmarks) {
for(auto& bookmark:m_bookmarks) {
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
validBookmarks.push_back(bookmark);
}
@@ -443,7 +397,7 @@ Library::BookIdCollection Library::getBooksIds() const
std::lock_guard<std::mutex> lock(m_mutex);
BookIdCollection bookIds;
for (auto& pair: mp_impl->m_books) {
for (auto& pair: m_books) {
bookIds.push_back(pair.first);
}
@@ -482,7 +436,7 @@ void Library::updateBookDB(const Book& book)
}
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
indexer.index_text(normalizeText(book.getName()), 1, "XN");
doc.add_term("XN"+normalizeText(book.getName()));
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
@@ -498,7 +452,7 @@ void Library::updateBookDB(const Book& book)
doc.set_data(book.getId());
mp_impl->m_bookDB.replace_document(idterm, doc);
m_bookDB->replace_document(idterm, doc);
}
namespace
@@ -549,25 +503,30 @@ Xapian::Query nameQuery(const std::string& name)
return Xapian::Query("XN" + normalizeText(name));
}
Xapian::Query categoryQuery(const std::string& category)
Xapian::Query multipleParamQuery(const std::string& commaSeparatedList, const std::string& prefix)
{
return Xapian::Query("XC" + normalizeText(category));
Xapian::Query q;
bool firstIteration = true;
for ( const auto& elem : kiwix::split(commaSeparatedList, ",") ) {
const Xapian::Query singleQuery(prefix + normalizeText(elem));
if ( firstIteration ) {
q = singleQuery;
firstIteration = false;
} else {
q = Xapian::Query(Xapian::Query::OP_OR, q, singleQuery);
}
}
return q;
}
Xapian::Query categoryQuery(const std::string& commaSeparatedCategoryList)
{
return multipleParamQuery(commaSeparatedCategoryList, "XC");
}
Xapian::Query langQuery(const std::string& commaSeparatedLanguageList)
{
Xapian::Query q;
bool firstIteration = true;
for ( const auto& lang : kiwix::split(commaSeparatedLanguageList, ",") ) {
const Xapian::Query singleLangQuery("L" + normalizeText(lang));
if ( firstIteration ) {
q = singleLangQuery;
firstIteration = false;
} else {
q = Xapian::Query(Xapian::Query::OP_OR, q, singleLangQuery);
}
}
return q;
return multipleParamQuery(commaSeparatedLanguageList, "L");
}
Xapian::Query publisherQuery(const std::string& publisher)
@@ -642,9 +601,9 @@ Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
BookIdCollection bookIds;
std::lock_guard<std::mutex> lock(m_mutex);
Xapian::Enquire enquire(mp_impl->m_bookDB);
Xapian::Enquire enquire(*m_bookDB);
enquire.set_query(query);
const auto results = enquire.get_mset(0, mp_impl->m_books.size());
const auto results = enquire.get_mset(0, m_books.size());
for ( auto it = results.begin(); it != results.end(); ++it ) {
bookIds.push_back(it.get_document().get_data());
}
@@ -658,7 +617,7 @@ Library::BookIdCollection Library::filter(const Filter& filter) const
const auto preliminaryResult = filterViaBookDB(filter);
std::lock_guard<std::mutex> lock(m_mutex);
for(auto id : preliminaryResult) {
if(filter.accept(mp_impl->m_books.at(id))) {
if(filter.accept(m_books.at(id))) {
result.push_back(id);
}
}

View File

@@ -23,65 +23,6 @@ void LibraryDumper::setOpenSearchInfo(int totalResults, int startIndex, int coun
m_count = count;
}
namespace {
std::map<std::string, std::string> iso639_3 = {
{"atj", "atikamekw"},
{"azb", "آذربایجان دیلی"},
{"bcl", "central bikol"},
{"bgs", "tagabawa"},
{"bxr", "буряад хэлэн"},
{"cbk", "chavacano"},
{"cdo", "閩東語"},
{"dag", "Dagbani"},
{"diq", "dimli"},
{"dty", "डोटेली"},
{"eml", "emiliân-rumagnōl"},
{"fbs", "српскохрватски"},
{"guw", "Gungbe"},
{"hbs", "srpskohrvatski"},
{"ido", "ido"},
{"kbp", "kabɩ"},
{"kld", "Gamilaraay"},
{"lbe", "лакку маз"},
{"lbj", "ལ་དྭགས་སྐད་"},
{"map", "Austronesian"},
{"mhr", "марий йылме"},
{"mnw", "ဘာသာမန်"},
{"myn", "mayan"},
{"nah", "nahuatl"},
{"nai", "north American Indian"},
{"nds", "plattdütsch"},
{"nrm", "bhasa narom"},
{"olo", "livvi"},
{"pih", "Pitcairn-Norfolk"},
{"pnb", "Western Panjabi"},
{"rmr", "Caló"},
{"rmy", "romani shib"},
{"roa", "romance languages"},
{"twi", "twi"},
};
std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
const ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
}
std::string getLanguageSelfName(const std::string& lang) {
const auto itr = iso639_3.find(lang);
if (itr != iso639_3.end()) {
return itr->second;
}
return lang;
};
} // unnamed namespace
kainjow::mustache::list LibraryDumper::getCategoryData() const
{
const auto now = gen_date_str();
@@ -102,7 +43,6 @@ kainjow::mustache::list LibraryDumper::getLanguageData() const
{
const auto now = gen_date_str();
kainjow::mustache::list languageData;
std::call_once(fillLanguagesFlag, fillLanguagesMap);
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
const std::string languageCode = langAndBookCount.first;
const int bookCount = langAndBookCount.second;

View File

@@ -27,22 +27,12 @@
namespace kiwix
{
namespace
{
struct NoDelete
{
template<class T> void operator()(T*) {}
};
} // unnamed namespace
////////////////////////////////////////////////////////////////////////////////
// LibraryManipulator
////////////////////////////////////////////////////////////////////////////////
LibraryManipulator::LibraryManipulator(Library* library)
: library(*library)
LibraryManipulator::LibraryManipulator(LibraryPtr library)
: library(library)
{}
LibraryManipulator::~LibraryManipulator()
@@ -50,7 +40,7 @@ LibraryManipulator::~LibraryManipulator()
bool LibraryManipulator::addBookToLibrary(const Book& book)
{
const auto ret = library.addBook(book);
const auto ret = library->addBook(book);
if ( ret ) {
bookWasAddedToLibrary(book);
}
@@ -59,13 +49,13 @@ bool LibraryManipulator::addBookToLibrary(const Book& book)
void LibraryManipulator::addBookmarkToLibrary(const Bookmark& bookmark)
{
library.addBookmark(bookmark);
library->addBookmark(bookmark);
bookmarkWasAddedToLibrary(bookmark);
}
uint32_t LibraryManipulator::removeBooksNotUpdatedSince(Library::Revision rev)
{
const auto n = library.removeBooksNotUpdatedSince(rev);
const auto n = library->removeBooksNotUpdatedSince(rev);
if ( n != 0 ) {
booksWereRemovedFromLibrary();
}
@@ -89,15 +79,15 @@ void LibraryManipulator::booksWereRemovedFromLibrary()
////////////////////////////////////////////////////////////////////////////////
/* Constructor */
Manager::Manager(LibraryManipulator* manipulator):
Manager::Manager(LibraryManipulator manipulator):
writableLibraryPath(""),
manipulator(manipulator, NoDelete())
manipulator(manipulator)
{
}
Manager::Manager(Library* library) :
Manager::Manager(LibraryPtr library) :
writableLibraryPath(""),
manipulator(new LibraryManipulator(library))
manipulator(LibraryManipulator(library))
{
}
@@ -121,7 +111,7 @@ bool Manager::parseXmlDom(const pugi::xml_document& doc,
if (!trustLibrary && !book.getPath().empty()) {
this->readBookFromPath(book.getPath(), &book);
}
manipulator->addBookToLibrary(book);
manipulator.addBookToLibrary(book);
}
return true;
@@ -166,7 +156,7 @@ bool Manager::parseOpdsDom(const pugi::xml_document& doc, const std::string& url
book.updateFromOpds(entryNode, urlHost);
/* Update the book properties with the new importer */
manipulator->addBookToLibrary(book);
manipulator.addBookToLibrary(book);
}
return true;
@@ -241,7 +231,7 @@ std::string Manager::addBookFromPathAndGetId(const std::string& pathToOpen,
|| (!book.getTitle().empty() && !book.getLanguages().empty()
&& !book.getDate().empty())) {
book.setUrl(url);
manipulator->addBookToLibrary(book);
manipulator.addBookToLibrary(book);
return book.getId();
}
}
@@ -296,7 +286,7 @@ bool Manager::readBookmarkFile(const std::string& path)
bookmark.updateFromXml(node);
manipulator->addBookmarkToLibrary(bookmark);
manipulator.addBookmarkToLibrary(bookmark);
}
return true;
@@ -304,7 +294,7 @@ bool Manager::readBookmarkFile(const std::string& path)
void Manager::reload(const Paths& paths)
{
const auto libRevision = manipulator->getLibrary().getRevision();
const auto libRevision = manipulator.getLibrary()->getRevision();
for (std::string path : paths) {
if (!path.empty()) {
if ( kiwix::isRelativePath(path) )
@@ -316,7 +306,7 @@ void Manager::reload(const Paths& paths)
}
}
manipulator->removeBooksNotUpdatedSince(libRevision);
manipulator.removeBooksNotUpdatedSince(libRevision);
}
}

View File

@@ -17,6 +17,8 @@ kiwix_sources = [
'tools/regexTools.cpp',
'tools/stringTools.cpp',
'tools/networkTools.cpp',
'tools/opdsParsingTools.cpp',
'tools/languageTools.cpp',
'tools/otherTools.cpp',
'tools/archiveTools.cpp',
'kiwixserve.cpp',

View File

@@ -63,7 +63,7 @@ std::string HumanReadableNameMapper::getIdForName(const std::string& name) const
// UpdatableNameMapper
////////////////////////////////////////////////////////////////////////////////
UpdatableNameMapper::UpdatableNameMapper(Library& lib, bool withAlias)
UpdatableNameMapper::UpdatableNameMapper(LibraryPtr lib, bool withAlias)
: library(lib)
, withAlias(withAlias)
{
@@ -72,7 +72,7 @@ UpdatableNameMapper::UpdatableNameMapper(Library& lib, bool withAlias)
void UpdatableNameMapper::update()
{
const auto newNameMapper = new HumanReadableNameMapper(library, withAlias);
const auto newNameMapper = new HumanReadableNameMapper(*library, withAlias);
std::lock_guard<std::mutex> lock(mutex);
nameMapper.reset(newNameMapper);
}

View File

View File

@@ -32,20 +32,46 @@
#include "libkiwix-resources.h"
#include "tools/stringTools.h"
#include "server/i18n.h"
namespace kiwix
{
/* Constructor */
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount)
: SearchRenderer(srs, mapper, nullptr, start, estimatedResultCount)
{}
namespace
{
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
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)
: m_srs(srs),
mp_nameMapper(mapper),
mp_library(library),
protocolPrefix("zim://"),
searchProtocolPrefix("search://"),
estimatedResultCount(estimatedResultCount),
@@ -164,7 +190,7 @@ kainjow::mustache::data buildPagination(
return pagination;
}
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const NameMapper& nameMapper, const Library* library)
{
const std::string absPathPrefix = protocolPrefix;
// Build the results list
@@ -172,15 +198,25 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
kainjow::mustache::data result;
const std::string zim_id(it.getZimId());
const auto path = mp_nameMapper->getNameForId(zim_id) + "/" + it.getPath();
const auto path = nameMapper.getNameForId(zim_id) + "/" + it.getPath();
result.set("title", it.getTitle());
result.set("absolutePath", absPathPrefix + urlEncode(path));
result.set("snippet", it.getSnippet());
if (mp_library) {
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
if (library) {
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);
@@ -188,7 +224,6 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
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)));
@@ -205,12 +240,15 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
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);
@@ -222,14 +260,14 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
return ss.str();
}
std::string SearchRenderer::getHtml()
std::string SearchRenderer::getHtml(const NameMapper& mapper, const Library* library)
{
return renderTemplate(RESOURCE::templates::search_result_html);
return renderTemplate(RESOURCE::templates::search_result_html, mapper, library);
}
std::string SearchRenderer::getXml()
std::string SearchRenderer::getXml(const NameMapper& mapper, const Library* library)
{
return renderTemplate(RESOURCE::templates::search_result_xml);
return renderTemplate(RESOURCE::templates::search_result_xml, mapper, library);
}

View File

@@ -29,7 +29,7 @@
namespace kiwix {
Server::Server(Library* library, NameMapper* nameMapper) :
Server::Server(LibraryPtr library, std::shared_ptr<NameMapper> nameMapper) :
mp_library(library),
mp_nameMapper(nameMapper),
mp_server(nullptr)

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"),
@@ -254,6 +248,11 @@ get_matching_if_none_match_etag(const RequestContext& r, const std::string& etag
}
}
struct NoDelete
{
template<class T> void operator()(T*) {}
};
} // unnamed namespace
std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const RequestContext& request) const
@@ -406,8 +405,8 @@ public:
};
InternalServer::InternalServer(Library* library,
NameMapper* nameMapper,
InternalServer::InternalServer(LibraryPtr library,
std::shared_ptr<NameMapper> nameMapper,
std::string addr,
int port,
std::string root,
@@ -433,7 +432,7 @@ InternalServer::InternalServer(Library* library,
m_ipConnectionLimit(ipConnectionLimit),
mp_daemon(nullptr),
mp_library(library),
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper),
mp_nameMapper(nameMapper ? nameMapper : std::shared_ptr<NameMapper>(&defaultNameMapper, NoDelete())),
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
m_customizedResources(new CustomizedResources)
@@ -514,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,
@@ -530,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();
@@ -559,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()) {
@@ -588,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) )
@@ -642,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");
}
}
@@ -663,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");
}
/**
@@ -692,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;
@@ -707,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);
}
@@ -742,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)
@@ -756,7 +769,7 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
{"enable_link_blocking", m_blockExternalLinks ? "true" : "false" },
{"enable_library_button", m_withLibraryButton ? "true" : "false" }
};
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
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
@@ -786,7 +799,7 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
{
const auto url = request.get_url();
const auto urlParts = kiwix::split(url, "/", true, false);
HTMLDumper htmlDumper(mp_library, mp_nameMapper);
HTMLDumper htmlDumper(mp_library.get(), mp_nameMapper.get());
htmlDumper.setRootLocation(m_root);
htmlDumper.setLibraryId(getLibraryId());
auto userLang = request.get_user_language();
@@ -811,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
@@ -861,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);
}
}
@@ -881,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();
}
}
@@ -936,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.
@@ -957,17 +960,24 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
const auto pageLength = getSearchPageSize(request);
/* Get the results */
SearchRenderer renderer(search->getResults(start-1, pageLength), mp_nameMapper, mp_library, start,
SearchRenderer renderer(search->getResults(start-1, pageLength), start,
search->getEstimatedMatches());
renderer.setSearchPattern(searchInfo.pattern);
renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
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(), "application/rss+xml; charset=utf-8");
return ContentResponse::build(
renderer.getXml(*mp_nameMapper, mp_library.get()),
"application/rss+xml; charset=utf-8"
);
}
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
auto response = ContentResponse::build(
renderer.getHtml(*mp_nameMapper, mp_library.get()),
"text/html; charset=utf-8"
);
// XXX: Now this has to be handled by the iframe-based viewer which
// XXX: has to resolve if the book selection resulted in a single book.
/*
@@ -987,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;
@@ -1002,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);
}
@@ -1010,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");
}
}
@@ -1023,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)
@@ -1042,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>
@@ -1103,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)
@@ -1127,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] == '/') {
@@ -1154,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") ) {
@@ -1175,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));
}
}
@@ -1194,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);
}
@@ -1211,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
@@ -1230,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 {
@@ -1238,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;
}
@@ -1246,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);
}
}
@@ -1272,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

@@ -92,8 +92,8 @@ class OPDSDumper;
class InternalServer {
public:
InternalServer(Library* library,
NameMapper* nameMapper,
InternalServer(LibraryPtr library,
std::shared_ptr<NameMapper> nameMapper,
std::string addr,
int port,
std::string root,
@@ -178,8 +178,8 @@ class InternalServer {
int m_ipConnectionLimit;
struct MHD_Daemon* mp_daemon;
Library* mp_library;
NameMapper* mp_nameMapper;
LibraryPtr mp_library;
std::shared_ptr<NameMapper> mp_nameMapper;
SearchCache searchCache;
SuggestionSearcherCache suggestionSearcherCache;
@@ -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,17 +71,16 @@ 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);
}
zim::Uuid uuid;
kiwix::OPDSDumper opdsDumper(mp_library, mp_nameMapper);
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
std::vector<std::string> bookIdsToDump;
@@ -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()},
@@ -164,13 +158,12 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
{
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
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,16 +174,14 @@ 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, mp_nameMapper);
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
return ContentResponse::build(
*this,
opdsFeed,
opdsMimeType[OPDS_ENTRY]
);
@@ -198,11 +189,10 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
{
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(getLibraryId());
return ContentResponse::build(
*this,
opdsDumper.categoriesOPDSFeed(),
opdsMimeType[OPDS_NAVIGATION_FEED]
);
@@ -210,11 +200,10 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
{
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
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 {
@@ -202,21 +194,12 @@ std::string RequestContext::get_user_language() const
return userlang.lang;
}
bool RequestContext::user_language_comes_from_cookie() const
{
return userlang.selectedBy == UserLanguage::SelectorKind::COOKIE;
}
RequestContext::UserLanguage RequestContext::determine_user_language() const
{
try {
return {UserLanguage::SelectorKind::QUERY_PARAM, get_argument("userlang")};
} catch(const std::out_of_range&) {}
try {
return {UserLanguage::SelectorKind::COOKIE, cookies.at("userlang")};
} catch(const std::out_of_range&) {}
try {
const std::string acceptLanguage = get_header("Accept-Language");
const auto userLangPrefs = parseUserLanguagePreferences(acceptLanguage);

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;
@@ -119,15 +124,12 @@ class RequestContext {
std::string get_user_language() const;
std::string get_requested_format() const;
bool user_language_comes_from_cookie() const;
private: // types
struct UserLanguage
{
enum SelectorKind
{
QUERY_PARAM,
COOKIE,
ACCEPT_LANGUAGE_HEADER,
DEFAULT
};
@@ -148,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);
@@ -387,17 +570,10 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
}
if ( ! request.user_language_comes_from_cookie() ) {
const std::string cookie = "userlang=" + request.get_user_language()
+ ";Path=" + request.get_root_path()
+ ";Max-Age=31536000";
MHD_add_response_header(response, MHD_HTTP_HEADER_SET_COOKIE, cookie.c_str());
}
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
if (m_verbose)
if (verbose)
print_response_info(m_returnCode, response);
auto ret = MHD_queue_response(connection, m_returnCode, response);
@@ -405,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)
{
@@ -415,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)
{
@@ -446,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

@@ -0,0 +1,75 @@
#include "tools.h"
#include "stringTools.h"
#include <mutex>
namespace kiwix
{
namespace
{
// These mappings are not provided by the ICU library, any such mappings can be manually added here
std::map<std::string, std::string> iso639_3 = {
{"atj", "atikamekw"},
{"azb", "آذربایجان دیلی"},
{"bcl", "central bikol"},
{"bgs", "tagabawa"},
{"bxr", "буряад хэлэн"},
{"cbk", "chavacano"},
{"cdo", "閩東語"},
{"dag", "Dagbani"},
{"diq", "dimli"},
{"dty", "डोटेली"},
{"eml", "emiliân-rumagnōl"},
{"fbs", "српскохрватски"},
{"fon", "fɔ̀ngbè"},
{"guw", "Gungbe"},
{"hbs", "srpskohrvatski"},
{"ido", "ido"},
{"kbp", "kabɩ"},
{"kld", "Gamilaraay"},
{"lbe", "лакку маз"},
{"lbj", "ལ་དྭགས་སྐད་"},
{"map", "Austronesian"},
{"mhr", "марий йылме"},
{"mnw", "ဘာသာမန်"},
{"myn", "mayan"},
{"nah", "nahuatl"},
{"nai", "north American Indian"},
{"nds", "plattdütsch"},
{"nrm", "bhasa narom"},
{"olo", "livvi"},
{"pih", "Pitcairn-Norfolk"},
{"pnb", "Western Panjabi"},
{"rmr", "Caló"},
{"rmy", "romani shib"},
{"roa", "romance languages"},
{"twi", "twi"},
// ICU for Ubuntu versions <= focal (20.04) returns "" for the language code ""
// unlike the later versions - which returns "und". We map this value to "Undetermined" for a common ground.
{"", "Undetermined"},
};
std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
const kiwix::ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
}
} // unnamed namespace
std::string getLanguageSelfName(const std::string& lang)
{
std::call_once(fillLanguagesFlag, fillLanguagesMap);
const auto itr = iso639_3.find(lang);
if (itr != iso639_3.end()) {
return itr->second;
}
return lang;
};
} // namespace kiwix

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

@@ -0,0 +1,70 @@
#include "tools.h"
#include <pugixml.hpp>
namespace kiwix
{
namespace
{
#define VALUE(name) entryNode.child(name).child_value()
FeedLanguages parseLanguages(const pugi::xml_document& doc)
{
pugi::xml_node feedNode = doc.child("feed");
FeedLanguages langs;
for (pugi::xml_node entryNode = feedNode.child("entry"); entryNode;
entryNode = entryNode.next_sibling("entry")) {
auto title = VALUE("title");
auto code = VALUE("dc:language");
langs.push_back({code, title});
}
return langs;
}
FeedCategories parseCategories(const pugi::xml_document& doc)
{
pugi::xml_node feedNode = doc.child("feed");
FeedCategories categories;
for (pugi::xml_node entryNode = feedNode.child("entry"); entryNode;
entryNode = entryNode.next_sibling("entry")) {
auto title = VALUE("title");
categories.push_back(title);
}
return categories;
}
} // unnamed namespace
FeedLanguages readLanguagesFromFeed(const std::string& content)
{
pugi::xml_document doc;
pugi::xml_parse_result result
= doc.load_buffer((void*)content.data(), content.size());
if (result) {
auto langs = parseLanguages(doc);
return langs;
}
return FeedLanguages();
}
FeedCategories readCategoriesFromFeed(const std::string& content)
{
pugi::xml_document doc;
pugi::xml_parse_result result
= doc.load_buffer((void*)content.data(), content.size());
FeedCategories categories;
if (result) {
categories = parseCategories(doc);
return categories;
}
return categories;
}
} // namespace kiwix

View File

@@ -327,22 +327,37 @@ 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 escapeBackslashes(const std::string& s)
std::string kiwix::escapeForJSON(const std::string& s, bool escapeQuote)
{
std::string es;
es.reserve(s.size());
std::ostringstream oss;
for (char c : s) {
if ( c == '\\' ) {
es.push_back('\\');
oss << "\\\\";
} else if ( unsigned(c) < 0x20U ) {
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;
}
es.push_back(c);
}
return es;
return oss.str();
}
namespace
{
std::string makeFulltextSearchSuggestion(const std::string& lang,
const std::string& queryString)
{
@@ -368,10 +383,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
? suggestion.getSnippet()
: suggestion.getTitle();
result.set("label", escapeBackslashes(label));
result.set("value", escapeBackslashes(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", escapeBackslashes(suggestion.getPath()));
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
result.set("first", m_data.is_empty_list());
m_data.push_back(result);
}
@@ -381,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", escapeBackslashes(label));
result.set("value", escapeBackslashes(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

@@ -31,7 +31,6 @@
namespace kiwix
{
std::string beautifyInteger(uint64_t number);
std::string beautifyFileSize(uint64_t number);
void printStringInHexadecimal(const char* s);
void printStringInHexadecimal(icu::UnicodeString s);
void stringReplacement(std::string& str,
@@ -54,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

@@ -5,9 +5,11 @@ skin/i18n/de.json
skin/i18n/dga.json
skin/i18n/el.json
skin/i18n/en.json
skin/i18n/es.json
skin/i18n/fi.json
skin/i18n/fr.json
skin/i18n/he.json
skin/i18n/hi.json
skin/i18n/hy.json
skin/i18n/ia.json
skin/i18n/it.json
@@ -16,14 +18,19 @@ skin/i18n/ko.json
skin/i18n/ku-latn.json
skin/i18n/lb.json
skin/i18n/mk.json
skin/i18n/ms.json
skin/i18n/nl.json
skin/i18n/nqo.json
skin/i18n/or.json
skin/i18n/pl.json
skin/i18n/ru.json
skin/i18n/sc.json
skin/i18n/sk.json
skin/i18n/skr-arab.json
skin/i18n/sl.json
skin/i18n/sq.json
skin/i18n/sv.json
skin/i18n/te.json
skin/i18n/test.json
skin/i18n/tr.json
skin/i18n/zh-hans.json

View File

@@ -9,7 +9,8 @@ skin/search-icon.svg
skin/iso6391To3.js
skin/isotope.pkgd.min.js
skin/index.js
skin/autoComplete.min.js
skin/autoComplete/autoComplete.min.js
skin/kiwix.css
skin/taskbar.css
skin/index.css
skin/fonts/Poppins.ttf
@@ -42,7 +43,7 @@ templates/no_js_download.html
opensearchdescription.xml
ft_opensearchdescription.xml
catalog_v2_searchdescription.xml
skin/css/autoComplete.css
skin/autoComplete/css/autoComplete.css
skin/favicon/android-chrome-192x192.png
skin/favicon/android-chrome-512x512.png
skin/favicon/apple-touch-icon.png

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

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

View File

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

View File

@@ -69,30 +69,70 @@ function $t(msgId, params={}) {
}
}
function getCookie(cookieName) {
const name = cookieName + "=";
let result;
decodeURIComponent(document.cookie).split('; ').forEach(val => {
if (val.indexOf(name) === 0) {
result = val.substring(name.length);
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;
}
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')
|| getCookie('userlang')
|| DEFAULT_UI_LANGUAGE;
|| window.localStorage.getItem('userlang')
|| getDefaultUserLanguage();
}
function setUserLanguage(lang, callback) {
setPermanentGlobalCookie('userlang', lang);
window.localStorage.setItem('userlang', lang);
Translations.load(lang);
Translations.whenReady(callback);
}
@@ -144,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;
}
@@ -156,3 +194,4 @@ window.$t = $t;
window.getUserLanguage = getUserLanguage;
window.setUserLanguage = setUserLanguage;
window.initUILanguageSelector = initUILanguageSelector;
window.I18n = I18n;

View File

@@ -1,20 +1,57 @@
{
"@metadata": {
"authors": [
"IMayBeABitShy",
"Lucas Werkmeister",
"ThisCarthing"
]
},
"name": "Deutsch",
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Buch nicht gefunden: {{BOOK_NAME}}",
"too-many-books": "Zu viele Bücher angefragt ({{NB_BOOKS}}), die Beschränkung liegt bei {{LIMIT}}",
"no-book-found": "Keine Bücher entsprechen den Auswahlkriterien",
"url-not-found": "Die angeforderte URL \"{{url}}\" konnte auf diesem Server nicht gefunden werden.",
"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",
"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.",
"400-page-title": "Ungültige Anfrage",
"400-page-heading": "Ungültige Anfrage",
"404-page-title": "Inhalt nicht gefunden",
"404-page-heading": "Nicht gefunden",
"500-page-title": "Interner Server-Fehler",
"500-page-heading": "Interner Server-Fehler",
"fulltext-search-unavailable": "Die Volltestsuche steht nicht zur Verfügung.",
"no-search-results": "Die Volltextsuche ist für diesen Inhalt nicht verfügbar.",
"library-button-text": "Zur Willkommensseite gehen",
"home-button-text": "Zur Hauptseite von '{{BOOK_TITLE}}' gehen",
"random-page-button-text": "Zu einer zufällig ausgewählten Seite gehen",
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen"
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen",
"confusion-of-tongues": "Zwei oder mehr Bücher unterschiedlicher Sprachen werden durchsucht, was zu unübersichtlichen Ergebnissen führen kann.",
"welcome-page-overzealous-filter": "Keine Ergebnisse gefunden. Möchten Sie den <a href=\"{{URL}}\">Filter zurücksetzen</a>?",
"powered-by-kiwix-html": "Angetrieben durch &nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Suchen",
"book-filtering-all-categories": "Alle Kategorien",
"book-filtering-all-languages": "Alle Sprachen",
"count-of-matching-books": "{{COUNT}} Bücher",
"download": "Herunterladen",
"direct-download-link-text": "Direkt",
"direct-download-alt-text": "direkt herunterladen",
"hash-download-link-text": "Sha256 Hash",
"hash-download-alt-text": "Hash herunterladen",
"magnet-link-text": "Magnet Link",
"magnet-alt-text": "Magnet Link herunterladen",
"torrent-download-link-text": "Torrent-Datei",
"torrent-download-alt-text": "Torrent herunterladen",
"library-opds-feed-all-entries": "ODPS Feed der Bibliothek - Alle Einträge",
"filter-by-tag": "Nach Tag \"{{TAG}}\" filtern",
"stop-filtering-by-tag": "Filterung nach Tag \"{{TAG}}\" aufheben",
"library-opds-feed-parameterised": "ODPS Feed der Bibliothek - Einträge mit {{#LANG}\nSprache {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}}{{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
"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"
}

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"
}

57
static/skin/i18n/es.json Normal file
View File

@@ -0,0 +1,57 @@
{
"@metadata": {
"authors": [
"Fitoschido",
"Ovruni",
"SpikeShroom",
"Vis4valentine"
]
},
"name": "español",
"suggest-full-text-search": "que contenga \"{{{SEARCH_TERMS}}}\"...",
"no-such-book": "No existe el libro: {{BOOK_NAME}}",
"too-many-books": "Demasiadas solicitudes de libros ({{NB_BOOKS}}) donde el límite es {{LIMIT}}",
"no-book-found": "Ningún libro coincide con los criterios de selección.",
"url-not-found": "La URL solicitada \"{{url}}\" no se encontró en este servidor.",
"suggest-search": "Haga una búsqueda de texto completo para <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "¡Ups! Error al elegir un artículo aleatorio :(",
"invalid-raw-data-type": "{{DATATYPE}} no es una solicitud válida de contenido en crudo.",
"no-value-for-arg": "No se ha proporcionado ningún valor para el argumento {{ARGUMENT}}",
"no-query": "No se ha proporcionado ninguna consulta.",
"raw-entry-not-found": "No se puede encontrar la entrada {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Solicitud inválida",
"400-page-heading": "Solicitud inválida",
"404-page-title": "Contenido no encontrado",
"404-page-heading": "No encontrado",
"500-page-title": "Error interno del servidor",
"500-page-heading": "Error interno del servidor",
"fulltext-search-unavailable": "Búsqueda de texto completo no disponible",
"no-search-results": "El motor de búsqueda de texto completo no está disponible para este contenido.",
"library-button-text": "Ir a la página de bienvenida",
"home-button-text": "Ir a la página principal de '{{BOOK_TITLE}}'",
"random-page-button-text": "Ir a una página seleccionada al azar",
"searchbox-tooltip": "Buscar '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Dos o más libros en diferentes idiomas participarían en la búsqueda, lo que puede llevar a resultados confusos.",
"welcome-page-overzealous-filter": "Sin resultados. ¿Quieres <a href=\"{{URL}}\">restablecer el filtro</a> ?",
"powered-by-kiwix-html": "Desarrollado por&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Buscar",
"book-filtering-all-categories": "Todas las categorías",
"book-filtering-all-languages": "Todos los idiomas",
"count-of-matching-books": "{{COUNT}} libro(s)",
"download": "Descargar",
"direct-download-link-text": "Directamente",
"direct-download-alt-text": "descarga directa",
"hash-download-link-text": "hash sha256",
"hash-download-alt-text": "descargar hash",
"magnet-link-text": "Enlace magnético",
"magnet-alt-text": "Descargar link magnético",
"torrent-download-link-text": "Archivo de torrent",
"torrent-download-alt-text": "descargar torrent",
"filter-by-tag": "Filtrar por etiqueta \"{{TAG}}\"",
"stop-filtering-by-tag": "Dejar de filtrar por etiqueta \"{{TAG}}\"",
"library-opds-feed-parameterised": "Feed OPDS de la biblioteca: entradas que coinciden con {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nEtiqueta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Bienvenido al servidor Kiwix",
"download-links-heading": "Enlaces de descarga para <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Descargar libro",
"preview-book": "Previsualizar"
}

View File

@@ -2,11 +2,14 @@
"@metadata": {
"authors": [
"Gomoko",
"Stephane",
"Thibaut120094",
"Verdy p"
"Verdy p",
"Vikoula5",
"Wladek92"
]
},
"name": "français",
"name": "Français",
"suggest-full-text-search": "contenant « {{{SEARCH_TERMS}}} »...",
"no-such-book": "Aucun livre avec ce nom: {{BOOK_NAME}}",
"too-many-books": "Trop de livres demandés ({{NB_BOOKS}}) alors que la limite est de {{LIMIT}}",
@@ -15,6 +18,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}} »",
@@ -24,6 +28,7 @@
"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.",
"library-button-text": "Aller à la page de bienvenue",
@@ -53,5 +58,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"
}

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,6 +24,7 @@
"404-page-heading": "לא נמצא",
"500-page-title": "שגיאת שרת פנימית",
"500-page-heading": "שגיאת שרת פנימית",
"500-page-text": "אירעה שגיאת שרת פנימית. אנחנו מצטערים על זה :/",
"fulltext-search-unavailable": "חיפוש בטקסט מלא אינו זמין",
"no-search-results": "מנוע החיפוש בטקסט מלא אינו זמין עבור התוכן הזה.",
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
@@ -52,5 +54,6 @@
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "הורדת ספר",
"preview-book": "תצוגה מקדימה"
"preview-book": "תצוגה מקדימה",
"unknown-error": "שגיאה בלתי־ידועה"
}

56
static/skin/i18n/hi.json Normal file
View File

@@ -0,0 +1,56 @@
{
"@metadata": {
"authors": [
"Abijeet Patro",
"Juuz0"
]
},
"name": "हिन्दी",
"suggest-full-text-search": "जिसमें '{{{SEARCH_TERMS}}}' शामिल है...",
"no-such-book": "ऐसी कोई किताब नहीं: {{BOOK_NAME}}",
"too-many-books": "बहुत सारी पुस्तकों का अनुरोध किया गया है ({{NB_BOOKS}}) जहां सीमा {{LIMIT}} है",
"no-book-found": "कोई भी पुस्तक चयन मानदंड से मेल नहीं खाती",
"url-not-found": "अनुरोधित URL \"{{url}}\" इस सर्वर पर नहीं मिला।",
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> के लिए पूर्ण पाठ खोज करें",
"random-article-failure": "उफ़! एक यादृच्छिक लेख चुनने में विफल :(",
"invalid-raw-data-type": "{{DATATYPE}} कच्ची सामग्री के लिए वैध अनुरोध नहीं है।",
"no-value-for-arg": "तर्क के लिए कोई मूल्य प्रदान नहीं किया गया {{ARGUMENT}}",
"no-query": "कोई प्रश्न नहीं दिया गया.",
"raw-entry-not-found": "{{DATATYPE}} प्रविष्टि {{ENTRY}} नहीं मिल सकी",
"400-page-title": "अवैध आवेदन",
"400-page-heading": "अवैध आवेदन",
"404-page-title": "सामग्री नहीं मिली",
"404-page-heading": "नहीं मिला",
"500-page-title": "आंतरिक सर्वर त्रुटि",
"500-page-heading": "आंतरिक सर्वर त्रुटि",
"fulltext-search-unavailable": "पूर्णपाठ खोज अनुपलब्ध",
"no-search-results": "इस सामग्री के लिए पूर्णपाठ खोज इंजन उपलब्ध नहीं है।",
"library-button-text": "स्वागत पृष्ठ पर जाएँ",
"home-button-text": "'{{BOOK_TITLE}}' के मुख्य पृष्ठ पर जाएँ",
"random-page-button-text": "यादृच्छिक रूप से चयनित पृष्ठ पर जाएँ",
"searchbox-tooltip": "'{{BOOK_TITLE}}' खोजें",
"confusion-of-tongues": "विभिन्न भाषाओं की दो या दो से अधिक पुस्तकें खोज में भाग लेंगी, जिससे भ्रमित करने वाले परिणाम मिल सकते हैं।",
"welcome-page-overzealous-filter": "कोई परिणाम नहीं। क्या आप <a href=\"{{URL}}\">फ़िल्टर रीसेट करना</a> चाहेंगे?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">किविक्स</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": "शा256 हैश",
"hash-download-alt-text": "हैश डाउनलोड करें",
"magnet-link-text": "MAGNET लिंक",
"magnet-alt-text": "MAGNET डाउनलोड करें",
"torrent-download-link-text": "टोरेंट फ़ाइल",
"torrent-download-alt-text": "टोरेंट डाउनलोड करें",
"library-opds-feed-all-entries": "लाइब्रेरी ओपीडीएस फ़ीड - सभी प्रविष्टियाँ",
"filter-by-tag": "टैग \"{{TAG}}\" द्वारा फ़िल्टर करें",
"stop-filtering-by-tag": "\"{{TAG}}\" टैग द्वारा फ़िल्टर करना बंद करें",
"library-opds-feed-parameterised": "लाइब्रेरी ओपीडीएस फ़ीड - मिलान वाली प्रविष्टियाँ {{#LANG}}\nभाषा: {{LANG}} {/LANG}}{{#CATEGORY}}\nश्रेणी: {{CATEGORY}} {/CATEGORY}} {{#TAG}}\nटैग: {{TAG}} {{/TAG}}{{#Q}}\nक्वेरी: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "कीविक्स सर्वर में आपका स्वागत है",
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> के लिए डाउनलोड लिंक",
"download-links-title": "पुस्तक डाउनलोड करें",
"preview-book": "पूर्वावलोकन"
}

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,7 @@
"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 :/",
"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 +33,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",
@@ -23,5 +24,7 @@
"book-filtering-all-languages": "All Sproochen",
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
"download": "Eroflueden",
"direct-download-link-text": "Direkt"
"direct-download-link-text": "Direkt",
"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,6 +24,7 @@
"404-page-heading": "Не е најдено",
"500-page-title": "Внатрешна грешка во опслужувачот",
"500-page-heading": "Внатрешна грешка во опслужувачот",
"500-page-text": "Настана внатрешна грешка во опслужувачот. Жал ни е :/",
"fulltext-search-unavailable": "Целотекстното пребарување е недостапно",
"no-search-results": "Погонот за целотекстно пребарување не е достапен за оваа содржина.",
"library-button-text": "Оди на воведната страница",
@@ -52,5 +54,6 @@
"welcome-to-kiwix-server": "Добре дојдовте на Опслужувачот на Кивикс",
"download-links-heading": "Врски за преземање на <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Преземи книга",
"preview-book": "Преглед"
"preview-book": "Преглед",
"unknown-error": "Непозната грешка"
}

20
static/skin/i18n/ms.json Normal file
View File

@@ -0,0 +1,20 @@
{
"@metadata": {
"authors": [
"Tofeiku"
]
},
"name": "Bahasa Melayu",
"404-page-heading": "Tidak Dijumpai",
"500-page-title": "Ralat Pelayan Dalaman",
"500-page-heading": "Ralat Pelayan Dalaman",
"library-button-text": "Pergi ke laman selamat datang",
"searchbox-tooltip": "Cari '{{BOOK_TITLE}}'",
"search": "Cari",
"book-filtering-all-categories": "Semua kategori",
"book-filtering-all-languages": "Semua bahasa",
"download": "Muat turun",
"direct-download-link-text": "Langsung",
"direct-download-alt-text": "muat turun langsung",
"download-links-title": "Muat turun buku"
}

View File

@@ -6,10 +6,31 @@
"Vistaus"
]
},
"too-many-books": "Er zijn teveel boeken opgevraagd ({{NB_BOOKS}}). Het limiet is {{LIMIT}}.",
"name": "Nederlands",
"suggest-full-text-search": "bevat {{{SEARCH_TERMS}}}’…",
"no-such-book": "Boek bestaat niet: {{BOOK_NAME}}",
"too-many-books": "Er zijn teveel boeken opgevraagd ({{NB_BOOKS}}). De limiet is {{LIMIT}}.",
"no-book-found": "Er zijn geen boeken die overeenkomen met de zoekcriteria",
"url-not-found": "De opgevraagde URL “{{url}}” is niet gevonden op deze server.",
"suggest-search": "In volledige tekst zoeen naar <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oeps! Kan geen willekeurig artikel kiezen :(",
"invalid-raw-data-type": "{{DATATYPE}} is geen geldig verzoek voor onbewerkte inhoud.",
"no-value-for-arg": "Er is geen waarde opgegeven bij {{ARGUMENT}}",
"no-query": "Er is geen zoekterm opgegeven.",
"raw-entry-not-found": "Kan het {{DATATYPE}}-item {{ENTRY}} niet vinden",
"400-page-title": "Ongeldig verzoek",
"400-page-heading": "Ongeldig verzoek",
"404-page-title": "Inhoud niet gevonden",
"404-page-heading": "Niet gevonden",
"500-page-title": "Interne serverfout",
"500-page-heading": "Interne serverfout",
"fulltext-search-unavailable": "Zoeken in volledige tekst is niet beschikbaar",
"no-search-results": "De zoekmachine voor volledige tekst is niet beschikbaar voor deze inhoud.",
"library-button-text": "Naar de welkomstpagina",
"home-button-text": "Naar de hoofdpagina van {{BOOK_TITLE}}",
"random-page-button-text": "Naar een willekeurig geselecteerde pagina gaan",
"searchbox-tooltip": "Naar {{BOOK_TITLE}} zoeken",
"confusion-of-tongues": "Er zouden twee of meer boeken in verschillende talen deelnemen aan de zoekopdracht, wat tot verwarrende resultaten kan leiden.",
"welcome-page-overzealous-filter": "Geen resultaat. Wilt u <a href=\"{{URL}}\">het filter resetten</a>?",
"powered-by-kiwix-html": "Mogelijk gemaakt door <a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Zoeken",
@@ -25,6 +46,12 @@
"magnet-alt-text": "magnet-link van de download",
"torrent-download-link-text": "Torrent-bestand",
"torrent-download-alt-text": "torrent downloaden",
"filter-by-tag": "Filteren op tag \"{{TAG}}\"",
"stop-filtering-by-tag": "Stoppen met filteren op tag \"{{TAG}}\""
"library-opds-feed-all-entries": "OPDS-feed bibliotheek: alle vermeldingen",
"filter-by-tag": "Filteren op label “{{TAG}}”",
"stop-filtering-by-tag": "Niet meer filteren op label “{{TAG}}”",
"library-opds-feed-parameterised": "OPDS-feed bibliotheek: vermeldingen die overeenkomen met {{#LANG}}\nTaal: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nLabel: {{TAG}} {{/TAG}}{{#Q}}\nZoekopdracht: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Welkom bij de Kiwix-server",
"download-links-heading": "Downloadkoppelingen voor <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Boek downloaden",
"preview-book": "Voorvertoning"
}

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

@@ -0,0 +1,55 @@
{
"@metadata": {
"authors": [
"Gouri"
]
},
"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}} for</a> ପାଇଁ ଏକ ସମ୍ପୂର୍ଣ୍ଣ ପାଠ ସନ୍ଧାନ କର |",
"random-article-failure": "ଓହୋ! ଏକ ଅନିୟମିତ ପ୍ରବନ୍ଧ ବାଛିବାରେ ବିଫଳ :(",
"invalid-raw-data-type": "{{DATATYPE}} କଞ୍ଚା ବିଷୟବସ୍ତୁ ପାଇଁ ଏକ ବ valid ଧ ଅନୁରୋଧ ନୁହେଁ |",
"no-value-for-arg": "ଯୁକ୍ତି ପାଇଁ କୌଣସି ମୂଲ୍ଯ଼ ପ୍ରଦାନ କରାଯାଇନାହିଁ ${{ARGUMENT}}",
"no-query": "କୌଣସି ପ୍ରଶ୍ନ ପ୍ରଦାନ କରାଯାଇନାହିଁ ।",
"raw-entry-not-found": "${{DATATYPE}} ${{ENTRY}} କୁ ଖୋଜି ପାରୁନାହିଁ|",
"400-page-title": "ଅବୈଧ ଅନୁରୋଧ",
"400-page-heading": "ଅବୈଧ ଅନୁରୋଧ",
"404-page-title": "ବିଷଯ଼ବସ୍ତୁ ମିଳୁନାହିଁ",
"404-page-heading": "ମିଳିଲାନାହିଁ",
"500-page-title": "ଆନ୍ତରିକ ସର୍ଭର ତୃଟି",
"500-page-heading": "ଆନ୍ତରିକ ସର୍ଭର ତୃଟି",
"fulltext-search-unavailable": "ପୂର୍ଣ୍ଣ ପାଠ ସନ୍ଧାନ ଉପଲବ୍ଧ ନାହିଁ |",
"no-search-results": "ଏହି ବିଷୟବସ୍ତୁ ପାଇଁ ଫୁଲ୍ ଟେକ୍ସଟ୍ ସର୍ଚ୍ଚ ଇଞ୍ଜିନ୍ ଉପଲବ୍ଧ ନାହିଁ |",
"library-button-text": "ସ୍ୱାଗତ ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ |",
"home-button-text": "'{{BOOK_TITLE}}' ର ମୁଖ୍ୟ ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ |",
"random-page-button-text": "ଏକ ଅନିୟମିତ ଭାବରେ ମନୋନୀତ ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ |",
"searchbox-tooltip": "'{{BOOK_TITLE}}' ସନ୍ଧାନ କରନ୍ତୁ |",
"confusion-of-tongues": "ବିଭିନ୍ନ ଭାଷାରେ ଦୁଇ କିମ୍ବା ଅଧିକ ପୁସ୍ତକ ସର୍ଚ୍ଚରେ ଅଂଶଗ୍ରହଣ କରିବେ ଯାହା ବିଭ୍ରାନ୍ତିକର ଫଳାଫଳ ଆଣିପାରେ |",
"welcome-page-overzealous-filter": "କୌଣସି ଫଳାଫଳ ନାହିଁ | ଆପଣ <a href=\"{{URL}}\">ଫିଲ୍ଟର ପୁନ res ସେଟ୍</a> କରିବାକୁ ଚାହୁଁଛନ୍ତି କି?",
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">କିୱିକ୍ସ</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": "torrent ଡାଉନଲୋଡ୍ କରନ୍ତୁ",
"library-opds-feed-all-entries": "ଲାଇବ୍ରେରୀ OPDS ଫିଡ୍ - ସମସ୍ତ ଏଣ୍ଟ୍ରିଗୁଡିକ |",
"filter-by-tag": "ଟ୍ୟାଗ୍ ଦ୍ୱାରା ଫିଲ୍ଟର୍ \"{{TAG}}\"",
"stop-filtering-by-tag": "\"${{TAG}}\" ଟ୍ୟାଗ୍ ଦ୍ୱାରା ଫିଲ୍ଟର କରିବା ବନ୍ଦ କରନ୍ତୁ |",
"library-opds-feed-parameterised": "ଲାଇବ୍ରେରୀ OPDS ଫିଡ୍ - ଏଣ୍ଟ୍ରିଗୁଡିକ {{#LANG}}! N! ଭାଷା! ${{LANG}} ! {#TAG}} Category: ${{CATEGORY}} ଟ୍ୟାଗ୍: ${{TAG}} {{/ TAG}} {{# Q}}! ପ୍ରଶ୍ନ: {{Q}} {{/ Q}}",
"welcome-to-kiwix-server": "Kiwix ସର୍ଭରକୁ ସ୍ୱାଗତ |",
"download-links-heading": "<b><i>${{BOOK_TITLE}} for</i></b> ପାଇଁ ଲିଙ୍କ୍ ଡାଉନଲୋଡ୍ କରନ୍ତୁ |",
"download-links-title": "ବହି ଡାଉନଲୋଡ୍ କରନ୍ତୁ |",
"preview-book": "ପୂର୍ବରୂପ"
}

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

@@ -4,7 +4,9 @@
"Fenixs-ru",
"Kareyac",
"Okras",
"Pacha Tchernof"
"Pacha Tchernof",
"Razno0",
"Smavrina"
]
},
"name": "русский",
@@ -32,7 +34,23 @@
"random-page-button-text": "Перейти на случайно выбранную страницу",
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'",
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам.",
"powered-by-kiwix-html": "При поддержке&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Найти",
"book-filtering-all-categories": "Все категории",
"book-filtering-all-languages": "Все языки",
"download": "Скачать"
"count-of-matching-books": "{{COUNT}} книг(и)",
"download": "Скачать",
"direct-download-alt-text": "прямая загрузка",
"hash-download-link-text": "Хэш Sha256",
"hash-download-alt-text": "скачать хэш",
"magnet-link-text": "Магнитная ссылка",
"torrent-download-link-text": "Торрент-файл",
"torrent-download-alt-text": "скачать торрент",
"library-opds-feed-all-entries": "Канал библиотеки OPDS  все записи",
"filter-by-tag": "Фильтровать по тегу \"{{TAG}}\"",
"stop-filtering-by-tag": "Прекратить фильтрацию по тегу \"{{TAG}}\"",
"welcome-to-kiwix-server": "Добро пожаловать на сервер Kiwix",
"download-links-heading": "Ссылки для скачивания <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Скачать книгу",
"preview-book": "Предпросмотр"
}

View File

@@ -14,6 +14,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,6 +24,7 @@
"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.",
"library-button-text": "Pojdite na pozdravno stran",
@@ -52,5 +54,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"
}

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

@@ -0,0 +1,55 @@
{
"@metadata": {
"authors": [
"Besnik b"
]
},
"name": "Shqip",
"suggest-full-text-search": "që përmban '{{{SEARCH_TERMS}}}'…",
"no-such-book": "Ska libër të tillë: {{BOOK_NAME}}",
"too-many-books": "U kërkuan shumë libra ({{NB_BOOKS}}), teksa kufiri është {{LIMIT}}",
"no-book-found": "Ska libër me përputhje me kriteret e përzgjedhjes",
"url-not-found": "URL “{{url}}” e kërkuar su gjet në këtë shërbyes.",
"suggest-search": "Bëni një kërkim të plotë teksti për <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Oh! Su arrit të merrej një artikull kuturu :(",
"invalid-raw-data-type": "{{DATATYPE}} sështë varg i vlefshëm kërkimi për lëndë të papërpunuar.",
"no-value-for-arg": "Su dha vlerë për argumentin {{ARGUMENT}}",
"no-query": "Su dha varg kërkimi.",
"raw-entry-not-found": "Sgjendet dot zëri {{DATATYPE}} {{ENTRY}}",
"400-page-title": "Kërkesë e pavlefshme",
"400-page-heading": "Kërkesë e pavlefshme",
"404-page-title": "Su gjet lëndë",
"404-page-heading": "Su Gjet",
"500-page-title": "Gabim i Brendshëm Shërbyesi",
"500-page-heading": "Gabim i Brendshëm Shërbyesi",
"fulltext-search-unavailable": "Kërkim teksti të plotë jo i mundshëm",
"no-search-results": "Sështë i passhëm motori i kërkimit të tekstit të plotë për këtë lëndë.",
"library-button-text": "Kalo te faqja e mirëseardhjes",
"home-button-text": "Kalo te faqja krye e '{{BOOK_TITLE}}'",
"random-page-button-text": "Kalo te një faqe e përzgjedhur kuturu",
"searchbox-tooltip": "Kërko në '{{BOOK_TITLE}}'",
"confusion-of-tongues": "Kërkimi do të merrej me dy ose më tepër libra në gjuhë të ndryshme, çka mund të sjellë përfundime të ngatërruara.",
"welcome-page-overzealous-filter": "Ska përfundime. Do të donit të <a href=\"{{URL}}\">riujdisni filtrimin</a>?",
"powered-by-kiwix-html": "Bazuar në&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>",
"search": "Kërko",
"book-filtering-all-categories": "Krejt kategoritë",
"book-filtering-all-languages": "Krejt gjuhët",
"count-of-matching-books": "{{COUNT}} libër(a)",
"download": "Shkarkoje",
"direct-download-link-text": "Drejtpërsëdrejti",
"direct-download-alt-text": "shkarkim i drejtpërdrejt",
"hash-download-link-text": "Hash sha256",
"hash-download-alt-text": "shkarko hashin",
"magnet-link-text": "Lidhje Magnet",
"magnet-alt-text": "shkarko magnetin",
"torrent-download-link-text": "Kartelë Torrent",
"torrent-download-alt-text": "shkarko torrent-in",
"library-opds-feed-all-entries": "Prurje OPDS Biblioteke - Krejt zërat",
"filter-by-tag": "Filtroji sipas etiketës “{{TAG}}”",
"stop-filtering-by-tag": "Resht së filtruari sipas etiketë “{{TAG}}”",
"library-opds-feed-parameterised": "Prurje OPDS Biblioteke - zëra që kanë përputhje me {{#LANG}}\nGjuhë: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategori: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEtiketë: {{TAG}} {{/TAG}}{{#Q}}\nVarg Kërkimi: {{Q}} {{/Q}}",
"welcome-to-kiwix-server": "Mirë se vini në Shërbyesin Kiwix",
"download-links-heading": "Lidhje shkarkimi për <b><i>{{BOOK_TITLE}}</i></b>",
"download-links-title": "Shkarkoje librin",
"preview-book": "Paraparje"
}

View File

@@ -15,6 +15,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,6 +25,7 @@
"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.",
"library-button-text": "Gå till hemsidan",
@@ -53,5 +55,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"
}

58
static/skin/i18n/te.json Normal file
View File

@@ -0,0 +1,58 @@
{
"@metadata": {
"authors": [
"Amire80",
"Chaduvari",
"Rishitha 1238",
"V Bhavya"
]
},
"name": "ఇంగ్లీషు",
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}'ని కలిగి ఉంది...",
"no-such-book": "అలాంటి పుస్తకం లేదు: {{BOOK_NAME}}",
"too-many-books": "పరిమితి {{LIMIT}} ఉన్న చాలా పుస్తకాలు అభ్యర్థించబడ్డాయి ({{NB_BOOKS}})",
"no-book-found": "ఎంపిక ప్రమాణాలకు ఏ పుస్తకం సరిపోలలేదు",
"url-not-found": "అభ్యర్థించిన URL \"{{url}}\" ఈ సర్వర్‌లో కనుగొనబడలేదు.",
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> కోసం పూర్తి వచన శోధన చేయండి",
"random-article-failure": "అయ్యో! యాదృచ్ఛిక కథనాన్ని ఎంచుకోవడంలో విఫలమైంది :(",
"invalid-raw-data-type": "ముడి కంటెంట్ కోసం {{DATATYPE}} చెల్లుబాటు అయ్యే అభ్యర్థన కాదు.",
"no-value-for-arg": "ఆర్గ్యుమెంట్ {{ARGUMENT}}కి విలువ అందించబడలేదు",
"no-query": "ప్రశ్న ఏదీ అందించబడలేదు.",
"raw-entry-not-found": "{{DATATYPE}} ఎంట్రీ {{ENTRY}} కనుగొనబడలేదు",
"400-page-title": "చెల్లని అభ్యర్థన",
"400-page-heading": "చెల్లని అభ్యర్థన",
"404-page-title": "కంటెంట్ కనుగొనబడలేదు",
"404-page-heading": "దొరకలేదు",
"500-page-title": "అంతర్గత సర్వర్ లోపం",
"500-page-heading": "అంతర్గత సర్వర్ లోపం",
"fulltext-search-unavailable": "పూర్తి వచన శోధన అందుబాటులో లేదు",
"no-search-results": "ఈ కంటెంట్ కోసం ఫుల్‌టెక్స్ట్ శోధన ఇంజిన్ అందుబాటులో లేదు.",
"library-button-text": "స్వాగత పేజీకి వెళ్లండి",
"home-button-text": "'{{BOOK_TITLE}}' యొక్క ప్రధాన పేజీకి వెళ్లండి",
"random-page-button-text": "యాదృచ్ఛికంగా ఎంచుకున్న పేజీకి వెళ్లండి",
"searchbox-tooltip": "'{{BOOK_TITLE}}'ని శోధించండి",
"confusion-of-tongues": "వివిధ భాషల్లోని రెండు లేదా అంతకంటే ఎక్కువ పుస్తకాలు శోధనలో పాల్గొంటాయి, ఇది గందరగోళ ఫలితాలకు దారితీయవచ్చు.",
"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": "షా256 హాష్",
"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}}\nభాష: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nట్యాగ్: {{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": "ప్రివ్యూ"
}

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

@@ -2,7 +2,9 @@
"@metadata": {
"authors": [
"GuoPC",
"StarrySky"
"StarrySky",
"Sunai",
"XtexChooser"
]
},
"name": "英语",
@@ -12,5 +14,12 @@
"404-page-heading": "未找到",
"500-page-title": "内部服务器错误",
"500-page-heading": "内部服务器错误",
"library-button-text": "前往欢迎页面"
"library-button-text": "前往欢迎页面",
"search": "搜索",
"book-filtering-all-categories": "所有分类",
"book-filtering-all-languages": "所有语言",
"download": "下载",
"magnet-link-text": "磁力链接",
"torrent-download-link-text": "种子文件",
"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,6 +25,7 @@
"404-page-heading": "查無頁面",
"500-page-title": "內部伺服器錯誤",
"500-page-heading": "內部伺服器錯誤",
"500-page-text": "內部伺服器發生錯誤。對此我們深感抱歉:/",
"fulltext-search-unavailable": "全文搜尋無效",
"no-search-results": "全文搜尋引擎不適用此內容。",
"library-button-text": "前往歡迎首頁",
@@ -53,5 +55,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,29 +1,7 @@
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
font-size: 62.5%;
}
body {
position: relative;
box-sizing: border-box;
}
::selection {
background-color: #00b4e4;
color: white;
}
.kiwixNav {
background-color: #f4f6f8;
width: 100%;
padding: 20px;
padding: 10px 20px;
position: sticky;
top: 0;
z-index: 3;
@@ -59,10 +37,10 @@ body {
background-image: none;
border-radius: 1px;
width: 195px;
height: 35px;
height: 30px;
flex: 1;
color: black;
padding: 7px 10px 10px;
padding: 0px 10px 0px 10px;
cursor: pointer;
}
@@ -74,7 +52,7 @@ body {
position: relative;
display: flex;
width: 231px;
height: 35px;
height: 30px;
line-height: 3;
background: #909090;
overflow: hidden;
@@ -104,7 +82,7 @@ body {
}
.kiwixSearch {
height: 35px;
height: 30px;
width: 231px;
border-radius: 10px;
border: solid 1px #b5b2b2;
@@ -118,7 +96,7 @@ body {
.kiwixButton {
margin: 0 17px;
height: 35px;
height: 30px;
width: 100px;
border-radius: 10px;
color: white;
@@ -162,29 +140,6 @@ body {
font-weight: bolder;
}
#uiLanguageSelector {
display: none;
}
#uiLanguageSelector .modal {
height: 140px;
}
#uiLanguageSelector .modal-heading {
height: 40%;
}
#uiLanguageSelector .modal-content #ui_language {
font-size: 1.6rem;
width: 100%;
}
#uiLanguageSelectorButton {
margin: 0 12px 0 0;
float: right;
cursor: pointer;
}
.book__list {
position: relative;
margin: 0 auto;
@@ -325,58 +280,6 @@ body {
-webkit-box-orient: vertical;
}
.modal-wrapper {
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
background-color: rgba(0, 0, 0, 30%);
}
.modal {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
}
.modal-heading {
background-color: #f0f0f0;
height: 20%;
width: 100%;
border-bottom: 1px solid #ececec;
display: grid;
grid-template-columns: 3fr 1fr;
}
.modal-title {
display: flex;
font-size: 15px;
align-items: center;
padding-left: 20px;
font-family: poppins;
}
.modal-close-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
padding: 20px;
}
.modal-content div {
width: 100%;
height: 40px;
@@ -469,7 +372,7 @@ body {
width: auto;
}
.feedLogo, #uiLanguageSelectorButton {
.feedLogo {
height: 30px;
float: right;
}

View File

@@ -46,7 +46,7 @@
window.modalUILanguageSelector.close();
const s = document.getElementById("ui_language");
const lang = s.options[s.selectedIndex].value;
setPermanentGlobalCookie('userlang', lang);
localStorage.setItem('userlang', lang);
window.location.reload();
}
@@ -197,6 +197,58 @@
}
}
function makeURLSearchString(params, keysToURIEncode) {
let output = '';
for (const [key, value] of params.entries()) {
let finalValue = (keysToURIEncode.indexOf(key) >= 0) ? encodeURIComponent(value) : value;
output += `&${key}=${finalValue}`;
}
return output;
}
/* hack for library.kiwix.org magnet links (created by MirrorBrain)
See https://github.com/kiwix/container-images/issues/242 */
async function getFixedMirrorbrainMagnet(magnetLink) {
// parse as query parameters
const params = new URLSearchParams(
magnetLink.replaceAll('&amp;', '&').replace(/^magnet:/, ''));
const zimUrl = params.get('as'); // as= is fallback URL
// download metalink to build list of mirrored URLs
let mirrorUrls = [];
const metalink = await fetch(`${zimUrl}.meta4`).then(response => {
return response.ok ? response.text() : '';
}).catch((_error) => '');
if (metalink) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(metalink, "application/xml");
doc.querySelectorAll("url").forEach((node) => {
if (node.hasAttribute("priority")) { // ensures its a mirror link
mirrorUrls.push(node.innerHTML);
}
});
} catch (err) {
// not a big deal, magnet will only contain primary URL
console.debug(`Failed to parse mirror links for ${zimUrl}`);
}
}
// set webseed (ws=) URL to primary download URL (redirects to mirror)
params.set('ws', zimUrl);
// if we got metalink mirror URLs, append them all
if (mirrorUrls) {
mirrorUrls.forEach((url) => {
params.append('ws', url);
});
}
params.set('xs', `${zimUrl}.torrent`); // adding xs= to point to torrent URL
return 'magnet:?' + makeURLSearchString(params, ['ws', 'as', 'dn', 'xs', 'tr']);
}
async function getMagnetLink(downloadLink) {
const magnetUrl = downloadLink + '.magnet';
const controller = new AbortController();
@@ -204,6 +256,9 @@
const magnetLink = await fetch(magnetUrl, { signal: controller.signal }).then(response => {
return response.ok ? response.text() : '';
}).catch((_error) => '');
if (magnetLink) {
return await getFixedMirrorbrainMagnet(magnetLink);
}
return magnetLink;
}
@@ -585,11 +640,6 @@
setInterval(updateNavVisibilityState, 250);
};
// required by i18n.js:setUserLanguage()
window.setPermanentGlobalCookie = function(name, value) {
document.cookie = `${name}=${value};path=${root};max-age=31536000`;
}
window.onload = () => { setUserLanguage(getUserLanguage(), onload); }
})();

107
static/skin/kiwix.css Normal file
View File

@@ -0,0 +1,107 @@
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
font-size: 62.5%;
}
body {
position: relative;
box-sizing: border-box;
}
::selection {
background-color: #00b4e4;
color: white;
}
.modal-wrapper {
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
background-color: rgba(0, 0, 0, 30%);
}
.modal {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
}
.modal-heading {
background-color: #f0f0f0;
height: 20%;
width: 100%;
border-bottom: 1px solid #ececec;
display: grid;
grid-template-columns: 3fr 1fr;
}
.modal-title {
display: flex;
font-size: 15px;
align-items: center;
padding-left: 20px;
font-family: poppins;
}
.modal-close-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
padding: 20px;
}
#uiLanguageSelector {
display: none;
}
#uiLanguageSelector .modal {
height: 140px;
}
#uiLanguageSelector .modal-heading {
height: 40%;
}
#uiLanguageSelector .modal-content #ui_language {
font-size: 1.6rem;
width: 100%;
}
#uiLanguageSelectorButton {
margin: 0px 12px;
float: right;
cursor: pointer;
height: 30px;
}
@font-face {
font-family: "poppins";
src: url("../skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
}
@font-face {
font-family: "roboto";
src: url("../skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
}

View File

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

View File

@@ -3,8 +3,9 @@
transition: 0.3s;
width: 100%;
box-sizing: border-box;
background: #e3e3e3;
background: #f4f6f8;
border-bottom: 1px solid #aaa;
display: flex;
}
#kiwixtoolbar>a {
@@ -21,8 +22,9 @@
}
.kiwixsearch {
font-size: 1.6rem;
position: relative;
height: 26px;
height: 30px;
width: 100%;
left: 0;
margin-bottom: 0;
@@ -41,6 +43,7 @@
.kiwix .kiwix_centered {
max-width: 720px;
width: 100%;
margin: 0 auto;
}
@@ -48,9 +51,8 @@
display: none;
}
#kiwix_button_show_toggle:checked~label~.kiwix_button_cont,
#kiwix_button_show_toggle:checked~label~.kiwix_button_cont>a {
display: block;
display: inline-block;
}
#kiwix_button_show_toggle:not(:checked)~label~.kiwix_button_cont {
@@ -59,12 +61,12 @@
label[for="kiwix_button_show_toggle"] {
display: inline-block;
height: 26px;
height: 30px;
}
label[for="kiwix_button_show_toggle"] img {
transition: 0.1s;
height: 26px;
height: 30px;
}
#kiwix_button_show_toggle:checked~label img {
@@ -83,11 +85,11 @@ label[for="kiwix_button_show_toggle"],
.kiwix #kiwixtoolbar button,
.kiwix #kiwixtoolbar input[type="submit"] {
box-sizing: border-box !important;
height: 26px !important;
height: 30px !important;
line-height: 20px !important;
margin-right: 5px !important;
padding: 2px 6px !important;
border: 1px solid #999 !important;
border: 1px solid #b5b2b2 !important;
border-radius: 3px !important;
background-color: #ededed !important;
font-weight: normal !important;
@@ -99,9 +101,9 @@ label[for="kiwix_button_show_toggle"],
left: 0;
box-sizing: border-box !important;
width: 100%;
height: 26px !important;
height: 30px !important;
line-height: 20px !important;
border: 1px solid #999 !important;
border: 1px solid #b5b2b2 !important;
border-radius: 3px !important;
background-color: #fff !important;
padding: 2px 2px 2px 27px !important;
@@ -114,11 +116,12 @@ label[for=kiwixsearchbox] {
height: 100%;
left: 5px;
font-size: 90%;
line-height: 26px;
line-height: 30px;
vertical-align: middle;
}
a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
font-size: 1.6rem;
display: block;
text-decoration: none;
color: inherit;
@@ -129,81 +132,6 @@ a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
column-count: 1 !important;
}
.modal-wrapper {
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
background-color: rgba(0, 0, 0, 30%);
}
.modal {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
}
.modal-heading {
background-color: #f0f0f0;
height: 20%;
width: 100%;
border-bottom: 1px solid #ececec;
display: grid;
grid-template-columns: 3fr 1fr;
}
.modal-title {
display: flex;
font-size: 15px;
align-items: center;
padding-left: 20px;
font-family: poppins;
}
.modal-close-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
padding: 20px;
}
#uiLanguageSelector {
display: none;
}
#uiLanguageSelector .modal {
height: 140px;
}
#uiLanguageSelector .modal-heading {
height: 40%;
}
#uiLanguageSelector .modal-content #ui_language {
width: 100%;
}
#uiLanguageSelectorButton {
margin: 0px 12px 6px 12px;
float: right;
cursor: pointer;
height: 30px;
}
@media(min-width:420px) {
.kiwix_button_cont {
display: inline-block !important;
@@ -250,8 +178,12 @@ a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
}
}
@media(max-width:415px) {
@media(max-width:419px) {
.kiwix_searchform {
width: 80%;
}
.kiwix_button_cont {
padding-top: 5px;
}
}

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';
@@ -549,7 +589,3 @@ function finishViewerSetupOnceTranslationsAreLoaded()
viewerSetupComplete = true;
}
function setPermanentGlobalCookie(name, value) {
document.cookie = `${name}=${value};path=${root};max-age=31536000`;
}

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

@@ -5,6 +5,11 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link type="root" href="{{root}}">
<title>Welcome to Kiwix Server</title>
<link
type="text/css"
href="{{root}}/skin/kiwix.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<link
type="text/css"
href="{{root}}/skin/index.css?KIWIXCACHEID"
@@ -26,17 +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">
<style>
@font-face {
font-family: "poppins";
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
}
@font-face {
font-family: "roboto";
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
}
</style>
<script 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>
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>

View File

@@ -5,6 +5,11 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link type="root" href="{{root}}">
<title>{{translations.welcome-to-kiwix-server}}</title>
<link
type="text/css"
href="{{root}}/skin/kiwix.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<link
type="text/css"
href="{{root}}/skin/index.css?KIWIXCACHEID"
@@ -137,4 +142,4 @@
</div>
<div id="kiwixfooter" class="kiwixfooter">{{{translations.powered-by-kiwix-html}}}</div>
</body>
</html>
</html>

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

@@ -8,13 +8,14 @@
object-src 'none';">
<title>ZIM Viewer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
<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>
<script type="text/javascript" src="./skin/viewer.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="./skin/autoComplete.min.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="./skin/autoComplete/autoComplete.min.js?KIWIXCACHEID"></script>
<script>
function getRootLocation() {
const p = location.pathname;
@@ -34,13 +35,6 @@
<div class="kiwix" style="display:none" id="kiwixtoolbarwrapper">
<div id="kiwixtoolbar" class="ui-widget-header">
<div class="kiwix_centered">
<a id="uiLanguageSelectorButton"
onclick="window.modalUILanguageSelector.show()"
alt="Select UI language"
aria-label="Select UI language"
title="Select UI language">
<img src="./skin/langSelector.svg?KIWIXCACHEID">
</a>
<div class="kiwix_searchform">
<form class="kiwixsearch" method="GET" action="javascript:performSearch()" id="kiwixsearchform">
<label for="kiwixsearchbox">&#x1f50d;</label>
@@ -65,6 +59,13 @@
</span>
</div>
</div>
<a onclick="window.modalUILanguageSelector.show()"
alt="Select UI language"
aria-label="Select UI language"
title="Select UI language">
<img id="uiLanguageSelectorButton"
src="./skin/langSelector.svg?KIWIXCACHEID">
</a>
</div>
</div>

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 \"\"");
}
}

41
test/languageTools.cpp Normal file
View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 Nikhil Tanwar (2002nikhiltanwar@gmail.com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
* NON-INFRINGEMENT. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "gtest/gtest.h"
#include "../include/tools.h"
namespace
{
TEST(LanguageToolsTest, englishTest)
{
EXPECT_EQ(kiwix::getLanguageSelfName("eng"), "English");
}
TEST(LanguageToolsTest, manualValuesTest)
{
EXPECT_EQ(kiwix::getLanguageSelfName("dty"), "डोटेली");
}
TEST(LanguageToolsTest, emptyStringTest)
{
EXPECT_EQ(kiwix::getLanguageSelfName(""), "Undetermined");
}
}

View File

@@ -218,7 +218,7 @@ const char sampleLibraryXML[] = R"(
creator="Wikibooks"
publisher="Kiwix & Some Enthusiasts"
date="2021-04-11"
name="wikibooks_de"
name="wikibooks.de"
tags="unittest;wikibooks;_category:wikibooks"
articleCount="12"
mediaCount="0"
@@ -238,14 +238,14 @@ typedef std::vector<std::string> Langs;
TEST(LibraryOpdsImportTest, allInOne)
{
kiwix::Library lib;
kiwix::Manager manager(&lib);
auto lib = kiwix::Library::create();
kiwix::Manager manager(lib);
manager.readOpds(sampleOpdsStream, "library-opds-import.unittests.dev");
EXPECT_EQ(10U, lib.getBookCount(true, true));
EXPECT_EQ(10U, lib->getBookCount(true, true));
{
const kiwix::Book& book1 = lib.getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd");
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");
@@ -271,7 +271,7 @@ TEST(LibraryOpdsImportTest, allInOne)
}
{
const kiwix::Book& book2 = lib.getBookById("0189d9be-2fd0-b4b6-7300-20fab0b5cdc8");
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(), "");
@@ -301,8 +301,10 @@ class LibraryTest : public ::testing::Test {
typedef kiwix::Library::BookIdCollection BookIdCollection;
typedef std::vector<std::string> TitleCollection;
LibraryTest(): lib(kiwix::Library::create()) {}
void SetUp() override {
kiwix::Manager manager(&lib);
kiwix::Manager manager(lib);
manager.readOpds(sampleOpdsStream, "foo.urlHost");
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
}
@@ -316,25 +318,25 @@ class LibraryTest : public ::testing::Test {
TitleCollection ids2Titles(const BookIdCollection& ids) {
TitleCollection titles;
for ( const auto& bookId : ids ) {
titles.push_back(lib.getBookById(bookId).getTitle());
titles.push_back(lib->getBookById(bookId).getTitle());
}
std::sort(titles.begin(), titles.end());
return titles;
}
kiwix::Library lib;
std::shared_ptr<kiwix::Library> lib;
};
TEST_F(LibraryTest, getBookMarksTest)
{
auto bookId1 = lib.getBooksIds()[0];
auto bookId2 = lib.getBooksIds()[1];
auto bookId1 = lib->getBooksIds()[0];
auto bookId2 = lib->getBooksIds()[1];
lib.addBookmark(createBookmark(bookId1));
lib.addBookmark(createBookmark("invalid-bookmark-id"));
lib.addBookmark(createBookmark(bookId2));
auto onlyValidBookmarks = lib.getBookmarks();
auto allBookmarks = lib.getBookmarks(false);
lib->addBookmark(createBookmark(bookId1));
lib->addBookmark(createBookmark("invalid-bookmark-id"));
lib->addBookmark(createBookmark(bookId2));
auto onlyValidBookmarks = lib->getBookmarks();
auto allBookmarks = lib->getBookmarks(false);
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
@@ -346,11 +348,11 @@ TEST_F(LibraryTest, getBookMarksTest)
TEST_F(LibraryTest, sanityCheck)
{
EXPECT_EQ(lib.getBookCount(true, true), 12U);
EXPECT_EQ(lib.getBooksLanguages(),
EXPECT_EQ(lib->getBookCount(true, true), 12U);
EXPECT_EQ(lib->getBooksLanguages(),
std::vector<std::string>({"deu", "eng", "fra", "ita", "spa"})
);
EXPECT_EQ(lib.getBooksCreators(), std::vector<std::string>({
EXPECT_EQ(lib->getBooksCreators(), std::vector<std::string>({
"Islam Stack Exchange",
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
@@ -361,7 +363,7 @@ TEST_F(LibraryTest, sanityCheck)
"Wikipedia",
"Wikiquote"
}));
EXPECT_EQ(lib.getBooksPublishers(), std::vector<std::string>({
EXPECT_EQ(lib->getBooksPublishers(), std::vector<std::string>({
"",
"Kiwix",
"Kiwix & Some Enthusiasts",
@@ -371,22 +373,22 @@ TEST_F(LibraryTest, sanityCheck)
TEST_F(LibraryTest, categoryHandling)
{
EXPECT_EQ("", lib.getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd").getCategory());
EXPECT_EQ("category_defined_via_tags_only", lib.getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2").getCategory());
EXPECT_EQ("category_defined_via_category_element_only", lib.getBookById("0ea1cde6-441d-6c58-f2c7-21c2838e659f").getCategory());
EXPECT_EQ("category_element_overrides_tags", lib.getBookById("1123e574-6eef-6d54-28fc-13e4caeae474").getCategory());
EXPECT_EQ("category_element_overrides_tags", lib.getBookById("14829621-c490-c376-0792-9de558b57efa").getCategory());
EXPECT_EQ("", lib->getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd").getCategory());
EXPECT_EQ("category_defined_via_tags_only", lib->getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2").getCategory());
EXPECT_EQ("category_defined_via_category_element_only", lib->getBookById("0ea1cde6-441d-6c58-f2c7-21c2838e659f").getCategory());
EXPECT_EQ("category_element_overrides_tags", lib->getBookById("1123e574-6eef-6d54-28fc-13e4caeae474").getCategory());
EXPECT_EQ("category_element_overrides_tags", lib->getBookById("14829621-c490-c376-0792-9de558b57efa").getCategory());
}
TEST_F(LibraryTest, emptyFilter)
{
const auto bookIds = lib.filter(kiwix::Filter());
EXPECT_EQ(bookIds, lib.getBooksIds());
const auto bookIds = lib->filter(kiwix::Filter());
EXPECT_EQ(bookIds, lib->getBooksIds());
}
#define EXPECT_FILTER_RESULTS(f, ...) \
EXPECT_EQ( \
ids2Titles(lib.filter(f)), \
ids2Titles(lib->filter(f)), \
TitleCollection({ __VA_ARGS__ }) \
)
@@ -678,17 +680,38 @@ TEST_F(LibraryTest, filterByPublisher)
TEST_F(LibraryTest, filterByName)
{
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikibooks_de"),
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikibooks.de"),
"An example ZIM archive"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("name:wikibooks_de"),
"An example ZIM archive"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("wikibooks_de"),
// Parsing the query with `name:` prefix splits the token on the dot, as if it was 2 sentences.
// It creates a query "XNwikibook@1 PHRASE 2 XNde@2".
// I haven't found the syntax to not split on dot.
EXPECT_FILTER_RESULTS(kiwix::Filter().query("name:wikibooks.de"),
/* no results */
);
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikibooks"),
/* no results */
);
// Wikibooks is in `tags` so it matches.
EXPECT_FILTER_RESULTS(kiwix::Filter().query("wikibooks"),
"An example ZIM archive"
);
// But "wikibooks.de" is only in name and `query` doesn't looks in name.
EXPECT_FILTER_RESULTS(kiwix::Filter().query("wikibooks.de"),
/* no results */
);
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikipedia_en_ray_charles"),
"Ray Charles"
);
EXPECT_FILTER_RESULTS(kiwix::Filter().query("name:wikipedia_en_ray_charles"),
"Ray Charles"
);
}
TEST_F(LibraryTest, filterByCategory)
@@ -736,33 +759,33 @@ TEST_F(LibraryTest, filterByMultipleCriteria)
TEST_F(LibraryTest, getBookByPath)
{
kiwix::Book book = lib.getBookById(lib.getBooksIds()[0]);
kiwix::Book book = lib->getBookById(lib->getBooksIds()[0]);
#ifdef _WIN32
auto path = "C:\\some\\abs\\path.zim";
#else
auto path = "/some/abs/path.zim";
#endif
book.setPath(path);
lib.addBook(book);
EXPECT_EQ(lib.getBookByPath(path).getId(), book.getId());
EXPECT_THROW(lib.getBookByPath("non/existant/path.zim"), std::out_of_range);
lib->addBook(book);
EXPECT_EQ(lib->getBookByPath(path).getId(), book.getId());
EXPECT_THROW(lib->getBookByPath("non/existant/path.zim"), std::out_of_range);
}
TEST_F(LibraryTest, removeBookByIdRemovesTheBook)
{
const auto initialBookCount = lib.getBookCount(true, true);
const auto initialBookCount = lib->getBookCount(true, true);
ASSERT_GT(initialBookCount, 0U);
EXPECT_NO_THROW(lib.getBookById("raycharles"));
lib.removeBookById("raycharles");
EXPECT_EQ(initialBookCount - 1, lib.getBookCount(true, true));
EXPECT_THROW(lib.getBookById("raycharles"), std::out_of_range);
EXPECT_NO_THROW(lib->getBookById("raycharles"));
lib->removeBookById("raycharles");
EXPECT_EQ(initialBookCount - 1, lib->getBookCount(true, true));
EXPECT_THROW(lib->getBookById("raycharles"), std::out_of_range);
};
TEST_F(LibraryTest, removeBookByIdDropsTheReader)
{
EXPECT_NE(nullptr, lib.getArchiveById("raycharles"));
lib.removeBookById("raycharles");
EXPECT_THROW(lib.getArchiveById("raycharles"), std::out_of_range);
EXPECT_NE(nullptr, lib->getArchiveById("raycharles"));
lib->removeBookById("raycharles");
EXPECT_THROW(lib->getArchiveById("raycharles"), std::out_of_range);
};
TEST_F(LibraryTest, removeBookByIdUpdatesTheSearchDB)
@@ -770,17 +793,17 @@ TEST_F(LibraryTest, removeBookByIdUpdatesTheSearchDB)
kiwix::Filter f;
f.local(true).valid(true).query(R"(title:"ray charles")", false);
EXPECT_NO_THROW(lib.getBookById("raycharles"));
EXPECT_EQ(1U, lib.filter(f).size());
EXPECT_NO_THROW(lib->getBookById("raycharles"));
EXPECT_EQ(1U, lib->filter(f).size());
lib.removeBookById("raycharles");
lib->removeBookById("raycharles");
EXPECT_THROW(lib.getBookById("raycharles"), std::out_of_range);
EXPECT_EQ(0U, lib.filter(f).size());
EXPECT_THROW(lib->getBookById("raycharles"), std::out_of_range);
EXPECT_EQ(0U, lib->filter(f).size());
// make sure that Library::filter() doesn't add an empty book with
// an id surviving in the search DB
EXPECT_THROW(lib.getBookById("raycharles"), std::out_of_range);
EXPECT_THROW(lib->getBookById("raycharles"), std::out_of_range);
};
TEST_F(LibraryTest, removeBooksNotUpdatedSince)
@@ -800,18 +823,18 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
"Wikiquote"
);
const uint64_t rev = lib.getRevision();
for ( const auto& id : lib.filter(kiwix::Filter().query("exchange")) ) {
lib.addBook(lib.getBookByIdThreadSafe(id));
const uint64_t rev = lib->getRevision();
for ( const auto& id : lib->filter(kiwix::Filter().query("exchange")) ) {
lib->addBook(lib->getBookByIdThreadSafe(id));
}
EXPECT_GT(lib.getRevision(), rev);
EXPECT_GT(lib->getRevision(), rev);
const uint64_t rev2 = lib.getRevision();
const uint64_t rev2 = lib->getRevision();
EXPECT_EQ(9u, lib.removeBooksNotUpdatedSince(rev));
EXPECT_EQ(9u, lib->removeBooksNotUpdatedSince(rev));
EXPECT_GT(lib.getRevision(), rev2);
EXPECT_GT(lib->getRevision(), rev2);
EXPECT_FILTER_RESULTS(kiwix::Filter(),
"Islam Stack Exchange",

View File

@@ -301,20 +301,41 @@ TEST_F(LibraryServerTest, catalog_search_by_tag)
TEST_F(LibraryServerTest, catalog_search_by_category)
{
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/search?category=jazz");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (category=jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
{
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/search?category=jazz");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (category=jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/search?category=jazz,wikipedia");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (category=jazz%2Cwikipedia)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CATALOG_LINK_TAGS
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_search_by_language)
@@ -793,6 +814,40 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_language)
}
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_category)
{
{
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?category=jazz");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?category=jazz")
" <title>Filtered Entries (category=jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?category=jazz,wikipedia");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?category=jazz%2Cwikipedia")
" <title>Filtered Entries (category=jazz%2Cwikipedia)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_v2_entries_multiple_filters)
{
{
@@ -973,7 +1028,12 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" <title>Welcome to Kiwix Server</title>\n" \
" <link\n" \
" type=\"text/css\"\n" \
" href=\"/ROOT%23%3F/skin/index.css?cacheid=e4d76d16\"\n" \
" href=\"/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9\"\n" \
" rel=\"Stylesheet\"\n" \
" />\n" \
" <link\n" \
" type=\"text/css\"\n" \
" href=\"/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf\"\n" \
" rel=\"Stylesheet\"\n" \
" />\n" \
" <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3\">\n" \
@@ -1090,7 +1150,7 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
" </div>\n" \
" <div id=\"kiwixfooter\" class=\"kiwixfooter\">Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a></div>\n" \
" </body>\n" \
"</html>"
"</html>\n"
#define FILTERS_HTML(SELECTED_ENG) \
" <div class=\"kiwixNav__filters\">\n" \

View File

@@ -8,18 +8,18 @@
TEST(ManagerTest, addBookFromPathAndGetIdTest)
{
kiwix::Library lib;
kiwix::Manager manager = kiwix::Manager(&lib);
auto lib = kiwix::Library::create();
kiwix::Manager manager = kiwix::Manager(lib);
auto bookId = manager.addBookFromPathAndGetId("./test/example.zim");
ASSERT_NE(bookId, "");
kiwix::Book book = lib.getBookById(bookId);
kiwix::Book book = lib->getBookById(bookId);
EXPECT_EQ(book.getPath(), kiwix::computeAbsolutePath("", "./test/example.zim"));
const std::string pathToSave = "./pathToSave";
const std::string url = "url";
bookId = manager.addBookFromPathAndGetId("./test/example.zim", pathToSave, url, true);
book = lib.getBookById(bookId);
book = lib->getBookById(bookId);
auto savedPath = kiwix::computeAbsolutePath(kiwix::removeLastPathElement(manager.writableLibraryPath), pathToSave);
EXPECT_EQ(book.getPath(), savedPath);
EXPECT_EQ(book.getUrl(), url);
@@ -48,11 +48,11 @@ const char sampleLibraryXML[] = R"(
TEST(ManagerTest, readXml)
{
kiwix::Library lib;
kiwix::Manager manager = kiwix::Manager(&lib);
auto lib = kiwix::Library::create();
kiwix::Manager manager = kiwix::Manager(lib);
EXPECT_EQ(true, manager.readXml(sampleLibraryXML, true, "/data/lib.xml", true));
kiwix::Book book = lib.getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2");
kiwix::Book book = lib->getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2");
EXPECT_EQ("/data/zimfiles/unittest.zim", book.getPath());
EXPECT_EQ("https://example.com/zimfiles/unittest.zim", book.getUrl());
EXPECT_EQ("Unit Test", book.getTitle());
@@ -70,24 +70,24 @@ TEST(ManagerTest, readXml)
TEST(Manager, reload)
{
kiwix::Library lib;
kiwix::Manager manager(&lib);
auto lib = kiwix::Library::create();
kiwix::Manager manager(lib);
manager.reload({ "./test/library.xml" });
EXPECT_EQ(lib.getBooksIds(), (kiwix::Library::BookIdCollection{
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
"charlesray",
"raycharles",
"raycharles_uncategorized"
}));
lib.removeBookById("raycharles");
EXPECT_EQ(lib.getBooksIds(), (kiwix::Library::BookIdCollection{
lib->removeBookById("raycharles");
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
"charlesray",
"raycharles_uncategorized"
}));
manager.reload({ "./test/library.xml" });
EXPECT_EQ(lib.getBooksIds(), kiwix::Library::BookIdCollection({
EXPECT_EQ(lib->getBooksIds(), kiwix::Library::BookIdCollection({
"charlesray",
"raycharles",
"raycharles_uncategorized"

View File

@@ -5,13 +5,17 @@ tests = [
'stringTools',
'pathTools',
'otherTools',
'opdsParsingTools',
'languageTools',
'kiwixserve',
'book',
'manager',
'name_mapper',
'opds_catalog',
'server_helper',
'lrucache'
'lrucache',
'i18n',
'response'
]
if build_machine.system() != 'windows'

View File

@@ -18,18 +18,20 @@ const char libraryXML[] = R"(
)";
class NameMapperTest : public ::testing::Test {
public:
NameMapperTest(): lib(kiwix::Library::create()) {}
protected:
void SetUp() override {
kiwix::Manager manager(&lib);
kiwix::Manager manager(lib);
manager.readXml(libraryXML, false, "./library.xml", true);
for ( const std::string& id : lib.getBooksIds() ) {
kiwix::Book bookCopy = lib.getBookById(id);
for ( const std::string& id : lib->getBooksIds() ) {
kiwix::Book bookCopy = lib->getBookById(id);
bookCopy.setPathValid(true);
lib.addBook(bookCopy);
lib->addBook(bookCopy);
}
}
kiwix::Library lib;
std::shared_ptr<kiwix::Library> lib;
};
class CapturedStderr
@@ -73,13 +75,13 @@ void checkUnaliasedEntriesInNameMapper(const kiwix::NameMapper& nm)
TEST_F(NameMapperTest, HumanReadableNameMapperWithoutAliases)
{
CapturedStderr stderror;
kiwix::HumanReadableNameMapper nm(lib, false);
kiwix::HumanReadableNameMapper nm(*lib, false);
EXPECT_EQ("", std::string(stderror));
checkUnaliasedEntriesInNameMapper(nm);
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
lib.removeBookById("04-2021-10");
lib->removeBookById("04-2021-10");
EXPECT_EQ("zero_four_2021-10", nm.getNameForId("04-2021-10"));
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four_2021-10"));
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
@@ -88,7 +90,7 @@ TEST_F(NameMapperTest, HumanReadableNameMapperWithoutAliases)
TEST_F(NameMapperTest, HumanReadableNameMapperWithAliases)
{
CapturedStderr stderror;
kiwix::HumanReadableNameMapper nm(lib, true);
kiwix::HumanReadableNameMapper nm(*lib, true);
EXPECT_EQ(
"Path collision: /data/zero_four_2021-10.zim and"
" /data/zero_four_2021-11.zim can't share the same URL path 'zero_four'."
@@ -99,7 +101,7 @@ TEST_F(NameMapperTest, HumanReadableNameMapperWithAliases)
checkUnaliasedEntriesInNameMapper(nm);
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four"));
lib.removeBookById("04-2021-10");
lib->removeBookById("04-2021-10");
EXPECT_EQ("zero_four_2021-10", nm.getNameForId("04-2021-10"));
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four_2021-10"));
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four"));
@@ -114,7 +116,7 @@ TEST_F(NameMapperTest, UpdatableNameMapperWithoutAliases)
checkUnaliasedEntriesInNameMapper(nm);
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
lib.removeBookById("04-2021-10");
lib->removeBookById("04-2021-10");
nm.update();
EXPECT_THROW(nm.getNameForId("04-2021-10"), std::out_of_range);
EXPECT_THROW(nm.getIdForName("zero_four_2021-10"), std::out_of_range);
@@ -137,7 +139,7 @@ TEST_F(NameMapperTest, UpdatableNameMapperWithAliases)
{
CapturedStderr nmUpdateStderror;
lib.removeBookById("04-2021-10");
lib->removeBookById("04-2021-10");
nm.update();
EXPECT_EQ("", std::string(nmUpdateStderror));
}

131
test/opdsParsingTools.cpp Normal file
View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2023 Nikhil Tanwar (2002nikhiltanwar@gmail.com)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
* NON-INFRINGEMENT. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "gtest/gtest.h"
#include "../include/tools.h"
typedef kiwix::FeedLanguages FeedLanguages;
typedef kiwix::FeedCategories FeedCategories;
namespace
{
const char sampleLanguageOpdsStream[] = R"(
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
xmlns:opds="https://specs.opds.io/opds-1.2"
xmlns:thr="http://purl.org/syndication/thread/1.0">
<id>1e587935-0f7b-dad6-eddc-ef3fafd4c3ed</id>
<link rel="self"
href="/catalog/v2/languages"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of languages</title>
<updated>2023-07-11T15:35:09Z</updated>
<entry>
<title>Abkhazian</title>
<dc:language>abk</dc:language>
<thr:count>3</thr:count>
<link rel="subsection"
href="/catalog/v2/entries?lang=abk"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>2023-07-11T15:35:09Z</updated>
<id>2e4d9a1c-9750-0418-8124-a0c663e206f7</id>
</entry>
<entry>
<title>isiZulu</title>
<dc:language>zul</dc:language>
<thr:count>4</thr:count>
<link rel="subsection"
href="/catalog/v2/entries?lang=zul"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>2023-07-11T15:35:09Z</updated>
<id>76eec223-994d-9b95-e309-baee06e585b0</id>
</entry>
</feed>
)";
const char sampleCategoriesOpdsStream[] = R"(
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2">
<id>231da20c-0fe0-7345-11b2-d29f50364108</id>
<link rel="self"
href="/catalog/v2/categories"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of categories</title>
<updated>2023-07-11T15:35:09Z</updated>
<entry>
<title>gutenberg</title>
<link rel="subsection"
href="/catalog/v2/entries?category=gutenberg"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>2023-07-11T15:35:09Z</updated>
<id>401dbe68-2f7a-5503-b431-054801c30bab</id>
<content type="text">All entries with category of 'gutenberg'.</content>
</entry>
<entry>
<title>iFixit</title>
<link rel="subsection"
href="/catalog/v2/entries?category=iFixit"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>2023-07-11T15:35:09Z</updated>
<id>c18e5459-af23-5fbf-0622-ff271bd9a5ad</id>
<content type="text">All entries with category of 'iFixit'.</content>
</entry>
<entry>
<title>wikivoyage</title>
<link rel="subsection"
href="/catalog/v2/entries?category=wikivoyage"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>2023-07-11T15:35:09Z</updated>
<id>9a75be6c-7a35-6f52-1a69-bee9ad248459</id>
<content type="text">All entries with category of 'wikivoyage'.</content>
</entry>
<entry>
<title>wiktionary</title>
<link rel="subsection"
href="/catalog/v2/entries?category=wiktionary"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>2023-07-11T15:35:09Z</updated>
<id>7adb9f1a-73d7-0391-1238-d2e2c300ddaa</id>
<content type="text">All entries with category of 'wiktionary'.</content>
</entry>
</feed>
)";
TEST(OpdsParsingTest, languageTest)
{
FeedLanguages expectedLanguagesFromFeed = {{"abk", "Abkhazian"}, {"zul", "isiZulu"}};
EXPECT_EQ(kiwix::readLanguagesFromFeed(sampleLanguageOpdsStream), expectedLanguagesFromFeed);
}
TEST(OpdsParsingTest, categoryTest)
{
FeedCategories expectedCategoriesFromFeed = {"gutenberg", "iFixit", "wikivoyage", "wiktionary"};
EXPECT_EQ(kiwix::readCategoriesFromFeed(sampleCategoriesOpdsStream), expectedCategoriesFromFeed);
}
}

View File

@@ -100,7 +100,7 @@ TEST(Suggestions, specialCharHandling)
{
// HTML special symbols (<, >, &, ", and ') must be HTML-escaped
// Backslash symbols (\) must be duplicated.
const std::string SYMBOLS(R"(\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?)");
const std::string SYMBOLS("\t\n\r" R"(\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?)");
{
kiwix::Suggestions s;
s.add(zim::SuggestionItem("Title with " + SYMBOLS,
@@ -110,10 +110,10 @@ TEST(Suggestions, specialCharHandling)
CHECK_SUGGESTIONS(s.getJSON(),
R"EXPECTEDJSON([
{
"value" : "Title with \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippet with \\&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 \\&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 \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.?",
"label" : "Snippetless title with \\&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 \\&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 \\&lt;&gt;&amp;&apos;&quot;~!@#$%^*()_+`-=[]{}|:;,.? ",
"label" : "containing &apos;text with \\&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

@@ -54,26 +54,28 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/viewer" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/viewer?cacheid=whatever" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete.min.js?cacheid=1191aaaf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/css/autoComplete.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/css/autoComplete.css?cacheid=08951e06" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf" },
{ 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=2cf0f8c5" },
{ 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=e4d76d16" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=07b06fca" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=ce19da2a" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js?cacheid=bd23c4fb" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=bbdaf425" },
{ 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=bb748367" },
{ 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" },
@@ -81,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=9ccd43fd" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
@@ -146,8 +150,6 @@ const ResourceCollection resources200Uncompressible{
{ 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=648526e1" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Description" },
@@ -274,7 +276,8 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
const std::vector<UrlAndExpectedResult> testData{
{
/* url */ "/ROOT%23%3F/",
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=e4d76d16"
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
href="/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf"
<link rel="apple-touch-icon" sizes="180x180" href="/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3">
<link rel="icon" type="image/png" sizes="32x32" href="/ROOT%23%3F/skin/favicon/favicon-32x32.png?cacheid=79ded625">
<link rel="icon" type="image/png" sizes="16x16" href="/ROOT%23%3F/skin/favicon/favicon-16x16.png?cacheid=a986fedc">
@@ -282,15 +285,19 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=e4d76d16"
<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">
src: url("/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837") format("truetype");
src: url("/ROOT%23%3F/skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=2cf0f8c5" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=648526e1" defer></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=9ccd43fd" 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=07b06fca" defer></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=ce19da2a" defer></script>
<img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f"
<img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961"
)EXPECTEDRESULT"
},
{
/* url */ "/ROOT%23%3F/skin/kiwix.css",
R"EXPECTEDRESULT( src: url("../skin/fonts/Poppins.ttf?cacheid=af705837") format("truetype");
src: url("../skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
)EXPECTEDRESULT"
},
{
@@ -308,15 +315,16 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download
},
{
/* url */ "/ROOT%23%3F/viewer",
R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=bbdaf425" rel="Stylesheet" />
<link type="text/css" href="./skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
<script type="module" src="./skin/i18n.js?cacheid=2cf0f8c5" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=648526e1" defer></script>
<script type="text/javascript" src="./skin/viewer.js?cacheid=bb748367" defer></script>
<script type="text/javascript" src="./skin/autoComplete.min.js?cacheid=1191aaaf"></script>
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=071abc9a" defer></script>
<script type="text/javascript" src="./skin/languages.js?cacheid=9ccd43fd" 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";
<img src="./skin/langSelector.svg?cacheid=00b59961">
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
src="./skin/langSelector.svg?cacheid=00b59961">
src="./skin/blank.html?cacheid=6b1fa032" title="ZIM content" width="100%"
)EXPECTEDRESULT"
},
@@ -329,6 +337,7 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=bbda
// 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"
},
};
@@ -371,7 +380,7 @@ const char* urls404[] = {
"/ROOT%23%3",
"/ROOT%23%3Fxyz",
"/ROOT%23%3F/skin/non-existent-skin-resource",
"/ROOT%23%3F/skin/autoComplete.min.js?cacheid=wrongcacheid",
"/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=wrongcacheid",
"/ROOT%23%3F/catalog",
"/ROOT%23%3F/catalog/",
"/ROOT%23%3F/catalog/non-existent-item",
@@ -527,6 +536,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;
@@ -536,6 +546,7 @@ enum ExpectedResponseDataType
{
expected_page_title,
expected_css_url,
expected_kiwix_response_data,
book_name,
book_title,
expected_body
@@ -548,11 +559,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{};
}
}
@@ -571,6 +584,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)
@@ -599,19 +613,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"
@@ -622,8 +646,10 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
+ frag[1]
+ pageCssLink()
+ frag[2]
+ expectedKiwixResponseData
+ frag[3]
+ expectedBody
+ frag[3];
+ frag[4];
}
std::string TestContentIn404HtmlResponse::pageTitle() const
@@ -640,7 +666,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
@@ -668,6 +695,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>
@@ -677,6 +705,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>
@@ -685,6 +714,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>
@@ -693,6 +723,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>
@@ -702,6 +733,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>
@@ -710,6 +742,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>
@@ -719,6 +752,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>
@@ -727,6 +761,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>
@@ -740,6 +775,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>
@@ -751,6 +787,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>
@@ -764,6 +801,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>
@@ -774,10 +812,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>
@@ -789,6 +844,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>
@@ -800,6 +856,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>
@@ -811,6 +868,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>
@@ -822,6 +880,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>
@@ -837,6 +896,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>
@@ -858,6 +918,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>
@@ -868,6 +929,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>
@@ -878,6 +940,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>
@@ -888,6 +951,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>
@@ -900,6 +964,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>
@@ -910,6 +975,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>
@@ -919,6 +985,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 ) {
@@ -1015,7 +1095,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>
@@ -1033,6 +1116,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");
}
}
@@ -1042,82 +1126,174 @@ 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": 12
},
{
"Čeština": "cs"
"iso_code": "cs",
"self_name": "Čeština",
"translation_count": 25
},
{
"Deutsch": "de"
"iso_code": "de",
"self_name": "Deutsch",
"translation_count": 49
},
{
"English": "en"
"iso_code": "en",
"self_name": "English",
"translation_count": 58
},
{
"suomi": "fi"
"iso_code": "es",
"self_name": "español",
"translation_count": 48
},
{
"français": "fr"
"iso_code": "fi",
"self_name": "suomi",
"translation_count": 22
},
{
"עברית": "he"
"iso_code": "fr",
"self_name": "Français",
"translation_count": 52
},
{
"Հայերեն": "hy"
"iso_code": "he",
"self_name": "עברית",
"translation_count": 52
},
{
"interlingua": "ia"
"iso_code": "hi",
"self_name": "हिन्दी",
"translation_count": 49
},
{
"italiano": "it"
"iso_code": "hy",
"self_name": "Հայերեն",
"translation_count": 15
},
{
"日本語": "ja"
"iso_code": "ia",
"self_name": "interlingua",
"translation_count": 49
},
{
"한국어": "ko"
"iso_code": "it",
"self_name": "italiano",
"translation_count": 29
},
{
"kurdî": "ku-latn"
"iso_code": "ja",
"self_name": "日本語",
"translation_count": 26
},
{
"Lëtzebuergesch": "lb"
"iso_code": "ko",
"self_name": "한국어",
"translation_count": 13
},
{
"македонски": "mk"
"iso_code": "ku-latn",
"self_name": "kurdî",
"translation_count": 26
},
{
"ߒߞߏ": "nqo"
"iso_code": "lb",
"self_name": "Lëtzebuergesch",
"translation_count": 22
},
{
"Polski": "pl"
"iso_code": "mk",
"self_name": "македонски",
"translation_count": 52
},
{
"русский": "ru"
"iso_code": "ms",
"self_name": "Bahasa Melayu",
"translation_count": 14
},
{
"Sardu": "sc"
"iso_code": "nl",
"self_name": "Nederlands",
"translation_count": 49
},
{
"slovenčina": "sk"
"iso_code": "nqo",
"self_name": "ߒߞߏ",
"translation_count": 43
},
{
"slovenščina": "sl"
"iso_code": "or",
"self_name": "ଓଡ଼ିଆ",
"translation_count": 49
},
{
"Svenska": "sv"
"iso_code": "pl",
"self_name": "Polski",
"translation_count": 24
},
{
"Türkçe": "tr"
"iso_code": "ru",
"self_name": "русский",
"translation_count": 45
},
{
"英语": "zh-hans"
"iso_code": "sc",
"self_name": "Sardu",
"translation_count": 49
},
{
"繁體中文": "zh-hant"
"iso_code": "sk",
"self_name": "slovenčina",
"translation_count": 25
},
{
"iso_code": "skr-arab",
"self_name": "سرائیکی",
"translation_count": 20
},
{
"iso_code": "sl",
"self_name": "slovenščina",
"translation_count": 52
},
{
"iso_code": "sq",
"self_name": "Shqip",
"translation_count": 49
},
{
"iso_code": "sv",
"self_name": "Svenska",
"translation_count": 52
},
{
"iso_code": "te",
"self_name": "ఇంగ్లీషు",
"translation_count": 49
},
{
"iso_code": "tr",
"self_name": "Türkçe",
"translation_count": 25
},
{
"iso_code": "zh-hans",
"self_name": "英语",
"translation_count": 16
},
{
"iso_code": "zh-hant",
"self_name": "繁體中文",
"translation_count": 52
}
])EXPECTEDRESPONSE");
}
@@ -1129,8 +1305,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 char* const responseSetCookie; // Set-Cookie: header of the response
const std::string expectedH1;
operator TestContext() const
@@ -1141,119 +1315,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,
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
/* 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,
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
/* 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,
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT%23%3F;Max-Age=31536000",
/* 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,
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
/* expected <h1> */ "Not Found"
},
{
"Accept-Language: header is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test",
/*Request Cookie:*/ NO_COOKIE,
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT%23%3F;Max-Age=31536000",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"userlang cookie is respected",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/*Request Cookie:*/ "userlang=test",
/*Response Set-Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"userlang cookie is correctly parsed",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/*Request Cookie:*/ "anothercookie=123; userlang=test",
/*Response Set-Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"userlang cookie is correctly parsed",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/*Request Cookie:*/ "userlang=test; anothercookie=abc",
/*Response Set-Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"userlang cookie is correctly parsed",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/*Request Cookie:*/ "cookie1=abc; userlang=test; cookie2=xyz",
/*Response Set-Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"Multiple userlang cookies are not a problem",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "",
/*Request Cookie:*/ "cookie1=abc; userlang=en; userlang=test; cookie2=xyz",
/*Response Set-Cookie:*/ NO_COOKIE,
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
"userlang query parameter takes precedence over Accept-Language",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "test",
/*Request Cookie:*/ NO_COOKIE,
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
/* expected <h1> */ "Not Found"
},
{
"userlang query parameter takes precedence over its cookie counterpart",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
/*Accept-Language:*/ "",
/*Request Cookie:*/ "userlang=test",
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
/* expected <h1> */ "Not Found"
},
{
"userlang in cookies takes precedence over Accept-Language",
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
/*Accept-Language:*/ "test",
/*Request Cookie:*/ "userlang=en",
/*Response Set-Cookie:*/ NO_COOKIE,
/* expected <h1> */ "Not Found"
},
{
@@ -1262,8 +1362,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,
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT%23%3F;Max-Age=31536000",
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
},
{
@@ -1272,8 +1370,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,
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
/* expected <h1> */ "Not Found"
},
};
@@ -1285,16 +1381,8 @@ TEST_F(ServerTest, UserLanguageControl)
if ( !t.acceptLanguageHeader.empty() ) {
headers.insert({"Accept-Language", t.acceptLanguageHeader});
}
if ( t.requestCookie ) {
headers.insert({"Cookie", t.requestCookie});
}
const auto r = zfs1_->GET(t.url.c_str(), headers);
if ( t.responseSetCookie ) {
ASSERT_TRUE(r->has_header("Set-Cookie")) << t;
EXPECT_EQ(t.responseSetCookie, getHeaderValue(r->headers, "Set-Cookie")) << t;
} else {
EXPECT_FALSE(r->has_header("Set-Cookie"));
}
EXPECT_FALSE(r->has_header("Set-Cookie"));
std::regex_search(r->body, h1Match, h1Regex);
const std::string h1(h1Match[1]);
EXPECT_EQ(h1, t.expectedH1) << t;

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

@@ -98,16 +98,17 @@ private:
void run(int serverPort, std::string indexTemplateString = "");
private: // data
kiwix::Library library;
std::shared_ptr<kiwix::Library> library;
kiwix::Manager manager;
std::unique_ptr<kiwix::NameMapper> nameMapper;
std::shared_ptr<kiwix::NameMapper> nameMapper;
std::unique_ptr<kiwix::Server> server;
std::unique_ptr<httplib::Client> client;
const Cfg cfg;
};
ZimFileServer::ZimFileServer(int serverPort, Cfg _cfg, std::string libraryFilePath)
: manager(&this->library)
: library(kiwix::Library::create())
, manager(this->library)
, cfg(_cfg)
{
if ( kiwix::isRelativePath(libraryFilePath) )
@@ -120,7 +121,8 @@ ZimFileServer::ZimFileServer(int serverPort,
Cfg _cfg,
const FilePathCollection& zimpaths,
std::string indexTemplateString)
: manager(&this->library)
: library(kiwix::Library::create())
, manager(this->library)
, cfg(_cfg)
{
for ( const auto& zimpath : zimpaths ) {
@@ -136,9 +138,9 @@ void ZimFileServer::run(int serverPort, std::string indexTemplateString)
if (cfg.options & NO_NAME_MAPPER) {
nameMapper.reset(new kiwix::IdNameMapper());
} else {
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
nameMapper.reset(new kiwix::HumanReadableNameMapper(*library, false));
}
server.reset(new kiwix::Server(&library, nameMapper.get()));
server.reset(new kiwix::Server(library, nameMapper));
server->setRoot(cfg.root);
server->setAddress(address);
server->setPort(serverPort);
@@ -188,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")";