Compare commits

...

139 Commits

Author SHA1 Message Date
Veloman Yunkan
cd9785fe85 Enter uriEncode() 2023-01-25 23:45:18 +04:00
Veloman Yunkan
b7a019469c Unconditional URI-encoding in RequestContext::get_query<F>(F) 2023-01-25 23:41:52 +04:00
Matthieu Gautier
76dfc03751 Merge pull request #870 from kiwix/urlEncode_quickfix 2023-01-25 16:41:24 +01:00
Veloman Yunkan
ca079a72cc Some clean-up 2023-01-25 19:15:12 +04:00
Veloman Yunkan
471c5b89f4 Dropped the 2nd param of urlEncode()
`urlEncode(str)` is now equivalent to the previous `urlEncode(str, true)`.
2023-01-25 19:15:12 +04:00
Veloman Yunkan
3bf8211b70 Made 2nd param of urlEncode() mandatory
This is a precautionary step before dropping the said parameter.
2023-01-25 19:15:12 +04:00
Veloman Yunkan
ec81d5904d Proper URI-encoding in kiwix::getSearchUrl() 2023-01-25 19:15:12 +04:00
Veloman Yunkan
82dcba542a Demonstrating bugs of kiwix::getSearchUrl() 2023-01-25 19:15:12 +04:00
Veloman Yunkan
63e0d5c7c2 RequestContext::get_query() is fully URI-encoded 2023-01-25 19:15:12 +04:00
Veloman Yunkan
772243e832 Category name is fully URI-encoded 2023-01-25 19:15:12 +04:00
Veloman Yunkan
bad13d76b4 Removed unused code 2023-01-25 19:15:12 +04:00
Veloman Yunkan
0bde4d9412 Properly URI-encoded links in search results
Special URI symbols occurring in the item path part of the search result
link were NOT encoded, because that would also encode the path separator (/)
symbol. Now that `urlEncode()` never encodes the / symbol, it is safe to
encode all other URI-special symbols in the path.
2023-01-25 19:15:12 +04:00
Veloman Yunkan
239b108fa7 / is no longer a reserved char for urlEncode()
This change is a quick hack solving known issues with URI-encoding in
libkiwix.

This change removes the slash character from the list of URL separator
symbols in URL encoding/decoding utilities, and makes it a symbol that
is safe to leave unencoded.

Effects:

- `urlEncode()` never encodes the '/' symbol (even when it is requested
  to encode the URL separator symbols too).

- `urlDecode(str)`/`urlDecode(..., false)` will now decode %2F to '/';
  other encoded URL separator symbols are NOT decoded when the second
  argument of `urlDecode()` is set to false (which is the default).
2023-01-25 19:15:12 +04:00
Veloman Yunkan
c5ccbd37e2 Extracted isHarmlessUriChar() 2023-01-25 19:15:12 +04:00
Veloman Yunkan
822fb3748a Added a unit-test for urlDecode() 2023-01-25 19:15:12 +04:00
Veloman Yunkan
aa2e443eb8 Fixed indentation
Replaced tabs with spaces.
2023-01-25 19:15:11 +04:00
Veloman Yunkan
82d477009d '#' is a URI delimiter symbol 2023-01-25 19:15:11 +04:00
Veloman Yunkan
e49081da80 Fixed urlEncode() for chars below 0x10 2023-01-25 19:15:11 +04:00
Veloman Yunkan
07c7d3931d Added a unit-test of buggy urlEncode()
Added a unit-test for urlEncode() that passes for its current
implementation despite the two bugs that were revealed while creating
the unit-test.
2023-01-25 19:15:11 +04:00
Matthieu Gautier
cf59a93cf1 Merge pull request #869 from kiwix/userlang_cookie_fixes 2023-01-24 19:16:08 +01:00
Veloman Yunkan
e35e7585e0 Server sets userlang cookie as global and permanent
Without specifying the "Path" attribute of the cookie in the "Set-Cookie" header
we end up with multiple instances of the cookie for different URLs. We
want a single "global" cookie for kiwix-serve. Besides we want it to be
"permanent" rather than a session cookie, hence the large (1-year-long)
TTL value for the "Max-Age" attribute.
2023-01-24 19:01:32 +01:00
Veloman Yunkan
fcb97c3c06 Sparing use of "Set-Cookie: userlang=..." header
Server adds the "Set-Cookie: userlang=..." header to the response only
if the "userlang" cookie is not already present with the same value.
2023-01-24 19:01:32 +01:00
Veloman Yunkan
0edee4d066 Improved ServerTest.UserLanguageControl unittest
- Description of a test point was not updated in an earlier commit
  that added proper handling of the Accept-Language header. Also
  after enhancing the limited implementation it made sense to
  add another test point demonstrating that the most suitable language
  (rather than just the first one in the list) is selected.

- Now failures of the test case because of a missing Set-Cookie header
  are more informative.
2023-01-24 19:01:32 +01:00
Kelson
b9937e6859 Merge pull request #868 from adamlamar/windows-git-clone
Fix git clone on Windows
2023-01-19 08:44:36 +01:00
Adam Lamar
59012c50b4 Fix git clone on Windows
The question mark (?) is not a valid filename character on Windows.
Changing to a the pound sign (#) so that this repository can still be
cloned on Windows.
2023-01-18 23:01:14 +01:00
Matthieu Gautier
7a98878273 Merge pull request #866 from kiwix/uri_encoded_redirections 2023-01-10 15:06:18 +01:00
Veloman Yunkan
8eb527389e URI-encoding of redirections to URLs with special symbols 2023-01-10 17:41:59 +04:00
Veloman Yunkan
78b2c1a273 Testing of redirection to URLs with special symbols 2023-01-10 17:41:59 +04:00
Veloman Yunkan
497c0700b5 Fixed metadata options in create_corner_cases_zim_file
Specifying the = symbol with single-character options makes that
character included in the option value (e.g. -l=en results in the
language of the ZIM file being set to =en).
2023-01-10 17:41:59 +04:00
Veloman Yunkan
bac12010aa Updated create_corner_cases_zim_file script
Updated the create_corner_cases_zim_file to work with the latest (v3.1.3)
release of zimwriterfs.
2023-01-10 17:41:59 +04:00
Veloman Yunkan
dad33a850c Merge pull request #857 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2023-01-09 15:20:52 +04:00
Veloman Yunkan
0968fc98ee Added new translations to i18n_resources_list.txt 2023-01-09 15:04:51 +04:00
translatewiki.net
ff44d88f21 Localisation updates from https://translatewiki.net. 2023-01-05 13:10:37 +01:00
Matthieu Gautier
1e7baee9d7 Merge pull request #862 from kiwix/suggestion_link_fix 2023-01-03 11:07:14 +01:00
Veloman Yunkan
d9342acf5b Suggestion link points to /content endpoint
Directly pointing the suggestion link to a /content/... URL avoids
an unnecessary redirection by the server (and an associated bug
related to redirection of URLs with URI-encoded special symbols in
them that - in the current implementation - go into the target URL
in decoded form).
2023-01-03 10:57:59 +01:00
Kelson
b3f1ab6579 Merge pull request #863 from kiwix/update-workflows-new-default-branch
New git default branch is 'main'
2022-12-27 14:28:13 +01:00
Emmanuel Engelhart
f5c9b2404a New git default branch is 'main' 2022-12-27 14:27:43 +01:00
Kelson
8b1fe21e4e Delete move.yml 2022-12-27 14:25:28 +01:00
Kelson
815c59ff6d "main" is the new git default branch 2022-12-27 14:23:14 +01:00
Matthieu Gautier
90318dfb6b Merge pull request #860 from kiwix/handling_of_suggestion_links_with_single_quotes 2022-12-21 12:02:58 +01:00
Veloman Yunkan
f3d2f474a7 Handling of suggestions containing special symbols
This change fixes two issues:

1. Presence of URL-specific special symbols (such as ? or #) in the book
   and/or article name resulted in a wrong suggestion link. This is
   fixed by URI-encoding the book name and the path, too.

2. Presence of a single quote symbol in the book and/or article name
   resulted in invalid javascript code in the href attribute of the
   suggestion link.

   The single quote (') symbol is not URL-encoded (unlike its double quote
   counterpart). As a result, enclosing a URL-encoded string in single
   quotes may result in invalid javascript. Using double quotes instead is
   safe, since both double quote (") and backslash (\) symbols (which are
   the only special symbols for such quoting) undergo URL-encoding.
2022-12-17 18:39:17 +04:00
Veloman Yunkan
12140098e6 Extracted makeJSLink() 2022-12-15 18:53:32 +04:00
Veloman Yunkan
c7d8081e9a gotoUrl() takes URLs relative to root location 2022-12-15 18:21:22 +04:00
Matthieu Gautier
a10067e6b6 Merge pull request #849 from kiwix/backend_userlang_control 2022-12-14 15:39:31 +01:00
Veloman Yunkan
28e9fb48b6 Properly implemented parseUserLanguagePreferences() 2022-12-14 15:34:46 +01:00
Veloman Yunkan
634f3fcf14 Properly implemented selectMostSuitableLanguage() 2022-12-14 15:34:46 +01:00
Veloman Yunkan
88597e1834 Enter selectMostSuitableLanguage() 2022-12-14 15:34:46 +01:00
Veloman Yunkan
69b3e1f8a7 Moved user language preferences into i18n.{h,cpp} 2022-12-14 15:34:46 +01:00
Veloman Yunkan
669d8898ac Enter UserLangPreferences 2022-12-14 15:34:46 +01:00
Veloman Yunkan
14f0f79061 User language control via userlang cookie 2022-12-14 15:34:46 +01:00
Veloman Yunkan
600ff07986 Test descriptions in ServerTest.UserLanguageControl 2022-12-14 15:34:46 +01:00
Veloman Yunkan
1d74b5e311 Server sets the userlang cookie on every response 2022-12-14 15:34:46 +01:00
Veloman Yunkan
c0fe6f4aee Added cookies to ServerTest.UserLanguageControl 2022-12-14 15:34:46 +01:00
Matthieu Gautier
aa7053bbe8 Merge pull request #859 from kiwix/safe_href_in_suggestion_links 2022-12-14 15:31:56 +01:00
Veloman Yunkan
99f24eb598 Safe href in suggestion links 2022-12-12 17:15:46 +04:00
Kelson
6790a144a1 Merge pull request #856 from kiwix/compress-web-fonts
Gzip compress HTTP response for Web fonts
2022-12-08 14:36:32 +01:00
Emmanuel Engelhart
cd3d2110d9 Error if run_command() fails, remove meson warning 2022-12-08 13:03:33 +01:00
Emmanuel Engelhart
b404241d0b Fix font compression tests 2022-12-08 12:55:28 +01:00
Emmanuel Engelhart
2d42d6dc60 Gzip compress HTTP response for Web fonts 2022-12-07 19:21:27 +01:00
Matthieu Gautier
e65c9c41d8 Merge pull request #850 from kiwix/version_12.0.0 2022-11-30 18:10:19 +01:00
Matthieu Gautier
0ae31bd181 New version 12.0.0
* [API Break] Remove wrapper around libzim (@mgautierfr #789)
* Allow kiwix-serve to use custom resource files (@veloman-yunkan #779)
* Properly handle searchProtocolPrefix when rendering search result (@veloman-yunkan #823)
* Prevent search on multi language content (@veloman-yunkan #838)
* Use new `zim::Archive::getMediaCount` from libzim (@mgautierfr #836)
* Catalog:
 - Include tags in free text catalog search (@veloman-yunkan #802)
 - Illustration's url is based on book's uuid (@veloman-yunkan #804)
 - Cleanup of the opds-dumper (@veloman-yunkan #829)
 - Allow filtering of catalog content using multiple languages (@veloman-yunkan #841)
 - Make opds-dumper respect the namemapper (@mgautierfr #837)
* Server:
 - Correctly handle `\` in suggestion json generation (@veloman-yunkan #843)
 - Better http caching (@veloman-yunkan #833)
 - Make `/suggest` endpoint thread-safe (@veloman-yunkan #834)
 - Better redirection of main page (@veloman-yunkan #827)
 - Remove jquery (@mgautierfr @juuz0 #796)
 - Better Viewer of zim content :
   . Introduce `/content` endpoints (@veloman-yunkan #806)
   . Switch to iframe based content viewer (@veloman-yunkan #716)
 - Optimised design of the welcome page:
   . Alignement (@juuz0 @kelson42 #786)
   . Exit download modal on pressing escape key (@juzz0 #800)
   . Add favicon for different devices (@juzz0 #805)
   . Fix auto hidding of the toolbar (@veloman-yunkan #821)
   . Allow user to filter books by tags in the front page (@juuz0 #711)
* CI :
  - Trigger CI on pull_request (@kelson42 #791)
  - Drop Ubuntu Impish packaging (@legoktm #825)
  - Add Ubuntu Kinetic packaging (@legoktm #801)
* Testing:
  - Test ICULanguageInfo (@veloman-yunkan #795)
  - Introduce fake `test` language to test i18n (@veloman-yunkan #848)
* Fix documentation (@kelson42 #816)
* Udpate translation (#787 #839 #847)
2022-11-30 18:01:13 +01:00
Matthieu Gautier
0d8971ef88 Merge pull request #847 from kiwix/translatewiki 2022-11-30 17:59:15 +01:00
translatewiki.net
2812b5ca5c Localisation updates from https://translatewiki.net. 2022-11-30 14:50:14 +01:00
Matthieu Gautier
4dc8973cdc Merge pull request #848 from kiwix/fake_language_for_i18n_testing 2022-11-29 16:19:35 +01:00
Veloman Yunkan
160c95e317 Fake language for testing is now based on English
Usage of non-latin scripts in unit-tests creates unnecessary problems
for maintainers.
2022-11-26 11:59:04 +04:00
Veloman Yunkan
956289d9f8 Introduced a fake language for i18n testing
We need a fake language for tests that won't be affected by
modifications made by 3rd party translators (see kiwix/libkiwix#749).

- static/i18n/hy.json was cloned as static/i18n/test.json
- usage of "hy" in unit-tests was replaced with "test"
2022-11-26 11:58:27 +04:00
Kelson
3568ccd511 Merge pull request #843 from kiwix/backslash_handling_in_suggestions
Backslash handling in suggestions
2022-11-17 11:48:37 +01:00
Emmanuel Engelhart
d66cc6286c Fix broken macOS CI (change Python version) 2022-11-17 11:42:42 +01:00
Veloman Yunkan
7743e73ede All non-alphanumeric symbols deserve a test 2022-11-17 11:51:53 +04:00
Veloman Yunkan
4966f4155d Fixed handling of backslashes in suggestions 2022-11-17 11:51:53 +04:00
Veloman Yunkan
c727de6591 Unit-testing of kiwix::Suggestions
The new unit test fails because of a buggy mishandling of backslashes
in suggestions. The fix is coming next.
2022-11-17 11:51:53 +04:00
Veloman Yunkan
0f0ae1cfed A small refactoring 2022-11-17 11:51:53 +04:00
Veloman Yunkan
da78aae62b kiwix::Suggestions gives up its temporary pedigree 2022-11-17 11:51:53 +04:00
Veloman Yunkan
abcd4ade99 kiwix::Suggestions::getJSON() 2022-11-17 11:51:53 +04:00
Veloman Yunkan
7a9780eb90 kiwix::Suggestions::addFTSearchSuggestion() 2022-11-17 11:51:53 +04:00
Veloman Yunkan
51bd881211 kiwix::Suggestions::add() 2022-11-17 11:51:53 +04:00
Veloman Yunkan
f36f1661d5 Got rid of result count tracker variable 2022-11-17 11:51:53 +04:00
Veloman Yunkan
18f4a58237 Conception of kiwix::Suggestions 2022-11-17 11:51:53 +04:00
Veloman Yunkan
6285599b7c Merge pull request #839 from kiwix/translatewiki
Localisation updates from https://translatewiki.net.
2022-11-17 11:29:21 +04:00
Veloman Yunkan
764f68f7d8 Updated i18n_resources_list 2022-11-17 11:10:07 +04:00
translatewiki.net
777c5e1f7a Localisation updates from https://translatewiki.net. 2022-11-14 13:06:22 +01:00
Kelson
8031ffa447 Merge pull request #801 from kiwix/legoktm-patch-1
PPA: Add kinetic
2022-11-13 21:03:46 +01:00
Kunal Mehta
0c8ceac117 PPA: Add kinetic 2022-11-13 21:02:32 +01:00
Kelson
ec31882e94 Merge pull request #836 from kiwix/media_count_libzim
Use new `zim::Archive::getMediaCount` from libzim.
2022-11-07 12:52:55 +01:00
Matthieu Gautier
8cec014691 Use new zim::Archive::getMediaCount from libzim.
As libzim also changed the behavior of `zim::Archive::getArticleCount`,
we don't need the hack, and we don't need the code to parse `M/Counter`.
2022-11-02 13:15:47 +01:00
Matthieu Gautier
bf9aeffbfa Merge pull request #841 from kiwix/catalog_filtering_by_multiple_languages 2022-11-01 19:23:36 +01:00
Veloman Yunkan
7765769e6f Beautification (better alignment) 2022-11-01 19:16:30 +01:00
Veloman Yunkan
7d69ece27d OPDS can be filtered using more than one language
From now on, the `lang` parameter of the /catalog/search,
/catalog/v2/entries, and /catalog/v2/partial_entries endpoints is
interpreted as a comma-separated list of languages.
2022-11-01 19:16:30 +01:00
Veloman Yunkan
c0d027e8a4 Unittests for OPDS filtering by language 2022-11-01 19:16:30 +01:00
Veloman Yunkan
c87add1419 Removed an unused variable 2022-11-01 19:16:30 +01:00
Matthieu Gautier
a52138e5ba Merge pull request #838 from kiwix/language_handling_during_search 2022-11-01 18:28:02 +01:00
Veloman Yunkan
d1b85192c0 ServerSearchTest.searchInMultilanguageBookSetIsDenied 2022-10-31 13:30:11 +04:00
Veloman Yunkan
cb02dbd92a RequestContext preserves the exact query string
Before this change RequestContext::get_query() returned a reordered
query string (alphabetically sorted by the parameter names).

This fix facilitiates testing of responses where the request URL appears
in the response.
2022-10-31 13:28:21 +04:00
Veloman Yunkan
9409e8bd91 Preventing confusion of tongues in multizim search
Multizim search requires that all selected books be in the same
language.

No new URL query parameter was introduced for specifying the intended
search language - `books.filter.lang` can be used for that purpose.

The server_search unit-test was updated to use a slightly cheating
library xml file where the language of example.zim was tweaked from "en"
to "eng" in order to match that of zimfile.zim. Note that this change
drops from the tested server two other goofy ZIM files corner_cases.zim
and poor.zim that have been/are included in ServerTest.
2022-10-31 13:27:57 +04:00
Veloman Yunkan
cd62b5dd91 Some clean-up 2022-10-31 13:22:15 +04:00
Veloman Yunkan
414d7ae4fe Fixed indentation 2022-10-31 13:22:15 +04:00
Veloman Yunkan
9d2cc35447 Extracted InternalServer::handle_search_request() 2022-10-31 13:22:15 +04:00
Veloman Yunkan
7167ca1e6a Adios kiwix::getArchiveId() 2022-10-31 13:22:15 +04:00
Kelson
8cc1c47133 Merge pull request #837 from kiwix/opds_name_mapper_bis
Make OPDSDumper respect the NameMapper of the server.
2022-10-31 09:14:55 +01:00
Matthieu Gautier
e5b94fa1bb Make the opds_dumper respect the provided nameMapper used in the server.
Fix #828
2022-10-30 19:21:01 +01:00
Matthieu Gautier
b0d719431d Use a macro to define catalog's entries in test. 2022-10-26 17:37:45 +02:00
Matthieu Gautier
0e20f50443 Merge pull request #833 from kiwix/http_caching 2022-10-20 16:17:43 +02:00
Veloman Yunkan
18a18c17a9 Applied KIWIXCACHEID to skin/search-icon.svg 2022-10-19 19:27:21 +04:00
Veloman Yunkan
602c20f160 Removed unused resource skin/css/images/search.svg 2022-10-19 19:27:21 +04:00
Veloman Yunkan
415ec41099 Cacheids are computed for all static resources
Before this change cacheids were computed only for those static
resources that were referenced from other resources via KIWIXCACHEID.

A few static resources without such references existed.

Now all resources under skin/ have their cacheids computed.
2022-10-19 19:26:04 +04:00
Veloman Yunkan
b9f60ecfe9 Handling of cacheid when serving static resources
During static resource preprocessing and compilation their cacheid
values are embedded into libkiwix and can be accessed at runtime.

If a static resource is requsted without specifying any cacheid
it is served as dynamic content (with short TTL and the library id
used for the ETag, though using the cacheid for the ETag would
be better).

If a cacheid is supplied in the request it must match the cacheid of the
resource (otherwise a 404 Not Found error is returned) whereupon the
resource is served as immutable content.

Known issues:

- One issue is caused by the fact that some static resources don't get a
  cacheid; this is resolved in the next commit.

- Interaction of this change with the support for dynamically customizing
  static resources (via KIWIX_SERVE_CUSTOMIZED_RESOURCES env var) was
  not addressed.
2022-10-19 19:26:04 +04:00
Veloman Yunkan
12a638750e Fixed URLs to static resources without cacheids
One (hopefully, last) remaining relative URL to a static resource
is the reference to ./search-icon.svg found in skin/index.css to which
KIWIXCACHEID could not be applied because of the limitations of the
resource preprocessing script `kiwix-resources`.
2022-10-19 19:26:04 +04:00
Veloman Yunkan
b62486c2f9 Added /catalog URLs to general purpose server tests 2022-10-19 19:26:04 +04:00
Veloman Yunkan
6bc7e0178d Added all static resources to the server unit-test 2022-10-19 19:26:04 +04:00
Veloman Yunkan
ce8b2bf9d9 Library::removeBookById() updates the revision 2022-10-19 19:26:04 +04:00
Veloman Yunkan
9fd1423100 Small clean-up 2022-10-19 19:26:04 +04:00
Veloman Yunkan
6b8d6232f0 InternalServer::getLibraryId() 2022-10-19 19:26:02 +04:00
Veloman Yunkan
c91df1cb26 Two private funcs of InternalServer became free 2022-10-19 19:21:28 +04:00
Veloman Yunkan
b249edee60 ETags for ZIM content use the ZIM file UUID 2022-10-19 19:21:28 +04:00
Veloman Yunkan
a31ccb6588 Decoupling ETags from the server id 2022-10-19 19:21:28 +04:00
Veloman Yunkan
43c8da9b04 Testing of cache control 2022-10-19 19:21:28 +04:00
Veloman Yunkan
190156e095 Setting Cache-Control: for three types of content
At this point the ETag value for ZIM content is still generated from the
timestamp of the server start-up time.
2022-10-19 19:21:28 +04:00
Veloman Yunkan
5471819021 Finer categorization of URLs in the server unit-test
Preparing the server unit-test for the more elaborate handling of HTTP
caching.
2022-10-19 19:21:28 +04:00
Matthieu Gautier
7feef320d9 Merge pull request #834 from kiwix/concurrency_safe_suggestion_endpoint 2022-10-18 17:00:02 +02:00
Veloman Yunkan
73191fb8f8 Made the /suggest endpoint concurrency-safe 2022-10-13 13:39:25 +04:00
Matthieu Gautier
a844bc4000 Merge pull request #829 from kiwix/opds_dumper_cleanup
OPDS dumper cleanup
2022-10-06 14:11:47 +02:00
Veloman Yunkan
f13ca55ef6 Eliminated the endpointRoot parameter 2022-10-06 14:02:50 +04:00
Veloman Yunkan
dc194683bb Split XML generation code for full & partial entries 2022-10-06 13:48:58 +04:00
Veloman Yunkan
0841472004 Separate templates for full & partial OPDS entries 2022-10-06 13:44:39 +04:00
Veloman Yunkan
ebb713cb85 Got rid of an unjustified parameter
The XML header is injected in a more straightforward way in the single
location where it is needed.
2022-10-06 12:49:51 +04:00
Matthieu Gautier
cd6cbe3655 Merge pull request #827 from kiwix/http_redirect_new_logic
New logic for generating HTTP-redirects
2022-10-04 16:04:20 +02:00
Veloman Yunkan
582c8d868a New logic for generating HTTP-redirects
Before this fix the root URL for a book was assumed to resolve to the
main page.  This was not true for ZIM files containing an entry at an
empty path or with a path equal to "/", resulting in issue #826. The
logic behind this behaviour is found in `kiwix::getEntryFromPath()`.

The fix to that issue is a little more general and will result in an
HTTP redirect in any case where `kiwix::getEntryFromPath(zim, path)`
returns an entry with a real path different from the requested one. In
particular, this will affect the behaviour on ZIM files with the old
namespace scheme, where the requested resource - if not found - is also
looked up in the 'A', 'I', 'J', and/or '-' namespaces. Now instead of
returning the contents of that other resource an HTTP redirect response
will be sent.
2022-10-04 14:18:08 +04:00
Kelson
f6ae75e41d Merge pull request #822 from kiwix/update-format-code-script
Update format_code.sh script
2022-10-03 16:20:32 +02:00
Emmanuel Engelhart
ffbda34b75 Fix: improvement to handle dirs with spaces 2022-10-01 21:15:50 +02:00
Emmanuel Engelhart
f61fc07121 Fix: autodetect proper directories to format 2022-09-29 20:37:20 +02:00
Emmanuel Engelhart
de7fa771fc More generic format_code.sh script 2022-09-29 20:23:15 +02:00
Emmanuel Engelhart
24c1ca5a4a Move format_code.sh to script/ folder 2022-09-29 20:23:15 +02:00
Matthieu Gautier
15f5abad3c Merge pull request #821 from kiwix/taskbar_autohiding
Auto-hiding of the iframe-based taskbar
2022-09-28 17:15:59 +02:00
Veloman Yunkan
0a866fa914 Fixed auto-hiding of the toolbar 2022-09-28 17:00:00 +02:00
Veloman Yunkan
ff192cba49 Fixed a misused setInterval()
In the commit that introduced `setInterval()` in `setupViewer()`
actually `setTimeout()` was intended.
2022-09-28 17:00:00 +02:00
Matthieu Gautier
0dd638f261 Merge pull request #825 from kiwix/no_impish
PPA: Drop impish
2022-09-28 16:59:26 +02:00
Kunal Mehta
229c0ceaf9 PPA: Drop impish 2022-09-28 16:31:33 +02:00
Matthieu Gautier
70f7be4202 Merge pull request #823 from kiwix/kiwix-desktop-friendly-search-results
Fixed search results for kiwix-desktop
2022-09-28 15:40:02 +02:00
Veloman Yunkan
60148717e1 Fixed search results for kiwix-desktop 2022-09-26 13:11:25 +04:00
74 changed files with 2040 additions and 900 deletions

27
.github/move.yml vendored
View File

@@ -1,27 +0,0 @@
# Configuration for Move Issues - https://github.com/dessant/move-issues
# Delete the command comment when it contains no other content
deleteCommand: true
# Close the source issue after moving
closeSourceIssue: true
# Lock the source issue after moving
lockSourceIssue: false
# Mention issue and comment authors
mentionAuthors: true
# Preserve mentions in the issue content
keepContentMentions: true
# Move labels that also exist on the target repository
moveLabels: true
# Set custom aliases for targets
# aliases:
# r: repo
# or: owner/repo
# Repository to extend settings from
# _extends: repo

View File

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

View File

@@ -8,8 +8,8 @@ jobs:
fail-fast: false
matrix:
distro:
- ubuntu-kinetic
- ubuntu-jammy
- ubuntu-impish
- ubuntu-focal
- ubuntu-bionic
steps:
@@ -34,18 +34,18 @@ jobs:
email: release+launchpad@kiwix.org
distro: ${{ matrix.distro }}
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
if: matrix.distro == 'ubuntu-jammy'
name: Build package for ubuntu-jammy
id: build-ubuntu-jammy
- uses: legoktm/gh-action-build-deb@ubuntu-kinetic
if: matrix.distro == 'ubuntu-kinetic'
name: Build package for ubuntu-kinetic
id: build-ubuntu-kinetic
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-impish
if: matrix.distro == 'ubuntu-impish'
name: Build package for ubuntu-impish
id: build-ubuntu-impish
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
if: matrix.distro == 'ubuntu-jammy'
name: Build package for ubuntu-jammy
id: build-ubuntu-jammy
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
@@ -73,8 +73,8 @@ jobs:
- uses: legoktm/gh-action-dput@master
name: Upload dev package
# Only upload on pushes to master
if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' && startswith(matrix.distro, 'ubuntu-')
# Only upload on pushes to git default branch
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
with:
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
repository: ppa:kiwixteam/dev

View File

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

View File

@@ -7,10 +7,10 @@ GNU/Linux, macOS, Android, iOS, ...).
[![Release](https://img.shields.io/github/v/tag/kiwix/libkiwix?label=release&sort=semver)](https://download.kiwix.org/release/libkiwix/)
[![Repositories](https://img.shields.io/repology/repositories/libkiwix?label=repositories)](https://github.com/kiwix/libkiwix/wiki/Repology)
[![Build Status](https://github.com/kiwix/libkiwix/workflows/CI/badge.svg?query=branch%3Amaster)](https://github.com/kiwix/libkiwix/actions?query=branch%3Amaster)
[![Build Status](https://github.com/kiwix/libkiwix/workflows/CI/badge.svg?query=branch%3Amain)](https://github.com/kiwix/libkiwix/actions?query=branch%3Amain)
[![Doc](https://readthedocs.org/projects/libkiwix/badge/?style=flat)](https://libkiwix.readthedocs.org/en/latest/?badge=latest)
[![CodeFactor](https://www.codefactor.io/repository/github/kiwix/libkiwix/badge)](https://www.codefactor.io/repository/github/kiwix/libkiwix)
[![Codecov](https://codecov.io/gh/kiwix/libkiwix/branch/master/graph/badge.svg)](https://codecov.io/gh/kiwix/libkiwix)
[![Codecov](https://codecov.io/gh/kiwix/libkiwix/branch/main/graph/badge.svg)](https://codecov.io/gh/kiwix/libkiwix)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
Disclaimer

View File

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

View File

@@ -106,7 +106,15 @@ class Filter {
Filter& rejectTags(const Tags& tags);
Filter& category(std::string category);
/**
* Set the filter to only accept books in the specified language.
*
* Multiple languages can be specified as a comma-separated list (in
* which case a book in any of those languages will match).
*/
Filter& lang(std::string lang);
Filter& publisher(std::string publisher);
Filter& creator(std::string creator);
Filter& maxSize(size_t size);
@@ -332,8 +340,8 @@ class Library
/**
* Return the current revision of the library.
*
* The revision of the library is updated (incremented by one) only by
* the addBook() operation.
* The revision of the library is updated (incremented by one) by
* the addBook() and removeBookById() operations.
*
* @return Current revision of the library.
*/

View File

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

View File

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

14
scripts/format_code.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

@@ -221,7 +221,11 @@ bool Library::removeBookById(const std::string& id)
// Having a too big cache is not a problem here (or it would have been before)
// (And setMaxSize doesn't actually reduce the cache size, extra cached items
// will be removed in put or getOrPut).
return mp_impl->m_books.erase(id) == 1;
const bool bookWasRemoved = mp_impl->m_books.erase(id) == 1;
if ( bookWasRemoved ) {
++mp_impl->m_revision;
}
return bookWasRemoved;
}
Library::Revision Library::getRevision() const
@@ -531,9 +535,20 @@ Xapian::Query categoryQuery(const std::string& category)
return Xapian::Query("XC" + normalizeText(category));
}
Xapian::Query langQuery(const std::string& lang)
Xapian::Query langQuery(const std::string& commaSeparatedLanguageList)
{
return Xapian::Query("L" + normalizeText(lang));
Xapian::Query q;
bool firstIteration = true;
for ( const auto& lang : kiwix::split(commaSeparatedLanguageList, ",") ) {
const Xapian::Query singleLangQuery("L" + normalizeText(lang));
if ( firstIteration ) {
q = singleLangQuery;
firstIteration = false;
} else {
q = Xapian::Query(Xapian::Query::OP_OR, q, singleLangQuery);
}
}
return q;
}
Xapian::Query publisherQuery(const std::string& publisher)

View File

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

View File

@@ -94,7 +94,7 @@ kainjow::mustache::data buildQueryData
kainjow::mustache::data query;
query.set("pattern", kiwix::encodeDiples(pattern));
std::ostringstream ss;
ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern, true);
ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern);
ss << "&" << bookQuery;
query.set("unpaginatedQuery", ss.str());
auto lang = extractValueFromQuery(bookQuery, "books.filter.lang");
@@ -166,14 +166,15 @@ kainjow::mustache::data buildPagination(
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
{
const std::string absPathPrefix = protocolPrefix + "content/";
const std::string absPathPrefix = protocolPrefix;
// Build the results list
kainjow::mustache::data items{kainjow::mustache::data::type::list};
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
kainjow::mustache::data result;
std::string zim_id(it.getZimId());
const std::string zim_id(it.getZimId());
const auto path = mp_nameMapper->getNameForId(zim_id) + "/" + it.getPath();
result.set("title", it.getTitle());
result.set("absolutePath", absPathPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath()));
result.set("absolutePath", absPathPrefix + urlEncode(path));
result.set("snippet", it.getSnippet());
if (mp_library) {
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
@@ -206,7 +207,7 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
kainjow::mustache::data allData;
allData.set("protocolPrefix", protocolPrefix);
allData.set("searchProtocolPrefix", searchProtocolPrefix);
allData.set("results", results);
allData.set("pagination", pagination);
allData.set("query", query);

View File

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

View File

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

View File

@@ -70,6 +70,14 @@ public: // functions
return s;
}
size_t getStringCount(const std::string& lang) const {
try {
return lang2TableMap.at(lang)->entryCount;
} catch(const std::out_of_range&) {
return 0;
}
}
private: // functions
const I18nStringTable* getStringsFor(const std::string& lang) const {
try {
@@ -84,13 +92,17 @@ private: // data
const I18nStringTable* enStrings;
};
const I18nStringDB& getStringDb()
{
static const I18nStringDB stringDb;
return stringDb;
}
} // unnamed namespace
std::string getTranslatedString(const std::string& lang, const std::string& key)
{
static const I18nStringDB stringDb;
return stringDb.get(lang, key);
return getStringDb().get(lang, key);
}
namespace i18n
@@ -111,4 +123,70 @@ std::string ParameterizedMessage::getText(const std::string& lang) const
return i18n::expandParameterizedString(lang, msgId, params);
}
namespace
{
LangPreference parseSingleLanguagePreference(const std::string& s)
{
const size_t langStart = s.find_first_not_of(" \t\n");
if ( langStart == std::string::npos ) {
return {"", 0};
}
const size_t langEnd = s.find(';', langStart);
if ( langEnd == std::string::npos ) {
return {s.substr(langStart), 1};
}
const std::string lang = s.substr(langStart, langEnd - langStart);
// We don't care about langEnd == langStart which will result in an empty
// language name - it will be dismissed by parseUserLanguagePreferences()
float q = 1.0;
int nCharsScanned;
if ( 1 == sscanf(s.c_str() + langEnd + 1, "q=%f%n", &q, &nCharsScanned)
&& langEnd + 1 + nCharsScanned == s.size() ) {
return {lang, q};
}
return {"", 0};
}
} // unnamed namespace
UserLangPreferences parseUserLanguagePreferences(const std::string& s)
{
UserLangPreferences result;
std::istringstream iss(s);
std::string singleLangPrefStr;
while ( std::getline(iss, singleLangPrefStr, ',') )
{
const auto langPref = parseSingleLanguagePreference(singleLangPrefStr);
if ( !langPref.lang.empty() && langPref.preference > 0 ) {
result.push_back(langPref);
}
}
return result;
}
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs)
{
if ( prefs.empty() ) {
return "en";
}
std::string bestLangSoFar("en");
float bestScoreSoFar = 0;
const auto& stringDb = getStringDb();
for ( const auto& entry : prefs ) {
const float score = entry.preference * stringDb.getStringCount(entry.lang);
if ( score > bestScoreSoFar ) {
bestScoreSoFar = score;
bestLangSoFar = entry.lang;
}
}
return bestLangSoFar;
}
} // namespace kiwix

View File

@@ -89,6 +89,18 @@ private: // data
const Parameters params;
};
struct LangPreference
{
const std::string lang;
const float preference;
};
typedef std::vector<LangPreference> UserLangPreferences;
UserLangPreferences parseUserLanguagePreferences(const std::string& s);
std::string selectMostSuitableLanguage(const UserLangPreferences& prefs);
} // namespace kiwix
#endif // KIWIX_SERVER_I18N

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,10 @@
#include <sstream>
#include <cstdio>
#include <atomic>
#include <cctype>
#include "tools/stringTools.h"
#include "i18n.h"
namespace kiwix {
@@ -66,12 +68,13 @@ fullURL2LocalURL(const std::string& full_url, const std::string& rootLocation)
} // unnamed namespace
RequestContext::RequestContext(struct MHD_Connection* connection,
std::string rootLocation,
std::string _rootLocation,
const std::string& _url,
const std::string& _method,
const std::string& version) :
rootLocation(_rootLocation),
full_url(_url),
url(fullURL2LocalURL(_url, rootLocation)),
url(fullURL2LocalURL(_url, _rootLocation)),
method(str2RequestMethod(_method)),
version(version),
requestIndex(s_requestIndex++),
@@ -80,6 +83,7 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
{
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this);
try {
acceptEncodingGzip =
@@ -89,6 +93,8 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
try {
byteRange_ = ByteRange::parse(get_header(MHD_HTTP_HEADER_RANGE));
} catch (const std::out_of_range&) {}
userlang = determine_user_language();
}
RequestContext::~RequestContext()
@@ -107,6 +113,22 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
{
RequestContext *_this = static_cast<RequestContext*>(__this);
_this->arguments[key].push_back(value == nullptr ? "" : value);
if ( ! _this->queryString.empty() ) {
_this->queryString += "&";
}
_this->queryString += urlEncode(key);
if ( value ) {
_this->queryString += "=";
_this->queryString += urlEncode(value);
}
return MHD_YES;
}
MHD_Result RequestContext::fill_cookie(void *__this, enum MHD_ValueKind kind,
const char *key, const char* value)
{
RequestContext *_this = static_cast<RequestContext*>(__this);
_this->cookies[key] = value == nullptr ? "" : value;
return MHD_YES;
}
@@ -172,6 +194,10 @@ std::string RequestContext::get_full_url() const {
return full_url;
}
std::string RequestContext::get_root_path() const {
return rootLocation.empty() ? "/" : rootLocation;
}
bool RequestContext::is_valid_url() const {
return !url.empty();
}
@@ -190,16 +216,33 @@ std::string RequestContext::get_header(const std::string& name) const {
}
std::string RequestContext::get_user_language() const
{
return userlang.lang;
}
bool RequestContext::user_language_comes_from_cookie() const
{
return userlang.selectedBy == UserLanguage::SelectorKind::COOKIE;
}
RequestContext::UserLanguage RequestContext::determine_user_language() const
{
try {
return get_argument("userlang");
return {UserLanguage::SelectorKind::QUERY_PARAM, get_argument("userlang")};
} catch(const std::out_of_range&) {}
try {
return get_header("Accept-Language");
return {UserLanguage::SelectorKind::COOKIE, cookies.at("userlang")};
} catch(const std::out_of_range&) {}
return "en";
try {
const std::string acceptLanguage = get_header("Accept-Language");
const auto userLangPrefs = parseUserLanguagePreferences(acceptLanguage);
const auto lang = selectMostSuitableLanguage(userLangPrefs);
return {UserLanguage::SelectorKind::ACCEPT_LANGUAGE_HEADER, lang};
} catch(const std::out_of_range&) {}
return {UserLanguage::SelectorKind::DEFAULT, "en"};
}
std::string RequestContext::get_requested_format() const

View File

@@ -91,22 +91,20 @@ class RequestContext {
std::string get_url() const;
std::string get_url_part(int part) const;
std::string get_full_url() const;
std::string get_root_path() const;
std::string get_query(bool mustEncode = false) const {
return get_query([](const std::string& key) {return true;}, mustEncode);
}
std::string get_query() const { return queryString; }
template<class F>
std::string get_query(F filter, bool mustEncode) const {
std::string get_query(F filter) const {
std::string q;
const char* sep = "";
auto encode = [=](const std::string& value) { return mustEncode?urlEncode(value, true):value; };
for ( const auto& a : arguments ) {
if (!filter(a.first)) {
continue;
}
for (const auto& v: a.second) {
q += sep + encode(a.first) + '=' + encode(v);
q += sep + urlEncode(a.first) + '=' + urlEncode(v);
sep = "&";
}
}
@@ -120,7 +118,25 @@ class RequestContext {
std::string get_user_language() const;
std::string get_requested_format() const;
bool user_language_comes_from_cookie() const;
private: // types
struct UserLanguage
{
enum SelectorKind
{
QUERY_PARAM,
COOKIE,
ACCEPT_LANGUAGE_HEADER,
DEFAULT
};
SelectorKind selectedBy;
std::string lang;
};
private: // data
std::string rootLocation;
std::string full_url;
std::string url;
RequestMethod method;
@@ -132,9 +148,15 @@ class RequestContext {
ByteRange byteRange_;
std::map<std::string, std::string> headers;
std::map<std::string, std::vector<std::string>> arguments;
std::map<std::string, std::string> cookies;
std::string queryString;
UserLanguage userlang;
private: // functions
UserLanguage determine_user_language() const;
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);
static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*);
static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
};

View File

@@ -64,7 +64,13 @@ bool is_compressible_mime_type(const std::string& mimeType)
|| mimeType.find("application/javascript") != std::string::npos
|| mimeType.find("application/atom") != std::string::npos
|| mimeType.find("application/opensearchdescription") != std::string::npos
|| mimeType.find("application/json") != std::string::npos;
|| mimeType.find("application/json") != std::string::npos
// Web fonts
|| mimeType.find("application/font-") != std::string::npos
|| mimeType.find("application/x-font-") != std::string::npos
|| mimeType.find("application/vnd.ms-fontobject") != std::string::npos
|| mimeType.find("font/") != std::string::npos;
}
bool compress(std::string &content) {
@@ -102,6 +108,14 @@ bool compress(std::string &content) {
}
const char* getCacheControlHeader(Response::Kind k)
{
switch(k) {
case Response::STATIC_RESOURCE: return "max-age=31536000, immutable";
case Response::ZIM_CONTENT: return "max-age=3600, must-revalidate";
default: return "max-age=0, must-revalidate";
}
}
} // unnamed namespace
@@ -112,6 +126,13 @@ Response::Response(bool verbose)
add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
}
void Response::set_kind(Kind k)
{
m_kind = k;
if ( k == ZIM_CONTENT )
m_etag.set_option(ETag::ZIM_CONTENT);
}
std::unique_ptr<Response> Response::build(const InternalServer& server)
{
return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
@@ -122,6 +143,9 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
auto response = Response::build(server);
response->set_code(MHD_HTTP_NOT_MODIFIED);
response->m_etag = etag;
if ( etag.get_option(ETag::ZIM_CONTENT) ) {
response->set_kind(Response::ZIM_CONTENT);
}
if ( etag.get_option(ETag::COMPRESSED_CONTENT) ) {
response->add_header(MHD_HTTP_HEADER_VARY, "Accept-Encoding");
}
@@ -355,7 +379,7 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
MHD_Response* response = create_mhd_response(request);
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
m_etag.get_option(ETag::CACHEABLE_ENTITY) ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
getCacheControlHeader(m_kind));
const std::string etag = m_etag.get_etag();
if ( ! etag.empty() )
MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etag.c_str());
@@ -363,6 +387,13 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
}
if ( ! request.user_language_comes_from_cookie() ) {
const std::string cookie = "userlang=" + request.get_user_language()
+ ";Path=" + request.get_root_path()
+ ";Max-Age=31536000";
MHD_add_response_header(response, MHD_HTTP_HEADER_SET_COOKIE, cookie.c_str());
}
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
@@ -411,7 +442,7 @@ ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::strin
m_mimeType(mimetype)
{
m_byteRange = byterange;
set_cacheable();
set_kind(Response::ZIM_CONTENT);
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
}
@@ -423,14 +454,14 @@ std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, cons
if (noRange && is_compressible_mime_type(mimetype)) {
// Return a contentResponse
auto response = ContentResponse::build(server, item.getData(), mimetype);
response->set_cacheable();
response->set_kind(Response::ZIM_CONTENT);
response->m_byteRange = byteRange;
return std::move(response);
}
if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
auto response = Response::build_416(server, item.getSize());
response->set_cacheable();
response->set_kind(Response::ZIM_CONTENT);
return response;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -161,15 +161,14 @@ std::string kiwix::encodeDiples(const std::string& str)
return result;
}
/* urlEncode() based on javascript encodeURI() &
encodeURIComponent(). Mostly code from rstudio/httpuv (GPLv3) */
namespace
{
bool isReservedUrlChar(char c)
{
switch (c) {
case ';':
case ',':
case '/':
case '?':
case ':':
case '@':
@@ -177,22 +176,22 @@ bool isReservedUrlChar(char c)
case '=':
case '+':
case '$':
case '#':
return true;
default:
return false;
}
}
bool needsEscape(char c, bool encodeReserved)
bool isHarmlessUriChar(char c)
{
if (c >= 'a' && c <= 'z')
return false;
return true;
if (c >= 'A' && c <= 'Z')
return false;
return true;
if (c >= '0' && c <= '9')
return false;
if (isReservedUrlChar(c))
return encodeReserved;
return true;
switch (c) {
case '-':
case '_':
@@ -203,8 +202,46 @@ bool needsEscape(char c, bool encodeReserved)
case '\'':
case '(':
case ')':
return false;
case '/':
return true;
}
return false;
}
bool mustBeUriEncodedFor(kiwix::URIComponentKind target, char c)
{
if (isHarmlessUriChar(c))
return false;
switch (c) {
case '/': // There is no reason to encode the path separator in the general
// case. It must be encoded only in a path component when its
// semantics of a path separator has to be suppressed.
return false;
case '@': // In a relative URL of the form abc@def/xyz (with no / in abc)
// a non-encoded @ will make "abc" and "def" to be interpreted as
// username and host components, respectively
return target == kiwix::URIComponentKind::PATH;
case ':': // In a relative URL of the form abc:def/xyz (with no / in abc)
// a non-encoded : will make "abc" and "def" to be interpreted as
// host and port components, respectively
return target == kiwix::URIComponentKind::PATH;
case '?': // A non-encoded '?' acts as a separator between the path
// and query components
return target == kiwix::URIComponentKind::PATH;
case '&': return target == kiwix::URIComponentKind::QUERY;
case '=': return target == kiwix::URIComponentKind::QUERY;
case '+': return target == kiwix::URIComponentKind::QUERY;
case '#': // A non-encoded '#' in either path or query-component
// would mark the beginning of the fragment component
return true;
}
return true;
}
@@ -230,23 +267,43 @@ int hexToInt(char c) {
}
}
std::string kiwix::urlEncode(const std::string& value, bool encodeReserved)
} // unnamed namespace
std::string kiwix::urlEncode(const std::string& value)
{
std::ostringstream os;
os << std::hex << std::uppercase;
for (std::string::const_iterator it = value.begin();
it != value.end();
it++) {
if (!needsEscape(*it, encodeReserved)) {
os << *it;
for (const char c : value) {
if (isHarmlessUriChar(c)) {
os << c;
} else {
os << '%' << std::setw(2) << static_cast<unsigned int>(static_cast<unsigned char>(*it));
const unsigned int charVal = static_cast<unsigned char>(c);
os << '%' << std::setw(2) << std::setfill('0') << charVal;
}
}
return os.str();
}
namespace kiwix
{
std::string uriEncode(URIComponentKind target, const std::string& value)
{
std::ostringstream os;
os << std::hex << std::uppercase;
for (const char c : value) {
if ( mustBeUriEncodedFor(target, c) ) {
const unsigned int charVal = static_cast<unsigned char>(c);
os << '%' << std::setw(2) << std::setfill('0') << charVal;
} else {
os << c;
}
}
return os.str();
}
} // namespace kiwix
std::string kiwix::urlDecode(const std::string& value, bool component)
{
std::ostringstream os;
@@ -267,15 +324,15 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
int iHi = hexToInt(hi);
int iLo = hexToInt(lo);
if (iHi < 0 || iLo < 0) {
// Invalid escape sequence
os << '%' << hi << lo;
continue;
// Invalid escape sequence
os << '%' << hi << lo;
continue;
}
char c = (char)(iHi << 4 | iLo);
if (!component && isReservedUrlChar(c)) {
os << '%' << hi << lo;
os << '%' << hi << lo;
} else {
os << c;
os << c;
}
} else {
os << *it;

View File

@@ -55,9 +55,22 @@ private:
};
std::string urlEncode(const std::string& value, bool encodeReserved = false);
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
* difference that the slash (/) symbol is NOT encoded. */
std::string urlEncode(const std::string& value);
std::string urlDecode(const std::string& value, bool component = false);
// Only URI components that are of interest to libkiwix
// are included in the below enumeration type
enum class URIComponentKind
{
PATH,
QUERY
};
// Encode 'value' for usage in a URI componenet specified by 'target'
std::string uriEncode(URIComponentKind target, const std::string& value);
std::string join(const std::vector<std::string>& list, const std::string& sep);
std::string ucAll(const std::string& word);

33
static/i18n/ar.json Normal file
View File

@@ -0,0 +1,33 @@
{
"@metadata": {
"authors": [
"Asma",
"Ravan",
"محمد أحمد عبد الفتاح"
]
},
"name": "الإنجليزية",
"no-such-book": "لا يوجد مثل هذا الكتاب: {{BOOK_NAME}}",
"too-many-books": "طلب العديد من الكتب {{NB_BOOKS}} حيث الحد {{LIMIT}}",
"no-book-found": "لا يوجد كتاب يطابق معايير الاختيار",
"url-not-found": "لم يتم العثور على عنوان URL المطلوب \"{{url}}\" على هذا الخادم.",
"suggest-search": "قم بإجراء بحث عن النص الكامل لـ <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "مع الأسف! فشل اختيار مقال عشوائي :(",
"invalid-raw-data-type": "{{DATATYPE}} ليس طلبًا صالحًا للمحتوى الأولي.",
"no-value-for-arg": "لم يتم تقديم قيمة للوسيطة {{ARGUMENT}}",
"no-query": "لم يتم تقديم ملخص.",
"raw-entry-not-found": "لا يمكن العثور على إدخال {{DATATYPE}} {{ENTRY}}",
"400-page-title": "طلب غير صالح",
"400-page-heading": "طلب غير صالح",
"404-page-title": "المحتوى غير موجود",
"404-page-heading": "لم يتم العثور عليه",
"500-page-title": "خطأ في الخادم الداخلي",
"500-page-heading": "خطأ في الخادم الداخلي",
"fulltext-search-unavailable": "البحث عن النص الكامل غير متاح",
"no-search-results": "محرك البحث عن النص الكامل غير متاح لهذا المحتوى.",
"library-button-text": "اذهب لصفحة الترحيب",
"home-button-text": "انتقل إلى الصفحة الرئيسية لـ \"{{BOOK_TITLE}}\"",
"random-page-button-text": "اذهب إلى صفحة عشوائية",
"searchbox-tooltip": "بحث \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "قد يشارك في البحث كتابان أو أكثر بلغات مختلفة، مما قد يؤدي إلى نتائج محيرة."
}

20
static/i18n/de.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

32
static/i18n/sl.json Normal file
View File

@@ -0,0 +1,32 @@
{
"@metadata": {
"authors": [
"Eleassar"
]
},
"name": "slovenščina",
"suggest-full-text-search": "vsebuje »{{{SEARCH_TERMS}}}« ...",
"no-such-book": "Ni take knjige: {{BOOK_NAME}}",
"too-many-books": "Preveč zahtevanih knjig ({{NB_BOOKS}}), omejitev je {{LIMIT}}",
"no-book-found": "Izbirnim merilom ne ustreza nobena knjiga",
"url-not-found": "Zahtevanega URL-ja »{{url}}« v tem strežniku ni bilo mogoče najti.",
"suggest-search": "Preiščite celotno besedilo za <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Ups! Ni bilo mogoče izbrati naključnega članka :(",
"invalid-raw-data-type": "{{DATATYPE}} ni veljaven zahtevek za neobdelano vsebino.",
"no-value-for-arg": "Argument {{ARGUMENT}} nima določene nobene vrednosti",
"no-query": "Poizvedba ni podana.",
"raw-entry-not-found": "Ni mogoče najti vnosa {{ENTRY}} vrste {{DATATYPE}}",
"400-page-title": "Neveljaven zahtevek",
"400-page-heading": "Neveljaven zahtevek",
"404-page-title": "Vsebine ni mogoče najti",
"404-page-heading": "Ni najdeno",
"500-page-title": "Notranja napaka strežnika",
"500-page-heading": "Notranja napaka strežnika",
"fulltext-search-unavailable": "Iskanje po celotnem besedilu ni na voljo",
"no-search-results": "Iskalnik po celotnem besedilu za to vsebino ni na voljo.",
"library-button-text": "Pojdite na pozdravno stran",
"home-button-text": "Pojdite na glavno stran »{{BOOK_TITLE}}«",
"random-page-button-text": "Pojdite na naključno izbrano stran",
"searchbox-tooltip": "Poiščite »{{BOOK_TITLE}}«",
"confusion-of-tongues": "V iskanju bi bili uporabljeni dve ali več knjig v različnih jezikih, kar lahko pripelje do nejasnih zadetkov."
}

View File

@@ -1,6 +1,7 @@
{
"@metadata": {
"authors": [
"Jopparn",
"Sabelöga",
"WikiPhoenix"
]
@@ -9,10 +10,13 @@
"suggest-full-text-search": "innehåller '{{{SEARCH_TERMS}}}'...",
"no-such-book": "Ingen sådan bok: {{BOOK_NAME}}",
"too-many-books": "För många böcker begärda ({{NB_BOOKS}}) där gränsen är {{LIMIT}}",
"no-book-found": "Ingen bok matchar urvalskriterierna",
"url-not-found": "Den begärda webbadressen \"{{url}}\" hittades inte på denna server.",
"suggest-search": "Utför en fulltextsökning för <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
"random-article-failure": "Hoppsan! Kunde inte välja en slumpartikel :(",
"invalid-raw-data-type": "{{DATATYPE}} är ingen giltig begäran för oformaterat innehåll.",
"no-value-for-arg": "Inget värde angett för argumentet {{ARGUMENT}}",
"no-query": "Ingen fråga tillhandahålls.",
"raw-entry-not-found": "Kunde inte hitta {{DATATYPE}}-inlägget {{ENTRY}}",
"400-page-title": "Ogiltig begäran",
"400-page-heading": "Ogiltig begäran",
@@ -25,5 +29,6 @@
"library-button-text": "Gå till hemsidan",
"home-button-text": "Gå till huvudsidan för \"{{BOOK_TITLE}}\"",
"random-page-button-text": "Gå till en slumpmässigt utvald sida",
"searchbox-tooltip": "Sök efter \"{{BOOK_TITLE}}\""
"searchbox-tooltip": "Sök efter \"{{BOOK_TITLE}}\"",
"confusion-of-tongues": "Två eller fler böcker på olika språk skulle delta i sökningen, vilket kan ge förvirrande resultat."
}

20
static/i18n/test.json Normal file
View File

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

View File

@@ -28,5 +28,6 @@
"library-button-text": "前往歡迎首頁",
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
"random-page-button-text": "前往隨機選取頁面",
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋"
"searchbox-tooltip": "在{{BOOK_TITLE}}搜尋",
"confusion-of-tongues": "搜索裡有加入兩本或更多不同語言的書籍,這可能會導致混淆結果。"
}

View File

@@ -1,5 +1,7 @@
i18n/ar.json
i18n/bn.json
i18n/cs.json
i18n/de.json
i18n/en.json
i18n/fr.json
i18n/he.json
@@ -14,7 +16,9 @@ i18n/pl.json
i18n/ru.json
i18n/sc.json
i18n/sk.json
i18n/sl.json
i18n/sv.json
i18n/test.json
i18n/tr.json
i18n/zh-hans.json
i18n/zh-hant.json

View File

@@ -1,6 +1,7 @@
resource_files = run_command(res_manager,
'--list-all',
files('resources_list.txt')
files('resources_list.txt'),
check: true
).stdout().strip().split('\n')
preprocessed_resources = custom_target('preprocessed_resource_files',
@@ -33,7 +34,8 @@ lib_resources = custom_target('resources',
i18n_resource_files = run_command(find_program('python3'),
'-c',
'import sys; f=open(sys.argv[1]); print(f.read())',
files('i18n_resources_list.txt')
files('i18n_resources_list.txt'),
check: true
).stdout().strip().split('\n')
i18n_resources = custom_target('i18n_resources',

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@@ -43,17 +43,27 @@ function gotoMainPageOfCurrentBook() {
}
function gotoUrl(url) {
contentIframe.src = url;
contentIframe.src = root + url;
}
function gotoRandomPage() {
gotoUrl(`${root}/random?content=${currentBook}`);
gotoUrl(`/random?content=${currentBook}`);
}
function performSearch() {
const searchbox = document.getElementById('kiwixsearchbox');
const q = encodeURIComponent(searchbox.value);
gotoUrl(`${root}/search?books.name=${currentBook}&pattern=${q}`);
gotoUrl(`/search?books.name=${currentBook}&pattern=${q}`);
}
function makeJSLink(jsCodeString, linkText, linkAttr="") {
// Values of the href attribute are assumed by the browser to be
// fully URI-encoded (no matter what the scheme is). Therefore, in
// order to prevent the browser from decoding any URI-encoded parts
// in the JS code we have to URI-encode a second time.
// (see https://stackoverflow.com/questions/33721510)
const uriEncodedJSCode = encodeURIComponent(jsCodeString);
return `<a ${linkAttr} href="javascript:${uriEncodedJSCode}">${linkText}</a>`;
}
function suggestionsApiURL()
@@ -152,6 +162,27 @@ function updateSearchBoxForBookChange() {
}
}
let previousScrollTop = Infinity;
function updateToolbarVisibilityState() {
const iframeDoc = contentIframe.contentDocument;
const st = iframeDoc.documentElement.scrollTop || iframeDoc.body.scrollTop;
if ( Math.abs(previousScrollTop - st) <= 5 )
return;
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
if (st > previousScrollTop) {
kiwixToolBar.style.position = 'fixed';
kiwixToolBar.style.top = '-100%';
} else {
kiwixToolBar.style.position = 'static';
kiwixToolBar.style.top = '0';
}
previousScrollTop = st;
}
function handle_visual_viewport_change() {
contentIframe.height = window.visualViewport.height - contentIframe.offsetTop - 4;
}
@@ -165,6 +196,7 @@ function handle_location_hash_change() {
contentIframe.contentWindow.location.replace(iframeContentUrl);
}
updateSearchBoxForLocationChange();
previousScrollTop = Infinity;
}
function handle_content_url_change() {
@@ -276,39 +308,7 @@ function htmlDecode(input) {
}
function setupAutoHidingOfTheToolbar() {
let lastScrollTop = 0;
const delta = 5;
let didScroll = false;
const kiwixToolBar = document.querySelector('#kiwixtoolbar');
contentIframe.contentWindow.addEventListener('scroll', () => {
didScroll = true;
});
setInterval(function() {
if (didScroll) {
hasScrolled();
didScroll = false;
}
}, 250);
function hasScrolled() {
const iframeDoc = contentIframe.contentDocument;
const st = iframeDoc.documentElement.scrollTop || iframeDoc.body.scrollTop;
if (Math.abs(lastScrollTop - st) <= delta)
return;
if (st > lastScrollTop) {
kiwixToolBar.style.position = 'fixed';
kiwixToolBar.style.top = '-100%';
} else {
kiwixToolBar.style.position = 'static';
kiwixToolBar.style.top = '0';
}
lastScrollTop = st;
}
setInterval(updateToolbarVisibilityState, 250);
}
function setupSuggestions() {
@@ -346,13 +346,21 @@ function setupSuggestions() {
},
resultItem: {
element: (item, data) => {
let searchLink;
const uriEncodedBookName = encodeURIComponent(currentBook);
let url;
if (data.value.kind == "path") {
searchLink = `${root}/${currentBook}/${htmlDecode(data.value.path)}`;
const path = encodeURIComponent(htmlDecode(data.value.path));
url = `/content/${uriEncodedBookName}/${path}`;
} else {
searchLink = `${root}/search?content=${encodeURIComponent(currentBook)}&pattern=${encodeURIComponent(htmlDecode(data.value.value))}`;
const pattern = encodeURIComponent(htmlDecode(data.value.value));
url = `/search?content=${uriEncodedBookName}&pattern=${pattern}`;
}
item.innerHTML = `<a class="suggest" href="javascript:gotoUrl('${searchLink}')">${htmlDecode(data.value.label)}</a>`;
// url can't contain any double quote and/or backslash symbols
// since they should have been URI-encoded. Therefore putting it
// inside double quotes should result in valid javascript.
const jsAction = `gotoUrl("${url}")`;
const linkText = htmlDecode(data.value.label);
item.innerHTML = makeJSLink(jsAction, linkText, 'class="suggest"');
},
highlight: "autoComplete_highlight",
selected: "autoComplete_selected"
@@ -384,7 +392,10 @@ function setupSuggestions() {
}
function setupViewer() {
setInterval(handle_visual_viewport_change, 0);
// Defer the call of handle_visual_viewport_change() until after the
// presence or absence of the taskbar as determined by this function
// has been settled.
setTimeout(handle_visual_viewport_change, 0);
const kiwixToolBarWrapper = document.getElementById('kiwixtoolbarwrapper');
if ( ! viewerSettings.toolbarEnabled ) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

Binary file not shown.

1
test/data/corner_cases/c# Symbolic link
View File

@@ -0,0 +1 @@
c#.html

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>C#</title>
</head>
<body>
<p>C# (pronounced see sharp) is a general-purpose, high-level multi-paradigm programming language. C# encompasses static typing, strong typing, lexically scoped, imperative, declarative, functional, generic, object-oriented (class-based), and component-oriented programming disciplines</p>
</body>
</html>

View File

@@ -0,0 +1 @@
c#.html

View File

@@ -2,13 +2,14 @@
cd "$(dirname "$0")"
rm -f corner_cases.zim
zimwriterfs -w empty.html \
-f empty.png \
-l=en \
-t="ZIM corner cases" \
-d="" \
-c="" \
-p="" \
zimwriterfs --withoutFTIndex --dont-check-arguments \
-w empty.html \
-I empty.png \
-l en \
-t "ZIM corner cases" \
-d "" \
-c "" \
-p "" \
corner_cases \
corner_cases.zim \
&& echo 'corner_cases.zim was successfully created' \

View File

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

View File

@@ -801,8 +801,14 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
lib.addBook(lib.getBookByIdThreadSafe(id));
}
EXPECT_GT(lib.getRevision(), rev);
const uint64_t rev2 = lib.getRevision();
EXPECT_EQ(9u, lib.removeBooksNotUpdatedSince(rev));
EXPECT_GT(lib.getRevision(), rev2);
EXPECT_FILTER_RESULTS(kiwix::Filter(),
"Islam Stack Exchange",
"Movies & TV Stack Exchange",

View File

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

View File

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

View File

@@ -37,34 +37,34 @@ TEST(OpdsCatalog, getSearchUrl)
}
{
Filter f;
f.query("abc def");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def");
f.query("abc def#xyz");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def%23xyz");
}
{
Filter f;
f.category("ted");
EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted");
f.category("ted&bob");
EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted%26bob");
}
{
Filter f;
f.lang("eng");
EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng");
f.lang("eng,fra");
EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng%2Cfra");
}
{
Filter f;
f.name("second");
EXPECT_SEARCH_URL("/catalog/v2/entries?name=second");
f.name("second?");
EXPECT_SEARCH_URL("/catalog/v2/entries?name=second%3F");
}
{
Filter f;
f.acceptTags({"paper", "plastic"});
EXPECT_SEARCH_URL("/catalog/v2/entries?tag=paper;plastic");
f.acceptTags({"#paper", "#plastic"});
EXPECT_SEARCH_URL("/catalog/v2/entries?tag=%23paper%3B%23plastic");
}
{
Filter f;
f.query("abc");
f.category("ted");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc&category=ted");
f.query("abc=123");
f.category("@ted");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%3D123&category=%40ted");
}
{
Filter f;
@@ -79,7 +79,7 @@ TEST(OpdsCatalog, getSearchUrl)
f.lang("html");
f.name("edsonarantesdonascimento");
f.acceptTags({"body", "script"});
EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body;script");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body%3Bscript");
}
#undef EXPECT_SEARCH_URL
}

235
test/otherTools.cpp Normal file
View File

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

View File

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

View File

@@ -6,6 +6,7 @@
#define SERVER_PORT 8101
#include "server_testing_tools.h"
std::string makeSearchResultsHtml(const std::string& pattern,
const std::string& header,
const std::string& results,
@@ -195,7 +196,7 @@ struct SearchResult
const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
SEARCH_RESULT(
/*link*/ "/ROOT/content/zimfile/A/Genius_+_Soul_=_Jazz",
/*link*/ "/ROOT/content/zimfile/A/Genius_%2B_Soul_%3D_Jazz",
/*title*/ "Genius + Soul = Jazz",
/*snippet*/ R"SNIPPET(...Grammy Hall of Fame in 2011. It was re-issued in the UK, first in 1989 on the Castle Communications "Essential Records" label, and by Rhino Records in 1997 on a single CD together with Charles' 1970 My Kind of <b>Jazz</b>. In 2010, Concord Records released a deluxe edition comprising digitally remastered versions of Genius + Soul = <b>Jazz</b>, My Kind of <b>Jazz</b>, <b>Jazz</b> Number II, and My Kind of <b>Jazz</b> Part 3. Professional ratings Review scores Source Rating Allmusic link Warr.org link Encyclopedia of Popular Music...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -235,7 +236,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays:_The_Music_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays%3A_The_Music_of_Ray_Charles",
/*title*/ "Catchin&apos; Some Rays: The Music of Ray Charles",
/*snippet*/ R"SNIPPET(...<b>jazz</b> singer Roseanna Vitro, released in August 1997 on the Telarc <b>Jazz</b> label. Catchin' Some Rays: The Music of Ray Charles Studio album by Roseanna Vitro Released August 1997 Recorded March 26, 1997 at Sound on Sound, NYC April 4,1997 at Quad Recording Studios, NYC Genre Vocal <b>jazz</b> Length 61:00 Label Telarc <b>Jazz</b> CD-83419 Producer Paul Wickliffe Roseanna Vitro chronology Passion Dance (1996) Catchin' Some Rays: The Music of Ray Charles (1997) The Time of My Life: Roseanna Vitro Sings the Songs of......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -243,7 +244,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say:_John_Scofield_Plays_the_Music_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say%3A_John_Scofield_Plays_the_Music_of_Ray_Charles",
/*title*/ "That&apos;s What I Say: John Scofield Plays the Music of Ray Charles",
/*snippet*/ R"SNIPPET(That's What I Say: John Scofield Plays the Music of Ray Charles Studio album by John Scofield Released June 7, 2005 (2005-06-07) Recorded December 2004 Studio Avatar Studios, New York City Genre <b>Jazz</b> Length 65:21 Label Verve Producer Steve Jordan John Scofield chronology EnRoute: John Scofield Trio LIVE (2004) That's What I Say: John Scofield Plays the Music of Ray Charles (2005) Out Louder (2006) Professional ratings Review scores Source Rating Allmusic All About <b>Jazz</b> All About <b>Jazz</b>...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -283,7 +284,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again:_Celebrating_the_Genius_of_Ray_Charles",
/*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again%3A_Celebrating_the_Genius_of_Ray_Charles",
/*title*/ "Here We Go Again: Celebrating the Genius of Ray Charles",
/*snippet*/ R"SNIPPET(...and <b>jazz</b> trumpeter Wynton Marsalis. It was recorded during concerts at the Rose Theater in New York City, on February 9 and 10, 2009. The album received mixed reviews, in which the instrumentation of Marsalis' orchestra was praised by the critics. Here We Go Again: Celebrating the Genius of Ray Charles Live album by Willie Nelson and Wynton Marsalis Released March 29, 2011 (2011-03-29) Recorded February 9 10 2009 Venue Rose Theater, New York Genre <b>Jazz</b>, country Length 61:49 Label Blue Note......)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -355,7 +356,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
),
SEARCH_RESULT(
/*link*/ "/ROOT/content/zimfile/A/Ray_Sings,_Basie_Swings",
/*link*/ "/ROOT/content/zimfile/A/Ray_Sings%2C_Basie_Swings",
/*title*/ "Ray Sings, Basie Swings",
/*snippet*/ R"SNIPPET(...from 1973 with newly recorded instrumental tracks by the contemporary Count Basie Orchestra. Professional ratings Review scores Source Rating AllMusic Ray Sings, Basie Swings Compilation album by Ray Charles, Count Basie Orchestra Released October 3, 2006 (2006-10-03) Recorded Mid-1970s, February - May 2006 Studio Los Angeles Genre Soul, <b>jazz</b>, Swing Label Concord/Hear Music Producer Gregg Field Ray Charles chronology Genius &amp; Friends (2005) Ray Sings, Basie Swings (2006) Rare Genius: The Undiscovered Masters (2010)...)SNIPPET",
/*bookTitle*/ "Ray Charles",
@@ -555,7 +556,7 @@ const std::vector<SearchResult> LARGE_SEARCH_RESULTS = {
//
// In order to be able to share the same expected output data
// LARGE_SEARCH_RESULTS between multiple build platforms and test-points
// of the ServerTest.searchResults test-case
// of the ServerSearchTest.searchResults test-case
//
// 1. Snippets are excluded from the plain-text comparison of actual and
// expected HTML strings. This is done with the help of the
@@ -916,7 +917,7 @@ struct TestData
}
};
TEST_F(ServerTest, searchResults)
TEST(ServerSearchTest, searchResults)
{
const TestData testData[] = {
{
@@ -1340,14 +1341,12 @@ TEST_F(ServerTest, searchResults)
/* pagination */ {}
},
// Only RayCharles is in English.
// [TODO] We should extend our test data to have another zim file in english returning results.
{
/* query */ "pattern=travel"
"&books.filter.lang=eng",
/* start */ 0,
/* resultsPerPage */ 10,
/* totalResultCount */ 1,
/* totalResultCount */ 2,
/* firstResultIndex */ 1,
/* results */ {
SEARCH_RESULT(
@@ -1357,6 +1356,14 @@ TEST_F(ServerTest, searchResults)
/*bookTitle*/ "Ray Charles",
/*wordCount*/ "204"
),
SEARCH_RESULT(
/*link*/ "/ROOT/content/example/Wikibooks.html",
/*title*/ "Wikibooks",
/*snippet*/ R"SNIPPET(...<b>Travel</b> guide Wikidata Knowledge database Commons Media repository Meta Coordination MediaWiki MediaWiki software Phabricator MediaWiki bug tracker Wikimedia Labs MediaWiki development The Wikimedia Foundation is a non-profit organization that depends on your voluntarism and donations to operate. If you find Wikibooks or other projects hosted by the Wikimedia Foundation useful, please volunteer or make a donation. Your donations primarily helps to purchase server equipment, launch new projects......)SNIPPET",
/*bookTitle*/ "Wikibooks",
/*wordCount*/ "538"
)
},
/* pagination */ {}
},
@@ -1451,15 +1458,86 @@ TEST_F(ServerTest, searchResults)
},
};
ZimFileServer zfs(SERVER_PORT, ZimFileServer::DEFAULT_OPTIONS,
"./test/lib_for_server_search_test.xml");
for ( const auto& t : testData ) {
const std::string htmlSearchUrl = t.url();
const auto htmlRes = zfs1_->GET(htmlSearchUrl.c_str());
const auto htmlRes = zfs.GET(htmlSearchUrl.c_str());
EXPECT_EQ(htmlRes->status, 200);
t.checkHtml(htmlRes->body);
const std::string xmlSearchUrl = t.xmlSearchUrl();
const auto xmlRes = zfs1_->GET(xmlSearchUrl.c_str());
const auto xmlRes = zfs.GET(xmlSearchUrl.c_str());
EXPECT_EQ(xmlRes->status, 200);
t.checkXml(xmlRes->body);
}
}
std::string expectedConfusionOfTonguesErrorHtml(std::string url)
{
return R"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Invalid request</title>
</head>
<body>
<h1>Invalid request</h1>
<p>
The requested URL ")" + url + R"(" is not a valid request.
</p>
<p>
Two or more books in different languages would participate in search, which may lead to confusing results.
</p>
</body>
</html>
)";
}
std::string expectedConfusionOfTonguesErrorXml(std::string url)
{
return R"(<?xml version="1.0" encoding="UTF-8">
<error>Invalid request</error>
<detail>The requested URL ")" + url + R"(" is not a valid request.</detail>
<detail>Two or more books in different languages would participate in search, which may lead to confusing results.</detail>
)";
}
TEST(ServerSearchTest, searchInMultilanguageBookSetIsDenied)
{
const std::string testQueries[] = {
"pattern=towerofbabel",
"pattern=babylon&books.filter.maxsize=1000000",
"pattern=baby&books.id=" RAYCHARLESZIMID "&books.id=" EXAMPLEZIMID,
};
// The default limit on the number of books in a multi-zim search is 3
const ZimFileServer::FilePathCollection ZIMFILES{
"./test/zimfile.zim", // eng
"./test/example.zim", // en
"./test/corner_cases.zim" // =en
};
ZimFileServer zfs(SERVER_PORT, ZimFileServer::DEFAULT_OPTIONS, ZIMFILES);
for ( const auto& q : testQueries ) {
{
// HTML mode
const std::string url = "/ROOT/search?" + q;
const auto r = zfs.GET(url.c_str());
const TestContext ctx{ {"url", url} };
EXPECT_EQ(r->status, 400) << ctx;
EXPECT_EQ(r->body, expectedConfusionOfTonguesErrorHtml(url)) << ctx;
}
{
// XML mode
const std::string url = "/ROOT/search?" + q + "&format=xml";
const auto r = zfs.GET(url.c_str());
const TestContext ctx{ {"url", url} };
EXPECT_EQ(r->status, 400) << ctx;
EXPECT_EQ(r->body, expectedConfusionOfTonguesErrorXml(url)) << ctx;
}
}
}

View File

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

View File

@@ -105,4 +105,93 @@ TEST(stringTools, extractFromString)
ASSERT_THROW(extractFromString<float>("3.14.5"), std::invalid_argument);
}
};
namespace URLEncoding
{
const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const char digits[] = "0123456789";
const char nonEncodableSymbols[] = ".-_~()*!/";
const char uriDelimSymbols[] = ":@?=+&#$;,";
const char otherSymbols[] = R"(`%^[]{}\|"<>)";
const char whitespace[] = " \n\t\r";
const char someNonASCIIChars[] = "Σ♂♀ツ";
}
TEST(stringTools, urlEncode)
{
using namespace URLEncoding;
EXPECT_EQ(urlEncode(letters), letters);
EXPECT_EQ(urlEncode(digits), digits);
EXPECT_EQ(urlEncode(nonEncodableSymbols), nonEncodableSymbols);
EXPECT_EQ(urlEncode(uriDelimSymbols), "%3A%40%3F%3D%2B%26%23%24%3B%2C");
EXPECT_EQ(urlEncode(otherSymbols), "%60%25%5E%5B%5D%7B%7D%5C%7C%22%3C%3E");
EXPECT_EQ(urlEncode(whitespace), "%20%0A%09%0D");
EXPECT_EQ(urlEncode(someNonASCIIChars), "%CE%A3%E2%99%82%E2%99%80%E3%83%84");
}
TEST(stringTools, urlDecode)
{
using namespace URLEncoding;
const std::string allTestChars = std::string(letters)
+ digits
+ nonEncodableSymbols
+ uriDelimSymbols
+ otherSymbols
+ whitespace
+ someNonASCIIChars;
for ( const char c : allTestChars ) {
const std::string str(1, c);
EXPECT_EQ(urlDecode(urlEncode(str), true), str);
}
EXPECT_EQ(urlDecode(urlEncode(allTestChars), true), allTestChars);
const std::string encodedUriDelimSymbols = urlEncode(uriDelimSymbols);
EXPECT_EQ(urlDecode(encodedUriDelimSymbols, false), encodedUriDelimSymbols);
}
TEST(stringTools, uriEncode)
{
const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
EXPECT_EQ(uriEncode(URIComponentKind::PATH, letters), letters);
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, letters), letters);
const char digits[] = "0123456789";
EXPECT_EQ(uriEncode(URIComponentKind::PATH, digits), digits);
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, digits), digits);
const char nonEncodableSymbols[] = ".-_~()*!/";
EXPECT_EQ(uriEncode(URIComponentKind::PATH, nonEncodableSymbols), nonEncodableSymbols);
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, nonEncodableSymbols), nonEncodableSymbols);
const char uriDelimSymbols[] = ":@?=+&#$;,";
EXPECT_EQ(uriEncode(URIComponentKind::PATH, uriDelimSymbols), "%3A%40%3F=+&%23%24%3B%2C");
EXPECT_EQ(uriEncode(URIComponentKind::QUERY, uriDelimSymbols), ":@?%3D%2B%26%23%24%3B%2C");
const char otherSymbols[] = R"(`%^[]{}\|"<>)";
EXPECT_EQ(uriEncode(URIComponentKind::PATH, otherSymbols), "%60%25%5E%5B%5D%7B%7D%5C%7C%22%3C%3E");
EXPECT_EQ(uriEncode(URIComponentKind::PATH, otherSymbols), uriEncode(URIComponentKind::QUERY, otherSymbols));
const char whitespace[] = " \n\t\r";
EXPECT_EQ(uriEncode(URIComponentKind::PATH, whitespace), "%20%0A%09%0D");
EXPECT_EQ(uriEncode(URIComponentKind::PATH, whitespace), uriEncode(URIComponentKind::QUERY, whitespace));
const char someNonASCIIChars[] = "Σ♂♀ツ";
EXPECT_EQ(uriEncode(URIComponentKind::PATH, someNonASCIIChars), "%CE%A3%E2%99%82%E2%99%80%E3%83%84");
EXPECT_EQ(uriEncode(URIComponentKind::PATH, someNonASCIIChars), uriEncode(URIComponentKind::QUERY, someNonASCIIChars));
}
} // unnamed namespace