Compare commits

...

132 Commits

Author SHA1 Message Date
Matthieu Gautier
e22e073d43 Merge pull request #747 from kiwix/version_10.1.1 2022-04-12 11:32:06 +02:00
Matthieu Gautier
6dcf4ee034 New version 10.1.1 2022-04-11 17:13:58 +02:00
Kelson
61ccbc65fb Merge pull request #743 from kiwix/fix_article_count
Correctly detect the number of article for zim version <= 6
2022-04-06 17:28:51 +02:00
Matthieu Gautier
85a9d35488 Correctly detect the number of article for zim version <= 6 2022-04-06 17:21:14 +02:00
Matthieu Gautier
a17258fcc9 Merge pull request #744 from kiwix/fullsearch_text_unavailable_error 2022-04-06 15:14:18 +02:00
Veloman Yunkan
ae1bf39023 Got rid of static/templates/no_search_result.html
The "Fulltext search unavailable" error page is now generated using the
static/templates/error.html template. Also added two test cases checking
that error page.
2022-04-06 14:42:29 +02:00
Veloman Yunkan
dbcbdff275 Added an optional CSS link to error.html 2022-04-05 20:49:09 +04:00
Matthieu Gautier
c1823b8ee4 Merge pull request #738 from kiwix/HTTPErrorHtmlResponse 2022-04-04 18:47:12 +02:00
Veloman Yunkan
3f41ce8337 Unit test for HTTP 500 Internal Server Error
Currently an internal server error can be triggered by accessing an
entry belonging to a redirect loop. The ZIM file (test/data/poor.zim)
containing such a loop was copied from openzim/zim-tools repository
(test/data/zimfiles/poor.zim).
2022-04-04 18:35:20 +02:00
Veloman Yunkan
2a20e87341 Got rid of Response::build_500()
This change is not tested (mostly due to the difficulties of triggering
an internal server error).
2022-04-04 18:35:20 +02:00
Veloman Yunkan
2028bf3a98 Fixed the CI build failure under android_arm* 2022-04-04 18:35:20 +02:00
Veloman Yunkan
545d409150 Reused HTTPErrorHtmlResponse in HTTP400HtmlResponse 2022-04-04 18:35:20 +02:00
Veloman Yunkan
89dc9afc28 Renamed 404.html to error.html
404.html no longer contains anything specific to the 404 error and will
henceforth serve (with some enhancements) as a general purpose error
page template.
2022-04-04 18:35:20 +02:00
Veloman Yunkan
647118dd5e Enter HTTPErrorHtmlResponse
In addition to serving as a base class for `HTTP404HtmlResponse`,
`HTTPErrorHtmlResponse` is going to be used for a couple of other error
pages.
2022-04-04 18:35:20 +02:00
Veloman Yunkan
d8a60db739 Preparing for a single error page template 2022-04-04 18:35:20 +02:00
Veloman Yunkan
f4059f3faf Got rid of withTaskbarInfo() 2022-04-04 18:35:20 +02:00
Veloman Yunkan
800cc5b68a Got rid of Response::build_404() 2022-04-04 18:35:19 +02:00
Kelson
b1f03385e4 Merge pull request #739 from kiwix/fix_windows_extern 2022-04-03 13:36:21 +02:00
Matthieu Gautier
feb30d08aa Correctly define the variable urlNotFoundMsg and invalidUrlMsg.
As we must declare the two variables as `extern` in response.h,
we must define it somewhere (and `response.cpp` is a good place).
2022-04-01 11:58:57 +02:00
Matthieu Gautier
95d4dd63ac Merge pull request #724 from kiwix/search_improvement 2022-03-29 14:42:24 +02:00
Matthieu Gautier
311f783ea9 Always use the search pattern when searching in the server.
There is no reason to not use the pattern if there is a geo_query.
If both the pattern and the qeo_query are provided, we must use both.
2022-03-29 14:06:19 +02:00
Matthieu Gautier
f2a1c0f106 Add braces around for loop's body. 2022-03-29 14:05:45 +02:00
Matthieu Gautier
2cc4befb12 Correctly display searchpattern in search result page.
The `searchPattern` is already "diples encoded".
So we can simply using it without protecting us from script injection.

Fix #723
2022-03-29 14:05:45 +02:00
Matthieu Gautier
3641dbf14d Handle book without xapian index. 2022-03-29 14:05:45 +02:00
Matthieu Gautier
1962262f94 Correctly handle invalid book.
If user request for a non existent book, we must return a 400 page.
(This is done by throwing a `std::invalid_argument` and let the catch
handle it)
2022-03-29 14:05:45 +02:00
Matthieu Gautier
7407f30790 Better cache usage.
It is better to directly try to get the `Search` from the cache instead
of getting the `Searcher` first which could be useless in Search already
exist.
2022-03-29 14:05:45 +02:00
Matthieu Gautier
d740ffe465 Introduce SearchInfo.
SearchInfo is a small helper structure to store information about the
queried search. It regroup already existing information (`patternString`,
geo query, ...) in one structure.
It is also used as key in the cache instead of using a generated string.
2022-03-29 14:05:39 +02:00
Matthieu Gautier
e7293346be Return http 400 error response when needed. 2022-03-28 17:37:41 +02:00
Matthieu Gautier
b1643e422e Introduce HTTP400HtmlResponse.
HTTP400HtmlResponse is build on the same design than HTTP404HtmlResponse.
2022-03-28 17:35:15 +02:00
Kelson
574c1ad690 Merge pull request #736 from kiwix/pin_jinja2_doc
Remove pinning of Sphinx<4
2022-03-28 15:50:17 +02:00
Matthieu Gautier
59364a737a [WIP] Remove pinning of Sphinx<4
It seems we add this pinning to fix a dependencies issue.
Let's remove it.
2022-03-28 15:37:05 +02:00
Kelson
49f24d18df Merge pull request #732 from kiwix/HTTP404HtmlResponse
New way of building 404 error HTML responses
2022-03-28 15:27:46 +02:00
Veloman Yunkan
ec2e10b40e Moved taskbarInfo into ContentResponseBlueprint 2022-03-28 14:56:40 +02:00
Veloman Yunkan
2da8ea1650 Moved function definition to cpp 2022-03-28 14:56:40 +02:00
Veloman Yunkan
0eb8f09f79 One more victory of HTTP404HtmlResponse
One more instance of `Response::build_404()` & `withTaskbarInfo()`
was taken over by `HTTP404HtmlResponse`.
2022-03-28 14:56:40 +02:00
Veloman Yunkan
0ecbdbcf63 Enter TaskbarInfo
After this change it's time to say thank you and good-bye to
`withTaskbarInfo()`. But it will take a while.
2022-03-28 14:56:40 +02:00
Veloman Yunkan
9bc09a815c noSuchBookErrorMsg() 2022-03-28 14:56:40 +02:00
Veloman Yunkan
48d377ca44 HTTP404HtmlResponse::operator+(const std::string&) 2022-03-28 14:56:40 +02:00
Veloman Yunkan
d5ae92e4e2 More uses of HTTP404HtmlResponse 2022-03-28 14:56:40 +02:00
Veloman Yunkan
1a5e2eda0f HTTP404HtmlResponse::operator+(UrlNotFoundMsg) 2022-03-28 14:56:40 +02:00
Veloman Yunkan
89785a259a Enter HTTP404HtmlResponse 2022-03-28 14:56:40 +02:00
Veloman Yunkan
668063205c Enter UrlNotFoundMsg iomanipulator-like class 2022-03-28 14:56:40 +02:00
Veloman Yunkan
df98c58d07 Enter ContentResponseBlueprint 2022-03-28 14:56:40 +02:00
Veloman Yunkan
ff8da65c68 Separated make404ResponseData() 2022-03-28 14:56:40 +02:00
Veloman Yunkan
ae60ba806b Made 404.html error template a little more generic
The fact that an info message was moved into C++ code is temporary
since it will be moved to a message resource file soon.
2022-03-28 14:56:40 +02:00
Veloman Yunkan
8cfcf2ea86 A new overload of Response::build_404() 2022-03-28 14:56:40 +02:00
Veloman Yunkan
26c16bb1b2 Renamed a variable 2022-03-28 14:56:40 +02:00
Veloman Yunkan
ca965d448f Got rid of 2 parameters in Response::build_404()
Instead of passing the `bookName` and `bookTitle` parameters to
`Response::build_404()`, `withTaskbarInfo()` is applied to its result
when needed. Note, that in `InternalServer::handle_raw()`
`withTaskbarInfo()` was not utilized since the results of the `/raw`
endpoint are not supposed to be decorated with a taskbar.
2022-03-28 14:56:40 +02:00
Veloman Yunkan
6d16d7386d Changed the signature of ContentResponse::set_taskbar() 2022-03-28 14:56:40 +02:00
Veloman Yunkan
40e9a19c48 Introduced withTaskbarInfo() helper function
This was done in preparation for removing the `bookName` and `bookTitle`
parameters from `Response::build_404()`, but since the new function
could already be put to some use in this commit that was done too.
2022-03-28 14:56:40 +02:00
Veloman Yunkan
d487c78ea4 Changed the return type of Response::build_404() 2022-03-28 14:56:40 +02:00
Veloman Yunkan
96cbd2bf26 kiwix::onlyAsNonEmptyMustacheValue() 2022-03-28 14:56:40 +02:00
Matthieu Gautier
941c3b5df3 Merge pull request #734 from kiwix/br_10.1.0 2022-03-24 18:55:38 +01:00
Matthieu Gautier
b9e40def88 New version 10.1.0 2022-03-24 18:26:35 +01:00
Kelson
116ecd1c78 Merge pull request #733 from kiwix/kelson42-patch-1
Add release badge
2022-03-24 17:45:54 +01:00
Kelson
8f2faf37dc Add release badge 2022-03-24 17:45:03 +01:00
Matthieu Gautier
ddc4c3ec2c Merge pull request #727 from kiwix/testing_of_ft_search_unavailable_page 2022-03-23 15:06:47 +01:00
Veloman Yunkan
511261cc81 Testing of "Fulltext search unavailable" page 2022-03-18 15:57:11 +04:00
Veloman Yunkan
aaf232bee4 Support for CSS URL in HTML response tests 2022-03-18 15:56:19 +04:00
Veloman Yunkan
a3460f6f48 Supporting varying page title in HTML response tests 2022-03-18 15:50:25 +04:00
Veloman Yunkan
e4a4b2f961 Extracted CSS out of no_search_results.html 2022-03-18 15:46:54 +04:00
Veloman Yunkan
389d29c92e Searching in a non-existent book is a 404 case 2022-03-18 15:46:41 +04:00
Veloman Yunkan
c64fce52e7 Made 404 HTML template consistent with the rest 2022-03-18 15:46:01 +04:00
Kelson
a5baafd09f Merge pull request #725 from kiwix/safer_testing_of_html_responses
Safer testing of HTML responses
2022-03-18 07:03:02 +01:00
Veloman Yunkan
ed46541b6f Clean-up promised in the previous commit 2022-03-11 22:53:46 +04:00
Veloman Yunkan
e93ccd18d4 Robust test data in ServerTest.404WithBodyTesting
Before this change the meaning of test data in the ServerTest.404WithBodyTesting
unit test entirely depended on the number of entries:

- 2 entries: url, expected body
- 4 entries: url, book name, book title and expected body

This was fragile and non scalable (if other combinations of expected
response data are needed).

This commit defines a mini-DSL taking advantage of operator overloading
that allows to define test data in a robust way with the help of the compiler.

Some code in `TestContentIn404HtmlResponse` is obsoleted by this change
however it is not removed in this commit so that the change is easier to
understand. That will be done next.
2022-03-11 22:53:38 +04:00
Kelson
f893777dc0 Merge pull request #721 from kiwix/xssVul
Use encoded URLs for searchSuggestionHtml
2022-03-09 14:33:11 +01:00
Nikhil Tanwar
04d682486a Add some tests to emulate XSS attack 2022-03-09 06:31:24 +01:00
Nikhil Tanwar
8136138492 use encoded URLs for searchSuggestionHtml
Previously, the seachURL was not encoded.
This resulted in an XSS vulnerability, a concept of proof is:

start kiwix-serve
visit - http://192.168.18.1:8081/"><svg onload="alert(1)">
This would display an alert message.

This encodes the searchURL before passing it to searchSuggestionHtml
2022-03-09 06:31:24 +01:00
Matthieu Gautier
e48b550b68 Merge pull request #620 from kiwix/search_caching 2022-03-08 18:12:33 +01:00
Maneesh P M
6523d9f563 Retrieve SuggestionSearcher from LRU Cache
We create a cache for SuggestionSearcher very similar to that of FT
searcher. User can specify a custom cache size using the environment
variable SUGGESTION_SEARCHER_CACHE_SIZE. It has a default value of 10%
of the number of books in the library.
2022-03-08 17:35:39 +01:00
Maneesh P M
7cb4c1361f Retrieve Searcher and Search from LRU Cache
We use the new cache template to implement two kind of cache.
1: The Searcher cache is more general in terms of its usage. A Searcher
   can be used for multiple searches without much change to itself. We
   try to retrieve the searcher and perform searches using it whenever
   possible, and if not we put a searcher into the cache. User can
   specify a custom cache length by manipulating the environment
   variable SEARCHER_CACHE_SIZE. It's default value is 10% of all the
   books available.
2: The search cache is much more restricted in terms of usage. It's main
   purpose is to avoid re-searching on the searcher during page changes
   to generate SearchResultSet of various ranges. User can specify a
   custom cache length using the environment variable SEARCH_CACHE_SIZE
   with a default value of 2;
2022-03-08 17:35:39 +01:00
Maneesh P M
a51f8d66a7 Introduce a LRU Cache and concurrent cache
The cache is copied from libzim project : https://github.com/openzim/libzim
The exact file as been copied from commit 27f5e70
2022-03-08 17:34:27 +01:00
Kelson
833bbc89ba Merge pull request #701 from kiwix/ladakhISO
Add mappings for languages not given by libicu
2022-03-05 17:07:48 +01:00
Emmanuel Engelhart
4bd02f07eb Beautify slightly the code 2022-03-05 16:59:15 +01:00
Nikhil Tanwar
9488842416 Add dagbani language in language map
Adds dagbani (dag) language in iso639_3 language map
2022-03-05 16:51:59 +01:00
Nikhil Tanwar
34b50ba30e Add mappings for languages not given by libicu
Adds a std::map<std::string, std::string> with display names for language codes not given by libicu
Fault language codes are taken from library.kiwix.org
2022-03-05 16:51:59 +01:00
Kelson
cfab560d74 Merge pull request #718 from kiwix/fix_booktitle_renderer
Fix search rendered for book title
2022-03-04 21:30:37 +01:00
Matthieu Gautier
422f4c7dd7 Reuse constructor when creating the SearchRenderer with basic constructor. 2022-03-04 17:08:59 +01:00
Matthieu Gautier
cc3545ac3b fixup! Readd a SearchRenderer constructor without Library argument. 2022-03-04 17:07:41 +01:00
Matthieu Gautier
609bc24cbe Small cleanups.
- Remove unused `archive`
- Replace tab by spaces
2022-02-25 15:46:13 +01:00
Matthieu Gautier
d9124ed40b Set the book title only if we have a library. 2022-02-25 15:46:13 +01:00
Matthieu Gautier
921671eb4d Do not use ostringstream to convert the uuid into string.
`zim::Uuid` already have a string convertion operator. Let's use it.
2022-02-25 15:46:13 +01:00
Matthieu Gautier
ec18eb40ea Readd a SearchRenderer constructor without Library argument.
Adding the library argument breaks the API. It is better to add
another constructor to not have to create another major version.
2022-02-25 15:46:13 +01:00
Matthieu Gautier
a11abcf480 Merge pull request #715 from kiwix/opds_dc_issued 2022-02-23 14:39:14 +01:00
Veloman Yunkan
ae2d7d20dc Handling of <dc:issued> in OPDS import 2022-02-23 14:20:49 +01:00
Veloman Yunkan
42ee14c8f5 New unit-test LibraryOpdsImportTest.allInOne 2022-02-21 23:20:35 +04:00
Veloman Yunkan
afb556bf64 Added <dc:issued> field to OPDS entries 2022-02-19 11:35:44 +04:00
Kelson
5c38300504 Merge pull request #713 from kiwix/use-local-include
Fix library.h include
2022-02-17 13:08:56 +01:00
Emmanuel Engelhart
cb2226c11f Fix library.h include 2022-02-17 10:56:46 +01:00
Matthieu Gautier
4cce4dce0b Merge pull request #710 from kiwix/unittests_for_404_html_page 2022-02-16 14:38:23 +01:00
Veloman Yunkan
34d069e61f Two more 404 error tests for the /raw endpoint 2022-02-16 14:25:43 +01:00
Veloman Yunkan
7a6562395a Testing of /ROOT/zimfile/invalid-article 2022-02-16 14:23:11 +01:00
Veloman Yunkan
92f9ee9280 Preparing to test archive dependent 404 responses 2022-02-16 14:23:11 +01:00
Veloman Yunkan
ae2d9b234f More test points in ServerTest.404WithBodyTesting 2022-02-16 14:23:11 +01:00
Veloman Yunkan
0ba452aece New unit-test ServerTest.404WithBodyTesting
The `ServerTest.RandomOnNonExistentBook` unit test was replaced with a
more general one testing multiple 404 scenarios where the content of the
body is checked too.
2022-02-16 14:23:11 +01:00
Veloman Yunkan
5f4256b900 Enter helper function makeExpected404Response() 2022-02-16 14:23:11 +01:00
Veloman Yunkan
a34dc725f9 ServerTest.RandomOnNonExistentBook
This test was introduced with the purpose of testing the error message
in the 404 page returned by /random for a non-existent book. The actual
expected output currently present in this new unit-test is too much for
that purpose and may become a maintenance burden if more tests of that
kind are added.
2022-02-16 14:23:11 +01:00
Kelson
892db07a2d Merge pull request #705 from thavelick/book_title_in_search_results
Add book titles to search results
2022-02-16 12:59:38 +01:00
Tristan Havelick
58be502f3f add book titles to search results 2022-02-16 12:50:18 +01:00
Matthieu Gautier
62ba2f4861 Merge pull request #709 from kiwix/unittest_for_suggestions 2022-02-16 11:30:26 +01:00
Veloman Yunkan
c782cc718a Workaround for Packages/build-deb CI failures
Packages/build-deb CI flows failed on ubuntu-bionic and ubuntu-focal
with the following mismatch in the ServerTest.suggestions unit-test:

```
[ RUN      ] ServerTest.suggestions
../test/server.cpp:715: Failure
Expected equality of these values:
  r->body
    Which is: ...
  removeEOLWhitespaceMarkers(expectedResponse)
    Which is: ...
With diff:
@@ -2,5 +2,5 @@
   {
     \"value\" : \"Ray (movie)\",
-    \"label\" : \"Ray (&lt;b&gt;movie&lt;/b&gt;...\",
+    \"label\" : \"Ray (&lt;b&gt;movie&lt;/b&gt;)\",
     \"kind\" : \"path\"
       , \"path\" : \"A/Ray_(movie)\"

Test context:
	url: /ROOT/suggest?content=zimfile&term=movie
```

For some reason (probably, a bug), the implementation of
`Xapian::MSet::snippet()` on those platforms decided that a single closing
parenthesis is more than is appropriate for inclusion in the snippet and
replaced it with a (longer) ellipsis.

Taking advantage of the necessity to work around that bug, the
ServerTest.suggestions's functional coverage was enhanced - the
problematic test point was replaced with a new one using a phrase
instead of a single term.
2022-02-14 18:17:48 +04:00
Veloman Yunkan
9a6aef4dba Moved/renamed LibraryServerTest.suggestions_in_range 2022-02-11 16:06:52 +04:00
Veloman Yunkan
943cbbf6ce New unit test ServerTest.suggestions 2022-02-11 16:06:52 +04:00
Kelson
ec94d9bfd9 Merge pull request #703 from kiwix/fix-mipsel-cross-compilation
Use "host_machine", not "target_machine" for cross-compilation
2022-02-09 07:09:56 +01:00
Emmanuel Engelhart
f2088d7fe0 Use "host_machine", not "target_machine" for cross-compilation
Read https://mesonbuild.com/Cross-compilation.html for all the rationals.
2022-02-09 07:02:14 +01:00
Kelson
19dd068e5a Merge pull request #706 from kiwix/langFix
Only add value in language object if value entry node is 'language'
2022-02-08 21:31:58 +01:00
Nikhil Tanwar
d56e56293b Only add value in language object if value entry node is 'language' 2022-02-08 19:03:35 +05:30
Kelson
dc4f9a4939 Merge pull request #700 from kiwix/ipLimit
Add method to change MHD_OPTION_PER_IP_CONNECTION_LIMIT
2022-02-06 15:12:32 +01:00
Nikhil Tanwar
261adf0ef9 Add method to change MHD_OPTION_PER_IP_CONNECTION_LIMIT
Adds new method setIpConnectionLimit() to server.
Default is 0 (infinite)
2022-02-05 18:31:42 +05:30
Matthieu Gautier
ce24b1fa5f Merge pull request #699 from kiwix/br_10.0.1 2022-02-02 16:06:18 +01:00
Matthieu Gautier
9193719c8f New version 10.0.1 2022-02-02 15:55:40 +01:00
Kelson
d0d253beed Merge pull request #698 from kiwix/legoktm-patch-1
PPA: Remove Ubuntu Hirsute, EOL
2022-02-01 08:27:50 +01:00
Kunal Mehta
cf95d513d6 PPA: Remove Ubuntu Hirsute, EOL 2022-01-31 23:13:00 -08:00
Kelson
e72c0b75f6 Merge pull request #697 from kiwix/titleWithLanguage
Add title with full language name
2022-01-29 10:01:20 +01:00
Nikhil Tanwar
4d996584fa Add title with full language name
This adds title and aria-label attributes with value as the language of book
Provides tooltip on language badges
2022-01-28 22:53:38 +05:30
Kelson
dd3338c2d0 Merge pull request #694 from kiwix/kelson42-patch-1
Fix macOS CI
2022-01-28 10:45:42 +01:00
Kelson
b19eb1ea61 Stop publishing code coverage for macOS
No reason to upload code coverage twice
2022-01-27 21:23:16 +01:00
Kelson
6d14639f77 Use Python 3.10 to fix macOS CI 2022-01-27 21:22:19 +01:00
Kelson
89e3a57a05 Merge pull request #693 from kiwix/non-minified-isotope
Add non-minified version of isotope.pkgd.js
2022-01-27 20:02:28 +01:00
Kunal Mehta
b94e4b7e3b Add non-minified version of isotope.pkgd.js
Debian wants to have the source files for minified scripts. Otherwise
same rationale as #368 which was for jquery-ui.

I downloaded this from <https://unpkg.com/isotope-layout@3.0.6/dist/isotope.pkgd.js>.
2022-01-27 00:11:04 -08:00
Kelson
68465079f0 Merge pull request #691 from kiwix/downloadLinkFix
Add span in querySelector
2022-01-24 15:43:10 +01:00
Nikhil Tanwar
f6309bb4c8 Add span in querySelector
Earlier querySelector for download button was returning a div, on which we called the getAttribute function hence returning null
This now returns a <span> element which returns the correct link with getAttribute
2022-01-24 19:23:26 +05:30
Kelson
45e9b76b19 Merge pull request #689 from kiwix/slightly-better-fix-685
Fix title='_' case too #685
2022-01-24 09:05:35 +01:00
Emmanuel Engelhart
5a9dbf85ec Fix title='_' case too #685 2022-01-24 08:35:36 +01:00
Kelson
cd412867d9 Merge pull request #688 from kiwix/legoktm-patch-1 2022-01-24 06:42:52 +01:00
Kunal Mehta
01edd830bc PPA: Fix libzim-dev dependency
Our libzim packages are "7.2.0~focal" but the ~ means that "7.2.0" is greater than
"7.2.0~focal" so the dependency can't be satisfied. Depending on "7.2.0~" will
allow "7.2.0~focal" to satisfy the dependency.
2022-01-23 20:30:23 -08:00
Kelson
ceb46f1069 Merge pull request #687 from kiwix/langCatBoxFill
Add undefined check for humanFriendlyTitle
2022-01-23 20:39:48 +01:00
Nikhil Tanwar
270773d6ba Add undefined check for humanFriendlyTitle
humanFriendlyTitle() now returns an empty string if title is undefined
which is handled in loadAndDisplayOptions()
2022-01-23 20:33:46 +01:00
Kelson
234606b170 Merge pull request #686 from kiwix/catalog_search_with_zero_count 2022-01-22 08:47:06 +01:00
Veloman Yunkan
b8328a78f6 /catalog/search?count=0 returns all entries 2022-01-21 19:31:46 +04:00
Veloman Yunkan
08c3a9d8b2 Testing of /catalog/search?count=0 2022-01-21 19:28:16 +04:00
41 changed files with 5394 additions and 459 deletions

View File

@@ -8,10 +8,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Setup python 3.5
uses: actions/setup-python@v1
- name: Setup python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.5'
python-version: '3.10'
- name: Install packages
run: |
brew update
@@ -37,17 +37,8 @@ jobs:
export LD_LIBRARY_PATH=$HOME/BUILD_native_dyn/INSTALL/lib:$HOME/BUILD_native_dyn/INSTALL/lib64
cd build
meson test --verbose
ninja coverage
env:
SKIP_BIG_MEMORY_TEST: 1
- name: Publish coverage
shell: bash
run: |
curl https://codecov.io/bash -o codecov.sh
bash codecov.sh -n osx_native_dyn -Z
rm codecov.sh
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Linux:
strategy:

View File

@@ -10,7 +10,6 @@ jobs:
distro:
- ubuntu-jammy
- ubuntu-impish
- ubuntu-hirsute
- ubuntu-focal
- ubuntu-bionic
steps:
@@ -51,14 +50,6 @@ jobs:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-hirsute
if: matrix.distro == 'ubuntu-hirsute'
name: Build package for ubuntu-hirsute
id: build-ubuntu-hirsute
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-focal
if: matrix.distro == 'ubuntu-focal'
name: Build package for ubuntu-focal

View File

@@ -1,3 +1,44 @@
libkiwix 10.1.1
===============
* Correctly detect the number of article for older zims (<=6) (@mgautier #743)
* [server] Fix fulltext search (@mgautierfr #724)
* [server][internal] New way to build Error message (@veloman-yunkan #732 #738 #744)
* Fix CI (@mgautierfr #736)
libkiwix 10.1.0
===============
This release is an important one as it fixes a Xss vulnerability introduced
in libkiwix 10.0.0
* [SECURITY] Fix a Xss attack vulnerability (introduced in 10.0.0) (@juuz0 #721)
* [server] Add a option to set a limit on the number of connexion per IP (@kelson42 #700)
* [server] Do not display a lang tag in the UI if the book has no language (@juuz0 #706)
* [server] Add the book title associated to a search results (@thavelick #705, @mgautierfr #718)
* Add `dc:issued` to opds output stream (@veloman-yunkan #715)
* Add handling of several languages not provided by ICU (@juuz0 #701)
* [server] Add a caching system for search and suggestion (@maneeshpm #620)
* Fix cross-compilation (@kelson42 #703)
* Add unit-testing of suggestions and error pages (@veloman-yunkan #709 #710 #727)
* Better testing system of html response (@veloman-yunkan #725)
libkiwix 10.0.1
===============
* [server] The catalog search interpret `count=0` as no limit.
This was the case for a long time. This was changed unintentionally
(@veloman-yunkan #686)
* [server] Correctly generere a human friendly title in the server frontend.
(@juuz0 #687, @kelson42 #689)
* [server] Fix download button if there is no url do download from.
(@juuz0 #691)
* Add non-minified isotope.pkdg.js
Needed for debian packaging as we need the source and minified version is
not the source (@legoktm #693)
* [server] Add a tooltip with the full language for the lang tag.
* CI fixes (@kelson42 @legoktm)
libkiwix 10.0.0
===============

View File

@@ -5,6 +5,7 @@ The Libkiwix provides the [Kiwix](https://kiwix.org) software suite
core. It contains the code shared by all Kiwix ports (Windows,
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)
[![CodeFactor](https://www.codefactor.io/repository/github/kiwix/libkiwix/badge)](https://www.codefactor.io/repository/github/kiwix/libkiwix)

View File

@@ -26,7 +26,7 @@ task writePom {
project {
groupId 'org.kiwix.kiwixlib'
artifactId 'kiwixlib'
version '10.0.0' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
version '10.1.1' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
packaging 'aar'
name 'kiwixlib'
url 'https://github.com/kiwix/libkiwix'

4
debian/control vendored
View File

@@ -4,7 +4,7 @@ Maintainer: Kiwix team <kiwix@kiwix.org>
Build-Depends: debhelper-compat (= 13),
meson,
pkg-config,
libzim-dev (>= 7.2.0),
libzim-dev (>= 7.2.0~),
libcurl4-gnutls-dev,
libicu-dev,
libgtest-dev,
@@ -23,7 +23,7 @@ Section: libdevel
Architecture: any
Multi-Arch: same
Depends: libkiwix10 (= ${binary:Version}), ${misc:Depends}, python3,
libzim-dev (>= 7.2.0),
libzim-dev (>= 7.2.0~),
libicu-dev,
libpugixml-dev,
libcurl4-gnutls-dev,

View File

@@ -1,3 +1,2 @@
breathe
exhale
sphinx<4

View File

@@ -22,6 +22,7 @@
#include <string>
#include <zim/search.h>
#include "library.h"
namespace kiwix
{
@@ -48,6 +49,10 @@ class SearchRenderer
/**
* Construct a SearchRenderer from a SearchResultSet.
*
* The constructed version of the SearchRenderer will not introduce
* the book name for each result. It is better to use the other constructor
* with a Library pointer to have a better html page.
*
* @param srs The `SearchResultSet` to render.
* @param mapper The `NameMapper` to use to do the rendering.
* @param start The start offset used for the srs.
@@ -56,6 +61,18 @@ class SearchRenderer
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount);
/**
* Construct a SearchRenderer from a SearchResultSet.
*
* @param srs The `SearchResultSet` to render.
* @param mapper The `NameMapper` to use to do the rendering.
* @param library The `Library` to use to look up book details for search results.
* @param start The start offset used for the srs.
* @param estimatedResultCount The estimatedResultCount of the whole search
*/
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
unsigned int start, unsigned int estimatedResultCount);
~SearchRenderer();
void setSearchPattern(const std::string& pattern);
@@ -91,6 +108,7 @@ class SearchRenderer
std::string beautifyInteger(const unsigned int number);
zim::SearchResultSet m_srs;
NameMapper* mp_nameMapper;
Library* mp_library;
std::string searchContent;
std::string searchPattern;
std::string protocolPrefix;

View File

@@ -54,6 +54,7 @@ namespace kiwix
void setAddress(const std::string& addr) { m_addr = addr; }
void setPort(int port) { m_port = port; }
void setNbThreads(int threads) { m_nbThreads = threads; }
void setIpConnectionLimit(int limit) { m_ipConnectionLimit = limit; }
void setVerbose(bool verbose) { m_verbose = verbose; }
void setIndexTemplateString(const std::string& indexTemplateString) { m_indexTemplateString = indexTemplateString; }
void setTaskbar(bool withTaskbar, bool withLibraryButton)
@@ -75,6 +76,7 @@ namespace kiwix
bool m_withTaskbar = true;
bool m_withLibraryButton = true;
bool m_blockExternalLinks = false;
int m_ipConnectionLimit = 0;
std::unique_ptr<InternalServer> mp_server;
};
}

View File

@@ -1,5 +1,5 @@
project('libkiwix', 'cpp',
version : '10.0.0', # Also change this in android-kiwix-lib-publisher/kiwixLibAndroid/build.gradle
version : '10.1.1', # Also change this in android-kiwix-lib-publisher/kiwixLibAndroid/build.gradle
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
@@ -19,11 +19,11 @@ if wrapper.contains('java')
endif
# See https://github.com/kiwix/libkiwix/issues/371
if ['arm', 'mips', 'm68k', 'ppc', 'sh4'].contains(target_machine.cpu_family())
if ['arm', 'mips', 'm68k', 'ppc', 'sh4'].contains(host_machine.cpu_family())
extra_libs += '-latomic'
endif
if (compiler.get_id() == 'gcc' and build_machine.system() == 'linux') or target_machine.system() == 'freebsd'
if (compiler.get_id() == 'gcc' and build_machine.system() == 'linux') or host_machine.system() == 'freebsd'
# C++ std::thread is implemented using pthread on linux by gcc
thread_dep = dependency('threads')
else
@@ -51,12 +51,12 @@ endif
extra_cflags = ''
if target_machine.system() == 'windows' and static_deps
if host_machine.system() == 'windows' and static_deps
add_project_arguments('-DCURL_STATICLIB', language : 'cpp')
extra_cflags += '-DCURL_STATICLIB'
endif
if target_machine.system() == 'windows'
if host_machine.system() == 'windows'
add_project_arguments('-DNOMINMAX', language: 'cpp')
endif

View File

@@ -83,7 +83,7 @@ void Book::update(const zim::Archive& archive) {
m_flavour = getMetaFlavour(archive);
m_tags = getMetaTags(archive);
m_category = getCategoryFromTags();
m_articleCount = archive.getArticleCount();
m_articleCount = getArchiveArticleCount(archive);
m_mediaCount = getArchiveMediaCount(archive);
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
@@ -161,7 +161,9 @@ void Book::updateFromOpds(const pugi::xml_node& node, const std::string& urlHost
m_language = VALUE("language");
m_creator = node.child("author").child("name").child_value();
m_publisher = node.child("publisher").child("name").child_value();
m_date = fromOpdsDate(VALUE("updated"));
const std::string dcIssuedDate = VALUE("dc:issued");
m_date = dcIssuedDate.empty() ? VALUE("updated") : dcIssuedDate;
m_date = fromOpdsDate(m_date);
m_name = VALUE("name");
m_flavour = VALUE("flavour");
m_tags = VALUE("tags");

View File

@@ -72,9 +72,7 @@ IllustrationInfo getBookIllustrationInfo(const Book& book)
kainjow::mustache::object getSingleBookData(const Book& book)
{
const MustacheData bookUrl = book.getUrl().empty()
? MustacheData(false)
: MustacheData(book.getUrl());
const auto bookDate = book.getDate() + "T00:00:00Z";
return kainjow::mustache::object{
{"id", book.getId()},
{"name", book.getName()},
@@ -82,7 +80,8 @@ kainjow::mustache::object getSingleBookData(const Book& book)
{"description", book.getDescription()},
{"language", book.getLanguage()},
{"content_id", urlEncode(book.getHumanReadableIdFromPath(), true)},
{"updated", book.getDate() + "T00:00:00Z"},
{"updated", bookDate}, // XXX: this should be the entry update datetime
{"book_date", bookDate},
{"category", book.getCategory()},
{"flavour", book.getFlavour()},
{"tags", book.getTags()},
@@ -90,7 +89,7 @@ kainjow::mustache::object getSingleBookData(const Book& book)
{"media_count", to_string(book.getMediaCount())},
{"author_name", book.getCreator()},
{"publisher_name", book.getPublisher()},
{"url", bookUrl},
{"url", onlyAsNonEmptyMustacheValue(book.getUrl())},
{"size", to_string(book.getSize())},
{"icons", getBookIllustrationInfo(book)},
};
@@ -124,13 +123,63 @@ BooksData getBooksData(const Library* library, const std::vector<std::string>& b
return booksData;
}
std::map<std::string, std::string> iso639_3 = {
{"atj", "atikamekw"},
{"azb", "آذربایجان دیلی"},
{"bcl", "central bikol"},
{"bgs", "tagabawa"},
{"bxr", "буряад хэлэн"},
{"cbk", "chavacano"},
{"cdo", "閩東語"},
{"dag", "Dagbani"},
{"diq", "dimli"},
{"dty", "डोटेली"},
{"eml", "emiliân-rumagnōl"},
{"fbs", "српскохрватски"},
{"ido", "ido"},
{"kbp", "kabɩ"},
{"kld", "Gamilaraay"},
{"lbe", "лакку маз"},
{"lbj", "ལ་དྭགས་སྐད་"},
{"map", "Austronesian"},
{"mhr", "марий йылме"},
{"mnw", "ဘာသာမန်"},
{"myn", "mayan"},
{"nah", "nahuatl"},
{"nai", "north American Indian"},
{"nds", "plattdütsch"},
{"nrm", "bhasa narom"},
{"olo", "livvi"},
{"pih", "Pitcairn-Norfolk"},
{"pnb", "Western Panjabi"},
{"rmr", "Caló"},
{"rmy", "romani shib"},
{"roa", "romance languages"},
{"twi", "twi"}
};
std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
auto lang = *icuLangPtr;
const icu::Locale locale(lang);
icu::UnicodeString ustring;
locale.getDisplayLanguage(locale, ustring);
std::string displayLanguage;
ustring.toUTF8String(displayLanguage);
std::string iso3LangCode = locale.getISO3Language();
iso639_3.insert({iso3LangCode, displayLanguage});
}
}
std::string getLanguageSelfName(const std::string& lang) {
const icu::Locale locale(lang.c_str());
icu::UnicodeString ustring;
locale.getDisplayLanguage(locale, ustring);
std::string result;
ustring.toUTF8String(result);
return result;
const auto itr = iso639_3.find(lang);
if (itr != iso639_3.end()) {
return itr->second;
}
return lang;
};
} // unnamed namespace
@@ -142,7 +191,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const s
{"date", gen_date_str()},
{"root", rootLocation},
{"feed_id", gen_uuid(libraryId + "/catalog/search?"+query)},
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
{"filter", onlyAsNonEmptyMustacheValue(query)},
{"totalResults", to_string(m_totalResults)},
{"startIndex", to_string(m_startIndex)},
{"itemsPerPage", to_string(m_count)},
@@ -162,7 +211,7 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const
{"date", gen_date_str()},
{"endpoint_root", endpointRoot},
{"feed_id", gen_uuid(libraryId + endpoint + "?" + query)},
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
{"filter", onlyAsNonEmptyMustacheValue(query)},
{"query", query.empty() ? "" : "?" + urlEncode(query)},
{"totalResults", to_string(m_totalResults)},
{"startIndex", to_string(m_startIndex)},
@@ -208,6 +257,7 @@ std::string OPDSDumper::languagesOPDSFeed() const
{
const auto now = gen_date_str();
kainjow::mustache::list languageData;
std::call_once(fillLanguagesFlag, fillLanguagesMap);
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
const std::string languageCode = langAndBookCount.first;
const int bookCount = langAndBookCount.second;

View File

@@ -26,6 +26,8 @@
#include "library.h"
#include "name_mapper.h"
#include "tools/archiveTools.h"
#include <zim/search.h>
#include <mustache.hpp>
@@ -37,18 +39,24 @@ namespace kiwix
/* Constructor */
SearchRenderer::SearchRenderer(Searcher* searcher, NameMapper* mapper)
: m_srs(searcher->getSearchResultSet()),
mp_nameMapper(mapper),
protocolPrefix("zim://"),
searchProtocolPrefix("search://?"),
estimatedResultCount(searcher->getEstimatedResultCount()),
resultStart(searcher->getResultStart())
: SearchRenderer(
searcher->getSearchResultSet(),
mapper,
nullptr,
searcher->getEstimatedResultCount(),
searcher->getResultStart())
{}
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount)
: SearchRenderer(srs, mapper, nullptr, start, estimatedResultCount)
{}
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
unsigned int start, unsigned int estimatedResultCount)
: m_srs(srs),
mp_nameMapper(mapper),
mp_library(library),
protocolPrefix("zim://"),
searchProtocolPrefix("search://?"),
estimatedResultCount(estimatedResultCount),
@@ -87,9 +95,13 @@ std::string SearchRenderer::getHtml()
result.set("title", it.getTitle());
result.set("url", it.getPath());
result.set("snippet", it.getSnippet());
std::ostringstream s;
s << it.getZimId();
result.set("resultContentId", mp_nameMapper->getNameForId(s.str()));
std::string zim_id(it.getZimId());
result.set("resultContentId", mp_nameMapper->getNameForId(zim_id));
if (!mp_library) {
result.set("bookTitle", kainjow::mustache::data(false));
} else {
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
}
if (it.getWordCount() >= 0) {
result.set("wordCount", kiwix::beautifyInteger(it.getWordCount()));

View File

@@ -49,7 +49,8 @@ bool Server::start() {
m_withTaskbar,
m_withLibraryButton,
m_blockExternalLinks,
m_indexTemplateString));
m_indexTemplateString,
m_ipConnectionLimit));
return mp_server->start();
}

View File

@@ -58,8 +58,6 @@ extern "C" {
#include <zim/uuid.h>
#include <zim/error.h>
#include <zim/search.h>
#include <zim/suggestion.h>
#include <zim/entry.h>
#include <zim/item.h>
@@ -80,6 +78,7 @@ extern "C" {
#define MAX_SEARCH_LEN 140
#define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100
#define DEFAULT_CACHE_SIZE 2
namespace kiwix {
@@ -96,8 +95,68 @@ inline std::string normalizeRootUrl(std::string rootUrl)
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
}
// Returns the value of env var `name` if found, otherwise returns defaultVal
unsigned int getCacheLength(const char* name, unsigned int defaultVal) {
try {
const char* envString = std::getenv(name);
if (envString == nullptr) {
throw std::runtime_error("Environment variable not set");
}
return extractFromString<unsigned int>(envString);
} catch (...) {}
return defaultVal;
}
} // unnamed namespace
SearchInfo::SearchInfo(const std::string& pattern)
: pattern(pattern),
geoQuery()
{}
SearchInfo::SearchInfo(const std::string& pattern, GeoQuery geoQuery)
: pattern(pattern),
geoQuery(geoQuery)
{}
SearchInfo::SearchInfo(const RequestContext& request)
: pattern(request.get_optional_param<std::string>("pattern", "")),
geoQuery(),
bookName(request.get_optional_param<std::string>("content", ""))
{
/* Retrive geo search */
try {
auto latitude = request.get_argument<float>("latitude");
auto longitude = request.get_argument<float>("longitude");
auto distance = request.get_argument<float>("distance");
geoQuery = GeoQuery(latitude, longitude, distance);
} catch(const std::out_of_range&) {}
catch(const std::invalid_argument&) {}
if (!geoQuery && pattern.empty()) {
throw std::invalid_argument("No query provided.");
}
}
zim::Query SearchInfo::getZimQuery(bool verbose) const {
zim::Query query;
if (verbose) {
std::cout << "Performing query '" << pattern<< "'";
}
query.setQuery(pattern);
if (geoQuery) {
if (verbose) {
std::cout << " with geo query '" << geoQuery.distance << "&(" << geoQuery.latitude << ";" << geoQuery.longitude << ")'";
}
query.setGeorange(geoQuery.latitude, geoQuery.longitude, geoQuery.distance);
}
if (verbose) {
std::cout << std::endl;
}
return query;
}
static IdNameMapper defaultNameMapper;
static MHD_Result staticHandlerCallback(void* cls,
@@ -120,7 +179,8 @@ InternalServer::InternalServer(Library* library,
bool withTaskbar,
bool withLibraryButton,
bool blockExternalLinks,
std::string indexTemplateString) :
std::string indexTemplateString,
int ipConnectionLimit) :
m_addr(addr),
m_port(port),
m_root(normalizeRootUrl(root)),
@@ -130,9 +190,13 @@ InternalServer::InternalServer(Library* library,
m_withLibraryButton(withLibraryButton),
m_blockExternalLinks(blockExternalLinks),
m_indexTemplateString(indexTemplateString.empty() ? RESOURCE::templates::index_html : indexTemplateString),
m_ipConnectionLimit(ipConnectionLimit),
mp_daemon(nullptr),
mp_library(library),
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper)
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper),
searcherCache(getCacheLength("SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
searchCache(getCacheLength("SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
suggestionSearcherCache(getCacheLength("SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U)))
{}
bool InternalServer::start() {
@@ -144,7 +208,6 @@ bool InternalServer::start() {
if (m_verbose.load())
flags |= MHD_USE_DEBUG;
struct sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = AF_INET;
@@ -168,6 +231,7 @@ bool InternalServer::start() {
this,
MHD_OPTION_SOCK_ADDR, &sockAddr,
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
MHD_OPTION_PER_IP_CONNECTION_LIMIT, m_ipConnectionLimit,
MHD_OPTION_END);
if (mp_daemon == nullptr) {
std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
@@ -262,8 +326,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& request)
{
try {
if (! request.is_valid_url())
return Response::build_404(*this, request.get_full_url(), "", "");
if (! request.is_valid_url()) {
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
const ETag etag = get_matching_if_none_match_etag(request);
if ( etag )
@@ -293,10 +359,12 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
return handle_content(request);
} catch (std::exception& e) {
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
return Response::build_500(*this, e.what());
return HTTP500HtmlResponse(*this, request)
+ e.what();
} catch (...) {
fprintf(stderr, "===== Unhandled unknown error\n");
return Response::build_500(*this, "Unknown error");
return HTTP500HtmlResponse(*this, request)
+ "Unknown error";
}
}
@@ -337,14 +405,15 @@ std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& r
* Archive and Zim handlers begin
**/
// TODO: retrieve searcher from caching mechanism
SuggestionsList_t getSuggestions(const zim::Archive* const archive,
const std::string& queryString, int start, int suggestionCount)
SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Archive* const archive,
const std::string& bookId, const std::string& queryString, int start, int suggestionCount)
{
SuggestionsList_t suggestions;
auto searcher = zim::SuggestionSearcher(*archive);
std::shared_ptr<zim::SuggestionSearcher> searcher;
searcher = cache.getOrPut(bookId, [=](){ return make_shared<zim::SuggestionSearcher>(*archive); });
if (archive->hasTitleIndex()) {
auto search = searcher.suggest(queryString);
auto search = searcher->suggest(queryString);
auto srs = search.getResults(start, suggestionCount);
for (auto it : srs) {
@@ -357,7 +426,7 @@ SuggestionsList_t getSuggestions(const zim::Archive* const archive,
std::vector<std::string> variants = getTitleVariants(queryString);
int currCount = 0;
for (auto it = variants.begin(); it != variants.end() && currCount < suggestionCount; it++) {
auto search = searcher.suggest(queryString);
auto search = searcher->suggest(queryString);
auto srs = search.getResults(0, suggestionCount);
for (auto it : srs) {
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
@@ -370,6 +439,20 @@ SuggestionsList_t getSuggestions(const zim::Archive* const archive,
return suggestions;
}
namespace
{
std::string noSuchBookErrorMsg(const std::string& bookName)
{
return "No such book: " + bookName;
}
std::string noSearchResultsMsg()
{
return "The fulltext search engine is not available for this content.";
}
} // unnamed namespace
std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& request)
{
@@ -377,19 +460,20 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
printf("** running handle_suggest\n");
}
std::string bookName;
std::string bookName, bookId;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
const std::string bookId = mp_nameMapper->getIdForName(bookName);
bookId = mp_nameMapper->getIdForName(bookName);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {
// error handled by the archive == nullptr check below
}
if (archive == nullptr) {
const std::string error_details = "No such book: " + bookName;
return Response::build_404(*this, "", bookName, "", error_details);
return HTTP404HtmlResponse(*this, request)
+ noSuchBookErrorMsg(bookName)
+ TaskbarInfo(bookName);
}
const auto queryString = request.get_optional_param("term", std::string());
@@ -408,7 +492,8 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
bool first = true;
/* Get the suggestions */
SuggestionsList_t suggestions = getSuggestions(archive.get(), queryString, start, count);
SuggestionsList_t suggestions = getSuggestions(suggestionSearcherCache, archive.get(),
bookId, queryString, start, count);
for(auto& suggestion:suggestions) {
MustacheData result;
result.set("label", suggestion.getTitle());
@@ -458,7 +543,8 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
response->set_cacheable();
return std::move(response);
} catch (const ResourceNotFound& e) {
return Response::build_404(*this, request.get_full_url(), "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
}
@@ -468,111 +554,92 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
printf("** running handle_search\n");
}
std::string patternString;
try {
patternString = request.get_argument("pattern");
} catch (const std::out_of_range&) {}
auto searchInfo = SearchInfo(request);
/* Retrive geo search */
bool has_geo_query = false;
float latitude = 0;
float longitude = 0;
float distance = 0;
try {
latitude = request.get_argument<float>("latitude");
longitude = request.get_argument<float>("longitude");
distance = request.get_argument<float>("distance");
has_geo_query = true;
} catch(const std::out_of_range&) {}
catch(const std::invalid_argument&) {}
std::string bookId;
std::shared_ptr<zim::Archive> archive;
if (!searchInfo.bookName.empty()) {
try {
bookId = mp_nameMapper->getIdForName(searchInfo.bookName);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {
throw std::invalid_argument("The requested book doesn't exist.");
}
}
std::string bookName;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
const std::string bookId = mp_nameMapper->getIdForName(bookName);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {}
/* Make the search */
if ( (!archive && !bookName.empty())
|| (patternString.empty() && ! has_geo_query) ) {
auto data = get_default_data();
data.set("pattern", encodeDiples(patternString));
auto response = ContentResponse::build(*this, RESOURCE::templates::no_search_result_html, data, "text/html; charset=utf-8");
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
response->set_code(MHD_HTTP_NOT_FOUND);
return std::move(response);
}
std::shared_ptr<zim::Searcher> searcher;
if (archive) {
searcher = std::make_shared<zim::Searcher>(*archive);
} else {
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
auto currentArchive = mp_library->getArchiveById(bookId);
if (currentArchive) {
if (! searcher) {
searcher = std::make_shared<zim::Searcher>(*currentArchive);
} else {
searcher->addArchive(*currentArchive);
/* Make the search */
// Try to get a search from the searchInfo, else build it
std::shared_ptr<zim::Search> search;
try {
search = searchCache.getOrPut(searchInfo,
[=](){
std::shared_ptr<zim::Searcher> searcher;
if (archive) {
searcher = searcherCache.getOrPut(bookId, [=](){ return std::make_shared<zim::Searcher>(*archive);});
} else {
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
auto currentArchive = mp_library->getArchiveById(bookId);
if (currentArchive) {
if (! searcher) {
searcher = std::make_shared<zim::Searcher>(*currentArchive);
} else {
searcher->addArchive(*currentArchive);
}
}
}
}
return make_shared<zim::Search>(searcher->search(searchInfo.getZimQuery(m_verbose.load())));
}
}
}
}
auto start = 0;
try {
start = request.get_argument<unsigned int>("start");
} catch (const std::exception&) {}
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 */
try {
zim::Query query;
if (patternString.empty()) {
// Execute geo-search
if (m_verbose.load()) {
cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl;
}
query.setQuery("");
query.setGeorange(latitude, longitude, distance);
} else {
// Execute Ft search
if (m_verbose.load()) {
cout << "Performing query `" << patternString << "'" << endl;
}
std::string queryString = removeAccents(patternString);
query.setQuery(queryString);
);
} 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)
return HTTPErrorHtmlResponse(*this, request, MHD_HTTP_NOT_FOUND,
"Fulltext search unavailable",
"Not Found",
m_root + "/skin/search_results.css")
+ noSearchResultsMsg()
+ TaskbarInfo(searchInfo.bookName, archive.get());
}
zim::Search search = searcher->search(query);
SearchRenderer renderer(search.getResults(start, pageLength), mp_nameMapper, start,
search.getEstimatedMatches());
renderer.setSearchPattern(patternString);
renderer.setSearchContent(bookName);
auto start = 0;
try {
start = request.get_argument<unsigned int>("start");
} catch (const std::exception&) {}
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, pageLength), mp_nameMapper, mp_library, start,
search->getEstimatedMatches());
renderer.setSearchPattern(searchInfo.pattern);
renderer.setSearchContent(searchInfo.bookName);
renderer.setProtocolPrefix(m_root + "/");
renderer.setSearchProtocolPrefix(m_root + "/search?");
renderer.setPageLength(pageLength);
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
response->set_taskbar(searchInfo.bookName, archive.get());
return std::move(response);
} catch (const std::invalid_argument& e) {
return HTTP400HtmlResponse(*this, request)
+ invalidUrlMsg
+ std::string(e.what());
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return Response::build_500(*this, e.what());
return HTTP500HtmlResponse(*this, request)
+ e.what();
}
}
@@ -593,8 +660,9 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
}
if (archive == nullptr) {
const std::string error_details = "No such book: " + bookName;
return Response::build_404(*this, "", bookName, "", error_details);
return HTTP404HtmlResponse(*this, request)
+ noSuchBookErrorMsg(bookName)
+ TaskbarInfo(bookName);
}
try {
@@ -602,7 +670,9 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
return build_redirect(bookName, getFinalItem(*archive, entry));
} catch(zim::EntryNotFound& e) {
const std::string error_details = "Oops! Failed to pick a random article :(";
return Response::build_404(*this, "", bookName, getArchiveTitle(*archive), error_details);
return HTTP404HtmlResponse(*this, request)
+ error_details
+ TaskbarInfo(bookName, archive.get());
}
}
@@ -613,8 +683,10 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
source = kiwix::urlDecode(request.get_argument("source"));
} catch (const std::out_of_range& e) {}
if (source.empty())
return Response::build_404(*this, request.get_full_url(), "", "");
if (source.empty()) {
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
auto data = get_default_data();
data.set("source", source);
@@ -633,7 +705,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
host = request.get_header("Host");
url = request.get_url_part(1);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request.get_full_url(), "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
if (url == "v2") {
@@ -641,7 +714,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
return Response::build_404(*this, request.get_full_url(), "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
if (url == "searchdescription.xml") {
@@ -720,7 +794,8 @@ InternalServer::search_catalog(const RequestContext& request,
const auto totalResults = bookIdsToDump.size();
const size_t count = request.get_optional_param("count", 10UL);
const size_t startIndex = request.get_optional_param("start", 0UL);
bookIdsToDump = subrange(bookIdsToDump, startIndex, count);
const size_t intendedCount = count > 0 ? count : bookIdsToDump.size();
bookIdsToDump = subrange(bookIdsToDump, startIndex, intendedCount);
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
return bookIdsToDump;
}
@@ -774,10 +849,11 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
} catch (const std::out_of_range& e) {}
if (archive == nullptr) {
std::string searchURL = m_root+"/search?pattern="+pattern; // Make a full search on the entire library.
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
return Response::build_404(*this, request.get_full_url(), bookName, "", details);
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true);
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
+ searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName);
}
auto urlStr = request.get_url().substr(bookName.size()+1);
@@ -794,7 +870,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
}
auto response = ItemResponse::build(*this, request, entry.getItem());
try {
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, getArchiveTitle(*archive));
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, archive.get());
} catch (std::bad_cast& e) {}
if (m_verbose.load()) {
@@ -807,10 +883,11 @@ 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="+pattern; // Make a search on this specific book only.
const std::string details = searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern));
return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), details);
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true);
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
+ searchSuggestionHTML(searchURL, kiwix::urlDecode(pattern))
+ TaskbarInfo(bookName, archive.get());
}
}
@@ -827,12 +904,15 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
bookName = request.get_url_part(1);
kind = request.get_url_part(2);
} catch (const std::out_of_range& e) {
return Response::build_404(*this, request.get_full_url(), bookName, "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
if (kind != "meta" && kind!= "content") {
const std::string error_details = kind + " is not a valid request for raw content.";
return Response::build_404(*this, request.get_full_url(), bookName, "", error_details);
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
+ error_details;
}
std::shared_ptr<zim::Archive> archive;
@@ -842,8 +922,9 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
} catch (const std::out_of_range& e) {}
if (archive == nullptr) {
const std::string error_details = "No such book: " + bookName;
return Response::build_404(*this, request.get_full_url(), bookName, "", error_details);
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
+ noSuchBookErrorMsg(bookName);
}
// Remove the beggining of the path:
@@ -868,7 +949,9 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
printf("Failed to find %s\n", itemPath.c_str());
}
const std::string error_details = "Cannot find " + kind + " entry " + itemPath;
return Response::build_404(*this, request.get_full_url(), bookName, getArchiveTitle(*archive), error_details);
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg
+ error_details;
}
}

View File

@@ -28,6 +28,9 @@ extern "C" {
#include "library.h"
#include "name_mapper.h"
#include <zim/search.h>
#include <zim/suggestion.h>
#include <mustache.hpp>
#include <atomic>
@@ -36,9 +39,58 @@ extern "C" {
#include "server/request_context.h"
#include "server/response.h"
#include "tools/concurrent_cache.h"
namespace kiwix {
struct GeoQuery {
GeoQuery()
: GeoQuery(0, 0, -1)
{}
GeoQuery(float latitude, float longitude, float distance)
: latitude(latitude), longitude(longitude), distance(distance)
{}
float latitude;
float longitude;
float distance;
explicit operator bool() const {
return distance >= 0;
}
friend bool operator<(const GeoQuery& l, const GeoQuery& r)
{
return std::tie(l.latitude, l.longitude, l.distance)
< std::tie(r.latitude, r.longitude, r.distance); // keep the same order
}
};
class SearchInfo {
public:
SearchInfo(const std::string& pattern);
SearchInfo(const std::string& pattern, GeoQuery geoQuery);
SearchInfo(const RequestContext& request);
zim::Query getZimQuery(bool verbose) const;
friend bool operator<(const SearchInfo& l, const SearchInfo& r)
{
return std::tie(l.bookName, l.pattern, l.geoQuery)
< std::tie(r.bookName, r.pattern, r.geoQuery); // keep the same order
}
public: //data
std::string pattern;
GeoQuery geoQuery;
std::string bookName;
};
typedef kainjow::mustache::data MustacheData;
typedef ConcurrentCache<string, std::shared_ptr<zim::Searcher>> SearcherCache;
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;
typedef ConcurrentCache<string, std::shared_ptr<zim::SuggestionSearcher>> SuggestionSearcherCache;
class Entry;
class OPDSDumper;
@@ -55,7 +107,8 @@ class InternalServer {
bool withTaskbar,
bool withLibraryButton,
bool blockExternalLinks,
std::string indexTemplateString);
std::string indexTemplateString,
int ipConnectionLimit);
virtual ~InternalServer() = default;
MHD_Result handlerCallback(struct MHD_Connection* connection,
@@ -68,7 +121,7 @@ class InternalServer {
bool start();
void stop();
std::string getAddress() { return m_addr; }
int getPort() { return m_port; }
int getPort() { return m_port; }
private: // functions
std::unique_ptr<Response> handle_request(const RequestContext& request);
@@ -108,19 +161,22 @@ class InternalServer {
bool m_withLibraryButton;
bool m_blockExternalLinks;
std::string m_indexTemplateString;
int m_ipConnectionLimit;
struct MHD_Daemon* mp_daemon;
Library* mp_library;
NameMapper* mp_nameMapper;
SearcherCache searcherCache;
SearchCache searchCache;
SuggestionSearcherCache suggestionSearcherCache;
std::string m_server_id;
std::string m_library_id;
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage, bool raw);
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item, bool raw);
friend std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg);
};
}

View File

@@ -43,7 +43,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
try {
url = request.get_url_part(2);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request.get_full_url(), "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
if (url == "root.xml") {
@@ -69,7 +70,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
} else if (url == "illustration") {
return handle_catalog_v2_illustration(request);
} else {
return Response::build_404(*this, request.get_full_url(), "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
}
@@ -110,7 +112,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
try {
mp_library->getBookById(entryId);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request.get_full_url(), "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
OPDSDumper opdsDumper(mp_library);
@@ -158,7 +161,8 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
auto illustration = book.getIllustration(size);
return ContentResponse::build(*this, illustration->getData(), illustration->mimeType);
} catch(...) {
return Response::build_404(*this, request.get_full_url(), "", "");
return HTTP404HtmlResponse(*this, request)
+ urlNotFoundMsg;
}
}

View File

@@ -25,6 +25,7 @@
#include "tools/regexTools.h"
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "tools/archiveTools.h"
#include "string.h"
#include <mustache.hpp>
@@ -83,19 +84,112 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
return response;
}
std::unique_ptr<Response> Response::build_404(const InternalServer& server, const std::string& url, const std::string& bookName, const std::string& bookTitle, const std::string& details)
const UrlNotFoundMsg urlNotFoundMsg;
const InvalidUrlMsg invalidUrlMsg;
std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObject() const
{
MustacheData results;
if ( !url.empty() ) {
results.set("url", url);
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
r->set_code(m_httpStatusCode);
if ( m_taskbarInfo ) {
r->set_taskbar(m_taskbarInfo->bookName, m_taskbarInfo->archive);
}
results.set("details", details);
return r;
}
auto response = ContentResponse::build(server, RESOURCE::templates::_404_html, results, "text/html");
response->set_code(MHD_HTTP_NOT_FOUND);
response->set_taskbar(bookName, bookTitle);
HTTPErrorHtmlResponse::HTTPErrorHtmlResponse(const InternalServer& server,
const RequestContext& request,
int httpStatusCode,
const std::string& pageTitleMsg,
const std::string& headingMsg,
const std::string& cssUrl)
: ContentResponseBlueprint(&server,
&request,
httpStatusCode,
"text/html; charset=utf-8",
RESOURCE::templates::error_html)
{
kainjow::mustache::list emptyList;
this->m_data = kainjow::mustache::object{
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) },
{"PAGE_TITLE", pageTitleMsg},
{"PAGE_HEADING", headingMsg},
{"details", emptyList}
};
}
return std::move(response);
HTTP404HtmlResponse::HTTP404HtmlResponse(const InternalServer& server,
const RequestContext& request)
: HTTPErrorHtmlResponse(server,
request,
MHD_HTTP_NOT_FOUND,
"Content not found",
"Not Found")
{
}
HTTPErrorHtmlResponse& HTTP404HtmlResponse::operator+(UrlNotFoundMsg /*unused*/)
{
const std::string requestUrl = m_request.get_full_url();
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{url}}" was not found on this server.)");
return *this + msgTmpl.render({"url", requestUrl});
}
HTTPErrorHtmlResponse& HTTPErrorHtmlResponse::operator+(const std::string& msg)
{
m_data["details"].push_back({"p", msg});
return *this;
}
HTTP400HtmlResponse::HTTP400HtmlResponse(const InternalServer& server,
const RequestContext& request)
: HTTPErrorHtmlResponse(server,
request,
MHD_HTTP_BAD_REQUEST,
"Invalid request",
"Invalid request")
{
}
HTTPErrorHtmlResponse& HTTP400HtmlResponse::operator+(InvalidUrlMsg /*unused*/)
{
std::string requestUrl = m_request.get_full_url();
const auto query = m_request.get_query();
if (!query.empty()) {
requestUrl += "?" + encodeDiples(query);
}
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{{url}}}" is not a valid request.)");
return *this + msgTmpl.render({"url", requestUrl});
}
HTTP500HtmlResponse::HTTP500HtmlResponse(const InternalServer& server,
const RequestContext& request)
: HTTPErrorHtmlResponse(server,
request,
MHD_HTTP_INTERNAL_SERVER_ERROR,
"Internal Server Error",
"Internal Server Error")
{
// operator+() is a state-modifying operator (akin to operator+=)
*this + "An internal server error occured. We are sorry about that :/";
}
std::unique_ptr<ContentResponse> HTTP500HtmlResponse::generateResponseObject() const
{
// We want a 500 response to be a minimalistic one (so that the server doesn't
// have to provide additional resources required for its proper rendering)
// ";raw=true" in the MIME-type below disables response decoration
// (see ContentResponse::contentDecorationAllowed())
const std::string mimeType = "text/html;charset=utf-8;raw=true";
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
r->set_code(m_httpStatusCode);
return r;
}
ContentResponseBlueprint& ContentResponseBlueprint::operator+(const TaskbarInfo& taskbarInfo)
{
this->m_taskbarInfo.reset(new TaskbarInfo(taskbarInfo));
return *this;
}
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
@@ -111,26 +205,6 @@ std::unique_ptr<Response> Response::build_416(const InternalServer& server, size
return response;
}
std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg)
{
MustacheData data;
data.set("error", msg);
auto content = render_template(RESOURCE::templates::_500_html, data);
std::unique_ptr<Response> response (
new ContentResponse(
server.m_root, //root
true, //verbose
true, //raw
false, //withTaskbar
false, //withLibraryButton
false, //blockExternalLinks
content, //content
"text/html" //mimetype
));
response->set_code(MHD_HTTP_INTERNAL_SERVER_ERROR);
return response;
}
std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
{
@@ -332,10 +406,10 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
return ret;
}
void ContentResponse::set_taskbar(const std::string& bookName, const std::string& bookTitle)
void ContentResponse::set_taskbar(const std::string& bookName, const zim::Archive* archive)
{
m_bookName = bookName;
m_bookTitle = bookTitle;
m_bookTitle = archive ? getArchiveTitle(*archive) : "";
}

View File

@@ -33,13 +33,15 @@ extern "C" {
#include "microhttpd_wrapper.h"
}
namespace zim {
class Archive;
} // namespace zim
namespace kiwix {
class InternalServer;
class RequestContext;
class EntryResponse;
class Response {
public:
Response(bool verbose);
@@ -47,9 +49,7 @@ class Response {
static std::unique_ptr<Response> build(const InternalServer& server);
static std::unique_ptr<Response> build_304(const InternalServer& server, const ETag& etag);
static std::unique_ptr<Response> build_404(const InternalServer& server, const std::string& url, const std::string& bookName, const std::string& bookTitle, const std::string& details="");
static std::unique_ptr<Response> build_416(const InternalServer& server, size_t resourceLength);
static std::unique_ptr<Response> build_500(const InternalServer& server, const std::string& msg);
static std::unique_ptr<Response> build_redirect(const InternalServer& server, const std::string& redirectUrl);
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
@@ -100,7 +100,7 @@ class ContentResponse : public Response {
const std::string& mimetype,
bool isHomePage = false);
void set_taskbar(const std::string& bookName, const std::string& bookTitle);
void set_taskbar(const std::string& bookName, const zim::Archive* archive);
private:
MHD_Response* create_mhd_response(const RequestContext& request);
@@ -124,6 +124,110 @@ class ContentResponse : public Response {
std::string m_bookTitle;
};
struct TaskbarInfo
{
const std::string bookName;
const zim::Archive* const archive;
TaskbarInfo(const std::string& bookName, const zim::Archive* a = nullptr)
: bookName(bookName)
, archive(a)
{}
};
class ContentResponseBlueprint
{
public: // functions
ContentResponseBlueprint(const InternalServer* server,
const RequestContext* request,
int httpStatusCode,
const std::string& mimeType,
const std::string& templateStr)
: m_server(*server)
, m_request(*request)
, m_httpStatusCode(httpStatusCode)
, m_mimeType(mimeType)
, m_template(templateStr)
{}
virtual ~ContentResponseBlueprint() = default;
operator std::unique_ptr<ContentResponse>() const
{
return generateResponseObject();
}
operator std::unique_ptr<Response>() const
{
return operator std::unique_ptr<ContentResponse>();
}
ContentResponseBlueprint& operator+(const TaskbarInfo& taskbarInfo);
protected: // functions
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
public: //data
const InternalServer& m_server;
const RequestContext& m_request;
const int m_httpStatusCode;
const std::string m_mimeType;
const std::string m_template;
kainjow::mustache::data m_data;
std::unique_ptr<TaskbarInfo> m_taskbarInfo;
};
struct HTTPErrorHtmlResponse : ContentResponseBlueprint
{
HTTPErrorHtmlResponse(const InternalServer& server,
const RequestContext& request,
int httpStatusCode,
const std::string& pageTitleMsg,
const std::string& headingMsg,
const std::string& cssUrl = "");
using ContentResponseBlueprint::operator+;
HTTPErrorHtmlResponse& operator+(const std::string& msg);
};
class UrlNotFoundMsg {};
extern const UrlNotFoundMsg urlNotFoundMsg;
struct HTTP404HtmlResponse : HTTPErrorHtmlResponse
{
HTTP404HtmlResponse(const InternalServer& server,
const RequestContext& request);
using HTTPErrorHtmlResponse::operator+;
HTTPErrorHtmlResponse& operator+(UrlNotFoundMsg /*unused*/);
};
class InvalidUrlMsg {};
extern const InvalidUrlMsg invalidUrlMsg;
struct HTTP400HtmlResponse : HTTPErrorHtmlResponse
{
HTTP400HtmlResponse(const InternalServer& server,
const RequestContext& request);
using HTTPErrorHtmlResponse::operator+;
HTTPErrorHtmlResponse& operator+(InvalidUrlMsg /*unused*/);
};
struct HTTP500HtmlResponse : HTTPErrorHtmlResponse
{
HTTP500HtmlResponse(const InternalServer& server,
const RequestContext& request);
private: // overrides
// generateResponseObject() is overriden in order to produce a minimal
// response without any need for additional resources from the server
std::unique_ptr<ContentResponse> generateResponseObject() const override;
};
class ItemResponse : public Response {
public:
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);

View File

@@ -125,6 +125,30 @@ unsigned int getArchiveMediaCount(const zim::Archive& archive) {
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;
}

View File

@@ -46,6 +46,7 @@ namespace kiwix
std::string& content, std::string& mimeType);
unsigned int getArchiveMediaCount(const zim::Archive& archive);
unsigned int getArchiveArticleCount(const zim::Archive& archive);
unsigned int getArchiveFileSize(const zim::Archive& archive);
zim::Item getFinalItem(const zim::Archive& archive, const zim::Entry& entry);

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2021 Matthieu Gautier <mgautier@kymeria.fr>
* Copyright (C) 2020 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
*
*/
#ifndef ZIM_CONCURRENT_CACHE_H
#define ZIM_CONCURRENT_CACHE_H
#include "lrucache.h"
#include <future>
#include <mutex>
namespace kiwix
{
/**
ConcurrentCache implements a concurrent thread-safe cache
Compared to kiwix::lru_cache, each access operation is slightly more expensive.
However, different slots of the cache can be safely accessed concurrently
with minimal blocking. Concurrent access to the same element is also
safe, and, in case of a cache miss, will block until that element becomes
available.
*/
template <typename Key, typename Value>
class ConcurrentCache
{
private: // types
typedef std::shared_future<Value> ValuePlaceholder;
typedef lru_cache<Key, ValuePlaceholder> Impl;
public: // types
explicit ConcurrentCache(size_t maxEntries)
: impl_(maxEntries)
{}
// Gets the entry corresponding to the given key. If the entry is not in the
// cache, it is obtained by calling f() (without any arguments) and the
// result is put into the cache.
//
// The cache as a whole is locked only for the duration of accessing
// the respective slot. If, in the case of the a cache miss, the generation
// of the missing element takes a long time, only attempts to access that
// element will block - the rest of the cache remains open to concurrent
// access.
template<class F>
Value getOrPut(const Key& key, F f)
{
std::promise<Value> valuePromise;
std::unique_lock<std::mutex> l(lock_);
const auto x = impl_.getOrPut(key, valuePromise.get_future().share());
l.unlock();
if ( x.miss() ) {
try {
valuePromise.set_value(f());
} catch (std::exception& e) {
drop(key);
throw;
}
}
return x.value().get();
}
bool drop(const Key& key)
{
std::unique_lock<std::mutex> l(lock_);
return impl_.drop(key);
}
private: // data
Impl impl_;
std::mutex lock_;
};
} // namespace kiwix
#endif // ZIM_CONCURRENT_CACHE_H

160
src/tools/lrucache.h Normal file
View File

@@ -0,0 +1,160 @@
/*
* Copyrigth (c) 2021, Matthieu Gautier <mgautier@kymeria.fr>
* Copyright (c) 2020, Veloman Yunkan
* Copyright (c) 2014, lamerman
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of lamerman nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* File: lrucache.hpp
* Author: Alexander Ponomarev
*
* Created on June 20, 2013, 5:09 PM
*/
#ifndef _LRUCACHE_HPP_INCLUDED_
#define _LRUCACHE_HPP_INCLUDED_
#include <map>
#include <list>
#include <cstddef>
#include <stdexcept>
#include <cassert>
namespace kiwix {
template<typename key_t, typename value_t>
class lru_cache {
public: // types
typedef typename std::pair<key_t, value_t> key_value_pair_t;
typedef typename std::list<key_value_pair_t>::iterator list_iterator_t;
enum AccessStatus {
HIT, // key was found in the cache
PUT, // key was not in the cache but was created by the getOrPut() access
MISS // key was not in the cache; get() access failed
};
class AccessResult
{
const AccessStatus status_;
const value_t val_;
public:
AccessResult(const value_t& val, AccessStatus status)
: status_(status), val_(val)
{}
AccessResult() : status_(MISS), val_() {}
bool hit() const { return status_ == HIT; }
bool miss() const { return !hit(); }
const value_t& value() const
{
if ( status_ == MISS )
throw std::range_error("There is no such key in cache");
return val_;
}
operator const value_t& () const { return value(); }
};
public: // functions
explicit lru_cache(size_t max_size) :
_max_size(max_size) {
}
// If 'key' is present in the cache, returns the associated value,
// otherwise puts the given value into the cache (and returns it with
// a status of a cache miss).
AccessResult getOrPut(const key_t& key, const value_t& value) {
auto it = _cache_items_map.find(key);
if (it != _cache_items_map.end()) {
_cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second);
return AccessResult(it->second->second, HIT);
} else {
putMissing(key, value);
return AccessResult(value, PUT);
}
}
void put(const key_t& key, const value_t& value) {
auto it = _cache_items_map.find(key);
if (it != _cache_items_map.end()) {
_cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second);
it->second->second = value;
} else {
putMissing(key, value);
}
}
AccessResult get(const key_t& key) {
auto it = _cache_items_map.find(key);
if (it == _cache_items_map.end()) {
return AccessResult();
} else {
_cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second);
return AccessResult(it->second->second, HIT);
}
}
bool drop(const key_t& key) {
try {
auto list_it = _cache_items_map.at(key);
_cache_items_list.erase(list_it);
_cache_items_map.erase(key);
return true;
} catch (std::out_of_range& e) {
return false;
}
}
bool exists(const key_t& key) const {
return _cache_items_map.find(key) != _cache_items_map.end();
}
size_t size() const {
return _cache_items_map.size();
}
private: // functions
void putMissing(const key_t& key, const value_t& value) {
assert(_cache_items_map.find(key) == _cache_items_map.end());
_cache_items_list.push_front(key_value_pair_t(key, value));
_cache_items_map[key] = _cache_items_list.begin();
if (_cache_items_map.size() > _max_size) {
_cache_items_map.erase(_cache_items_list.back().first);
_cache_items_list.pop_back();
}
}
private: // data
std::list<key_value_pair_t> _cache_items_list;
std::map<key_t, list_iterator_t> _cache_items_map;
size_t _max_size;
};
} // namespace kiwix
#endif /* _LRUCACHE_HPP_INCLUDED_ */

View File

@@ -370,6 +370,13 @@ std::string kiwix::gen_uuid(const std::string& s)
return kiwix::to_string(zim::Uuid::generate(s));
}
kainjow::mustache::data kiwix::onlyAsNonEmptyMustacheValue(const std::string& s)
{
return s.empty()
? kainjow::mustache::data(false)
: kainjow::mustache::data(s);
}
std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data)
{
kainjow::mustache::mustache tmpl(template_str);

View File

@@ -48,6 +48,10 @@ namespace kiwix
std::string gen_date_str();
std::string gen_uuid(const std::string& s);
// if s is empty then returns kainjow::mustache::data(false)
// otherwise kainjow::mustache::data(value)
kainjow::mustache::data onlyAsNonEmptyMustacheValue(const std::string& s);
std::string render_template(const std::string& template_str, kainjow::mustache::data data);
}

View File

@@ -32,10 +32,9 @@ skin/index.css
skin/fonts/Poppins.ttf
skin/fonts/Roboto.ttf
skin/block_external.js
skin/search_results.css
templates/search_result.html
templates/no_search_result.html
templates/404.html
templates/500.html
templates/error.html
templates/index.html
templates/suggestion.json
templates/head_taskbar.html

View File

@@ -16,6 +16,7 @@
let filters = getCookie(filterCookieName);
let params = new URLSearchParams(window.location.search || filters || '');
let timer;
let languages = {};
function queryUrlBuilder() {
let url = `${root}/catalog/search?`;
@@ -53,8 +54,13 @@
};
const humanFriendlyTitle = (title) => {
title = title.replace(/_/g, ' ');
return htmlEncode(title[0].toUpperCase() + title.slice(1));
if (typeof title === 'string' && title.length > 0) {
title = title.replace(/_/g, ' ');
if (title.length > 0) {
return htmlEncode(title[0].toUpperCase() + title.slice(1));
}
}
return '';
}
function htmlEncode(str) {
@@ -82,7 +88,8 @@
const title = getInnerHtml(book, 'title');
const description = getInnerHtml(book, 'summary');
const id = getInnerHtml(book, 'id');
const language = getInnerHtml(book, 'language');
const langCode = getInnerHtml(book, 'language');
const language = languages[langCode];
const tags = getInnerHtml(book, 'tags');
let tagHtml = tags.split(';').filter(tag => {return !(tag.split(':')[0].startsWith('_'))})
.map((tag) => {return tag.charAt(0).toUpperCase() + tag.slice(1)})
@@ -105,7 +112,7 @@
divTag.setAttribute('data-idx', bookOrderMap.get(id));
}
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
const languageAttr = language != '' ? '' : 'style="background-color: transparent"';
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
divTag.innerHTML = `<a class="book__link" href="${link}" data-hover="Preview">
<div class="book__wrapper">
<div class="book__icon" ${faviconAttr}></div>
@@ -114,7 +121,7 @@
${downloadLink ? `<div class="book__download"><span data-link="${downloadLink}">Download ${humanFriendlyZimSize ? ` - ${humanFriendlyZimSize}</span></div>`: ''}` : ''}
</div>
<div class="book__description" title="${description}">${description}</div>
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(language)}</div>
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(langCode)}</div>
<div class="book__tags"><div class="book__tags--wrapper">${tagHtml}</div></div>
</div></div></a>`;
return divTag;
@@ -238,7 +245,11 @@
data.querySelectorAll('entry').forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
optionStr += `<option value="${value}">${humanFriendlyTitle(title)}</option>`;
const hfTitle = humanFriendlyTitle(title);
if (valueEntryNode == 'language') {
languages[value] = hfTitle;
}
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
});
document.querySelector(nodeQuery).innerHTML += optionStr;
});
@@ -316,7 +327,7 @@
booksToDelete.forEach(book => {iso.remove(book);});
books.forEach((book) => {
iso.insert(generateBookHtml(book, sort))
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .book__download`);
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .book__download span`);
if (downloadButton) {
insertModal(downloadButton);
}
@@ -390,9 +401,9 @@
footer = document.getElementById('kiwixfooter');
fadeOutDiv = document.getElementById('fadeOut');
loader = document.querySelector('.loader');
await loadAndDisplayBooks();
await loadAndDisplayOptions('#languageFilter', `${root}/catalog/v2/languages`, 'language');
await loadAndDisplayOptions('#categoryFilter', `${root}/catalog/v2/categories`, 'title');
await loadAndDisplayBooks();
document.querySelectorAll('.filter').forEach(filter => {
filter.addEventListener('change', () => {resetAndFilter(filter.name, filter.value)});
});

3563
static/skin/isotope.pkgd.js Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
body{
background-color: white;
color: #000000;
font: small/normal Arial,Helvetica,Sans-Serif;
margin-top: 0.5em;
font-size: 90%;
}
a{
color: #04c;
}
a:visited {
color: #639
}
a:hover {
text-decoration: underline
}
h1 {
font-size: 120%;
}
ul {
margin:0;
padding:0
}
.results {
font-size: 110%;
}
.results li {
list-style-type:none;
margin-top: 0.5em;
}
.results a {
font-size: 110%;
text-decoration: underline
}
cite {
font-style:normal;
word-wrap:break-word;
display: block;
font-size: 100%;
}
.informations {
color: #388222;
font-size: 100%;
}
.footer {
padding: 0;
margin-top: 1em;
width: 100%;
float: left
}
.footer a, .footer span {
display: block;
padding: .3em .7em;
margin: 0 .38em 0 0;
text-align:center;
text-decoration: none;
}
.footer a:hover {
background: #ededed;
}
.footer ul, .footer li {
list-style:none;
margin: 0;
padding: 0;
}
.footer li {
float: left;
}
.selected {
background: #ededed;
}

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Content not found</title>
</head>
<body>
<h1>Not Found</h1>
{{#url}}
<p>
The requested URL "{{url}}" was not found on this server.
</p>
{{/url}}
{{#details}}
<p>
{{{details}}}
</p>
{{/details}}
</body>
</html>

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Internal Server Error</title>
</head>
<body>
<h1>Internal Server Error</h1>
<p>
An internal server error occured. We are sorry about that :/
</p>
<p>
{{ error }}
</p>
</body>
</html>

View File

@@ -1,4 +1,6 @@
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
xmlns:opds="http://opds-spec.org/2010/catalog">
<id>{{feed_id}}</id>
<title>{{^filter}}All zims{{/filter}}{{#filter}}Filtered zims ({{filter}}){{/filter}}</title>
<updated>{{date}}</updated>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
xmlns:opds="https://specs.opds.io/opds-1.2"
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
<id>{{feed_id}}</id>

View File

@@ -25,6 +25,7 @@
<publisher>
<name>{{publisher_name}}</name>
</publisher>
<dc:issued>{{book_date}}</dc:issued>
{{#url}}
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
{{/url}}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>{{PAGE_TITLE}}</title>
{{#CSS_URL}}
<link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />
{{/CSS_URL}}
</head>
<body>
<h1>{{PAGE_HEADING}}</h1>
{{#details}}
<p>
{{{p}}}
</p>
{{/details}}
</body>
</html>

View File

@@ -1,103 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type" />
<style type="text/css">
body{
color: #000000;
font: small/normal Arial,Helvetica,Sans-Serif;
margin-top: 0.5em;
font-size: 90%;
}
a{
color: #04c;
}
a:visited {
color: #639
}
a:hover {
text-decoration: underline
}
.header {
font-size: 120%;
}
ul {
margin:0;
padding:0
}
.results {
font-size: 110%;
}
.results li {
list-style-type:none;
margin-top: 0.5em;
}
.results a {
font-size: 110%;
text-decoration: underline
}
cite {
font-style:normal;
word-wrap:break-word;
display: block;
font-size: 100%;
}
.informations {
color: #388222;
font-size: 100%;
}
.footer {
padding: 0;
margin-top: 1em;
width: 100%;
float: left
}
.footer a, .footer span {
display: block;
padding: .3em .7em;
margin: 0 .38em 0 0;
text-align:center;
text-decoration: none;
}
.footer a:hover {
background: #ededed;
}
.footer ul, .footer li {
list-style:none;
margin: 0;
padding: 0;
}
.footer li {
float: left;
}
.selected {
background: #ededed;
}
</style>
<title>Fulltext search unavailable</title>
</head>
<body bgcolor="white">
<div class="header">Not found</div>
<p>
There is no article with the title <b> "{{pattern}}"</b>
and the fulltext search engine is not available for this content.
</p>
</body>
</html>

View File

@@ -57,6 +57,11 @@
font-size: 100%;
}
.book-title {
color: #662200;
font-size: 100%;
}
.footer {
padding: 0;
margin-top: 1em;
@@ -102,11 +107,11 @@
</b> of <b>
{{count}}
</b> for <b>
{{searchPattern}}
"{{{searchPattern}}}"
</b>
{{/hasResults}}
{{^hasResults}}
No results were found for <b>{{searchPattern}}</b>
No results were found for <b>"{{{searchPattern}}}"</b>
{{/hasResults}}
</div>
@@ -120,6 +125,9 @@
{{#snippet}}
<cite>{{>snippet}}...</cite>
{{/snippet}}
{{#bookTitle}}
<div class="book-title">from {{bookTitle}}</div>
{{/bookTitle}}
{{#wordCount}}
<div class="informations">{{wordCount}} words</div>
{{/wordCount}}

BIN
test/data/poor.zim Normal file
View File

Binary file not shown.

View File

@@ -22,13 +22,18 @@
const char * sampleOpdsStream = R"(
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
xmlns:opds="http://opds-spec.org/2010/catalog">
<id>00000000-0000-0000-0000-000000000000</id>
<entry>
<title>Encyclopédie de la Tunisie</title>
<name>wikipedia_fr_tunisie_novid_2018-10</name>
<flavour>unforgettable</flavour>
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd</id>
<icon>/meta?name=favicon&amp;content=wikipedia_fr_tunisie_novid_2018-10</icon>
<updated>2018-10-08T00:00::00:Z</updated>
<dc:issued>8 Oct 2018</dc:issued>
<language>fra</language>
<summary>Le meilleur de Wikipédia sur la Tunisie</summary>
<tags>wikipedia;novid;_ftindex</tags>
@@ -36,8 +41,13 @@ const char * sampleOpdsStream = R"(
<author>
<name>Wikipedia</name>
</author>
<publisher>
<name>Wikipedia Publishing House</name>
</publisher>
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4" length="90030080" />
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&amp;content=wikipedia_fr_tunisie_novid_2018-10" />
<mediaCount>1100</mediaCount>
<articleCount>172</articleCount>
</entry>
<entry>
<title>Tania Louis</title>
@@ -224,6 +234,64 @@ const char sampleLibraryXML[] = R"(
namespace
{
TEST(LibraryOpdsImportTest, allInOne)
{
kiwix::Library lib;
kiwix::Manager manager(&lib);
manager.readOpds(sampleOpdsStream, "library-opds-import.unittests.dev");
EXPECT_EQ(10U, lib.getBookCount(true, true));
{
const kiwix::Book& book1 = lib.getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd");
EXPECT_EQ(book1.getTitle(), "Encyclopédie de la Tunisie");
EXPECT_EQ(book1.getName(), "wikipedia_fr_tunisie_novid_2018-10");
EXPECT_EQ(book1.getFlavour(), "unforgettable");
EXPECT_EQ(book1.getLanguage(), "fra");
EXPECT_EQ(book1.getDate(), "8 Oct 2018");
EXPECT_EQ(book1.getDescription(), "Le meilleur de Wikipédia sur la Tunisie");
EXPECT_EQ(book1.getCreator(), "Wikipedia");
EXPECT_EQ(book1.getPublisher(), "Wikipedia Publishing House");
EXPECT_EQ(book1.getTags(), "wikipedia;novid;_ftindex");
EXPECT_EQ(book1.getCategory(), "");
EXPECT_EQ(book1.getUrl(), "http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4");
EXPECT_EQ(book1.getSize(), 90030080UL);
EXPECT_EQ(book1.getMediaCount(), 1100U); // Roman MC (MediaCount) is 1100
EXPECT_EQ(book1.getArticleCount(), 172U); // Hex AC (ArticleCount) is 172
const auto illustration = book1.getIllustration(48);
EXPECT_EQ(illustration->width, 48U);
EXPECT_EQ(illustration->height, 48U);
EXPECT_EQ(illustration->mimeType, "image/png");
EXPECT_EQ(illustration->url, "library-opds-import.unittests.dev/meta?name=favicon&content=wikipedia_fr_tunisie_novid_2018-10");
}
{
const kiwix::Book& book2 = lib.getBookById("0189d9be-2fd0-b4b6-7300-20fab0b5cdc8");
EXPECT_EQ(book2.getTitle(), "TED talks - Business");
EXPECT_EQ(book2.getName(), "");
EXPECT_EQ(book2.getFlavour(), "");
EXPECT_EQ(book2.getLanguage(), "eng");
EXPECT_EQ(book2.getDate(), "2018-07-23");
EXPECT_EQ(book2.getDescription(), "Ideas worth spreading");
EXPECT_EQ(book2.getCreator(), "TED");
EXPECT_EQ(book2.getPublisher(), "");
EXPECT_EQ(book2.getTags(), "");
EXPECT_EQ(book2.getCategory(), "");
EXPECT_EQ(book2.getUrl(), "http://download.kiwix.org/zim/ted/ted_en_business_2018-07.zim.meta4");
EXPECT_EQ(book2.getSize(), 8855827456UL);
EXPECT_EQ(book2.getMediaCount(), 0U);
EXPECT_EQ(book2.getArticleCount(), 0U);
const auto illustration = book2.getIllustration(48);
EXPECT_EQ(illustration->width, 48U);
EXPECT_EQ(illustration->height, 48U);
EXPECT_EQ(illustration->mimeType, "image/png");
EXPECT_EQ(illustration->url, "library-opds-import.unittests.dev/meta?name=favicon&content=ted_en_business_2018-07");
}
}
class LibraryTest : public ::testing::Test {
protected:
typedef kiwix::Library::BookIdCollection BookIdCollection;
@@ -292,7 +360,8 @@ TEST_F(LibraryTest, sanityCheck)
EXPECT_EQ(lib.getBooksPublishers(), std::vector<std::string>({
"",
"Kiwix",
"Kiwix & Some Enthusiasts"
"Kiwix & Some Enthusiasts",
"Wikipedia Publishing House"
}));
}

View File

@@ -29,6 +29,7 @@ if gtest_dep.found() and not meson.is_cross_build()
'zimfile.zim',
'zimfile&other.zim',
'corner_cases.zim',
'poor.zim',
'library.xml'
]
foreach file : data_files

View File

@@ -45,6 +45,18 @@ Headers invariantHeaders(Headers headers)
return headers;
}
// 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, "");
}
class ZimFileServer
{
@@ -130,6 +142,7 @@ protected:
const int PORT = 8001;
const ZimFileServer::FilePathCollection ZIMFILES {
"./test/zimfile.zim",
"./test/poor.zim",
"./test/corner_cases.zim"
};
@@ -275,6 +288,22 @@ TEST_F(ServerTest, UncompressibleContentIsNotCompressed)
}
}
const char* urls400[] = {
"/ROOT/search",
"/ROOT/search?content=zimfile",
"/ROOT/search?content=non-existing-book&pattern=asdfqwerty",
"/ROOT/search?content=non-existing-book&pattern=asd<qwerty",
"/ROOT/search?pattern"
};
TEST_F(ServerTest, 400)
{
for (const char* url: urls400 ) {
EXPECT_EQ(400, zfs1_->GET(url)->status) << "url: " << url;
}
}
const char* urls404[] = {
"/",
"/zimfile",
@@ -290,7 +319,6 @@ const char* urls404[] = {
"/ROOT/meta?content=non-existent-book&name=title",
"/ROOT/random",
"/ROOT/random?content=non-existent-book",
"/ROOT/search",
"/ROOT/suggest",
"/ROOT/suggest?content=non-existent-book&term=abcd",
"/ROOT/catch/external",
@@ -306,8 +334,470 @@ const char* urls404[] = {
TEST_F(ServerTest, 404)
{
for ( const char* url : urls404 )
for ( const char* url : urls404 ) {
EXPECT_EQ(404, zfs1_->GET(url)->status) << "url: " << url;
}
}
namespace TestingOfHtmlResponses
{
struct ExpectedResponseData
{
const std::string expectedPageTitle;
const std::string expectedCssUrl;
const std::string bookName;
const std::string bookTitle;
const std::string expectedBody;
};
enum ExpectedResponseDataType
{
expected_page_title,
expected_css_url,
book_name,
book_title,
expected_body
};
// Operator overloading is used as a means of defining a mini-DSL for
// defining test data in a concise way (see usage in
// TEST_F(ServerTest, 404WithBodyTesting))
ExpectedResponseData operator==(ExpectedResponseDataType t, std::string s)
{
switch (t)
{
case expected_page_title: return ExpectedResponseData{s, "", "", "", ""};
case expected_css_url: return ExpectedResponseData{"", s, "", "", ""};
case book_name: return ExpectedResponseData{"", "", s, "", ""};
case book_title: return ExpectedResponseData{"", "", "", s, ""};
case expected_body: return ExpectedResponseData{"", "", "", "", s};
default: assert(false); return ExpectedResponseData{};
}
}
std::string selectNonEmpty(const std::string& a, const std::string& b)
{
if ( a.empty() ) return b;
assert(b.empty());
return a;
}
ExpectedResponseData operator&&(const ExpectedResponseData& a,
const ExpectedResponseData& b)
{
return ExpectedResponseData{
selectNonEmpty(a.expectedPageTitle, b.expectedPageTitle),
selectNonEmpty(a.expectedCssUrl, b.expectedCssUrl),
selectNonEmpty(a.bookName, b.bookName),
selectNonEmpty(a.bookTitle, b.bookTitle),
selectNonEmpty(a.expectedBody, b.expectedBody)
};
}
class TestContentIn404HtmlResponse : public ExpectedResponseData
{
public:
TestContentIn404HtmlResponse(const std::string& url,
const ExpectedResponseData& erd)
: ExpectedResponseData(erd)
, url(url)
{}
virtual ~TestContentIn404HtmlResponse() = default;
const std::string url;
std::string expectedResponse() const;
private:
virtual std::string pageTitle() const;
std::string pageCssLink() const;
std::string hiddenBookNameInput() const;
std::string searchPatternInput() const;
std::string taskbarLinks() const;
};
std::string TestContentIn404HtmlResponse::expectedResponse() const
{
const std::string frag[] = {
R"FRAG(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>)FRAG",
R"FRAG(</title>
)FRAG",
R"FRAG(
<link type="root" href="/ROOT"><link type="text/css" href="/ROOT/skin/jquery-ui/jquery-ui.min.css" rel="Stylesheet" />
<link type="text/css" href="/ROOT/skin/jquery-ui/jquery-ui.theme.min.css" rel="Stylesheet" />
<link type="text/css" href="/ROOT/skin/taskbar.css" rel="Stylesheet" />
<script type="text/javascript" src="/ROOT/skin/jquery-ui/external/jquery/jquery.js" defer></script>
<script type="text/javascript" src="/ROOT/skin/jquery-ui/jquery-ui.min.js" defer></script>
<script type="text/javascript" src="/ROOT/skin/taskbar.js" defer></script>
</head>
<body><span class="kiwix">
<span id="kiwixtoolbar" class="ui-widget-header">
<div class="kiwix_centered">
<div class="kiwix_searchform">
<form class="kiwixsearch" method="GET" action="/ROOT/search" id="kiwixsearchform">
)FRAG",
R"FRAG(
<label for="kiwixsearchbox">&#x1f50d;</label>
)FRAG",
R"FRAG( </form>
</div>
<input type="checkbox" id="kiwix_button_show_toggle">
<label for="kiwix_button_show_toggle"><img src="/ROOT/skin/caret.png" alt=""></label>
<div class="kiwix_button_cont">
<a id="kiwix_serve_taskbar_library_button" title="Go to welcome page" aria-label="Go to welcome page" href="/ROOT/"><button>&#x1f3e0;</button></a>
)FRAG",
R"FRAG(
</div>
</div>
</span>
</span>
)FRAG",
R"FRAG( </body>
</html>
)FRAG"
};
return frag[0]
+ pageTitle()
+ frag[1]
+ pageCssLink()
+ frag[2]
+ hiddenBookNameInput()
+ frag[3]
+ searchPatternInput()
+ frag[4]
+ taskbarLinks()
+ frag[5]
+ removeEOLWhitespaceMarkers(expectedBody)
+ frag[6];
}
std::string TestContentIn404HtmlResponse::pageTitle() const
{
return expectedPageTitle.empty()
? "Content not found"
: expectedPageTitle;
}
std::string TestContentIn404HtmlResponse::pageCssLink() const
{
if ( expectedCssUrl.empty() )
return "";
return R"( <link type="text/css" href=")"
+ expectedCssUrl
+ R"(" rel="Stylesheet" />)";
}
std::string TestContentIn404HtmlResponse::hiddenBookNameInput() const
{
return bookName.empty()
? ""
: R"(<input type="hidden" name="content" value=")" + bookName + R"(" />)";
}
std::string TestContentIn404HtmlResponse::searchPatternInput() const
{
return R"( <input autocomplete="off" class="ui-autocomplete-input" id="kiwixsearchbox" name="pattern" type="text" title="Search ')"
+ bookTitle
+ R"('" aria-label="Search ')"
+ bookTitle
+ R"('">
)";
}
std::string TestContentIn404HtmlResponse::taskbarLinks() const
{
if ( bookName.empty() )
return "";
return R"(<a id="kiwix_serve_taskbar_home_button" title="Go to the main page of ')"
+ bookTitle
+ R"('" aria-label="Go to the main page of ')"
+ bookTitle
+ R"('" href="/ROOT/)"
+ bookName
+ R"(/"><button>)"
+ bookTitle
+ R"(</button></a>
<a id="kiwix_serve_taskbar_random_button" title="Go to a randomly selected page" aria-label="Go to a randomly selected page"
href="/ROOT/random?content=)"
+ bookName
+ R"("><button>&#x1F3B2;</button></a>)";
}
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
{
public:
TestContentIn400HtmlResponse(const std::string& url,
const ExpectedResponseData& erd)
: TestContentIn404HtmlResponse(url, erd)
{}
private:
std::string pageTitle() const;
};
std::string TestContentIn400HtmlResponse::pageTitle() const {
return expectedPageTitle.empty()
? "Invalid request"
: expectedPageTitle;
}
} // namespace TestingOfHtmlResponses
TEST_F(ServerTest, 404WithBodyTesting)
{
using namespace TestingOfHtmlResponses;
const std::vector<TestContentIn404HtmlResponse> testData{
{ /* url */ "/ROOT/random?content=non-existent-book",
expected_body==R"(
<h1>Not Found</h1>
<p>
No such book: non-existent-book
</p>
)" },
{ /* url */ "/ROOT/suggest?content=no-such-book&term=whatever",
expected_body==R"(
<h1>Not Found</h1>
<p>
No such book: no-such-book
</p>
)" },
{ /* url */ "/ROOT/catalog/",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/catalog/" was not found on this server.
</p>
)" },
{ /* url */ "/ROOT/catalog/invalid_endpoint",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/catalog/invalid_endpoint" was not found on this server.
</p>
)" },
{ /* url */ "/ROOT/invalid-book/whatever",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/invalid-book/whatever" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT/search?pattern=whatever">whatever</a>
</p>
)" },
{ /* url */ "/ROOT/zimfile/invalid-article",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/zimfile/invalid-article" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT/search?content=zimfile&pattern=invalid-article">invalid-article</a>
</p>
)" },
{ /* url */ R"(/ROOT/"><svg onload=alert(1)>)",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/&quot;&gt;&lt;svg onload=alert(1)&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E">&quot;&gt;&lt;svg onload=alert(1)&gt;</a>
</p>
)" },
{ /* url */ R"(/ROOT/zimfile/"><svg onload=alert(1)>)",
book_name=="zimfile" &&
book_title=="Ray Charles" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/zimfile/&quot;&gt;&lt;svg onload=alert(1)&gt;" was not found on this server.
</p>
<p>
Make a full text search for <a href="/ROOT/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E">&quot;&gt;&lt;svg onload=alert(1)&gt;</a>
</p>
)" },
{ /* url */ "/ROOT/raw/no-such-book/meta/Title",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/raw/no-such-book/meta/Title" was not found on this server.
</p>
<p>
No such book: no-such-book
</p>
)" },
{ /* url */ "/ROOT/raw/zimfile/XYZ",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/raw/zimfile/XYZ" was not found on this server.
</p>
<p>
XYZ is not a valid request for raw content.
</p>
)" },
{ /* url */ "/ROOT/raw/zimfile/meta/invalid-metadata",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/raw/zimfile/meta/invalid-metadata" was not found on this server.
</p>
<p>
Cannot find meta entry invalid-metadata
</p>
)" },
{ /* url */ "/ROOT/raw/zimfile/content/invalid-article",
expected_body==R"(
<h1>Not Found</h1>
<p>
The requested URL "/ROOT/raw/zimfile/content/invalid-article" was not found on this server.
</p>
<p>
Cannot find content entry invalid-article
</p>
)" },
{ /* url */ "/ROOT/search?content=poor&pattern=whatever",
expected_page_title=="Fulltext search unavailable" &&
expected_css_url=="/ROOT/skin/search_results.css" &&
book_name=="poor" &&
book_title=="poor" &&
expected_body==R"(
<h1>Not Found</h1>
<p>
The fulltext search engine is not available for this content.
</p>
)" },
};
for ( const auto& t : testData ) {
const TestContext ctx{ {"url", t.url} };
const auto r = zfs1_->GET(t.url.c_str());
EXPECT_EQ(r->status, 404) << ctx;
EXPECT_EQ(r->body, t.expectedResponse()) << ctx;
}
}
TEST_F(ServerTest, 400WithBodyTesting)
{
using namespace TestingOfHtmlResponses;
const std::vector<TestContentIn400HtmlResponse> testData{
{ /* url */ "/ROOT/search",
expected_body== R"(
<h1>Invalid request</h1>
<p>
The requested URL "/ROOT/search" is not a valid request.
</p>
<p>
No query provided.
</p>
)" },
{ /* url */ "/ROOT/search?content=zimfile",
expected_body==R"(
<h1>Invalid request</h1>
<p>
The requested URL "/ROOT/search?content=zimfile" is not a valid request.
</p>
<p>
No query provided.
</p>
)" },
{ /* url */ "/ROOT/search?content=non-existing-book&pattern=asdfqwerty",
expected_body==R"(
<h1>Invalid request</h1>
<p>
The requested URL "/ROOT/search?content=non-existing-book&pattern=asdfqwerty" is not a valid request.
</p>
<p>
The requested book doesn't exist.
</p>
)" },
{ /* url */ "/ROOT/search?content=non-existing-book&pattern=a\"<script foo>",
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.
</p>
<p>
The requested book doesn't exist.
</p>
)" },
// There is a flaw in our way to handle query string, we cannot differenciate
// between `pattern` and `pattern=`
{ /* url */ "/ROOT/search?pattern",
expected_body==R"(
<h1>Invalid request</h1>
<p>
The requested URL "/ROOT/search?pattern=" is not a valid request.
</p>
<p>
No query provided.
</p>
)" },
};
for ( const auto& t : testData ) {
const TestContext ctx{ {"url", t.url} };
const auto r = zfs1_->GET(t.url.c_str());
EXPECT_EQ(r->status, 400) << ctx;
EXPECT_EQ(r->body, t.expectedResponse()) << ctx;
}
}
TEST_F(ServerTest, 500)
{
const std::string expectedBody = R"(<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
<title>Internal Server Error</title>
</head>
<body>
<h1>Internal Server Error</h1>
<p>
An internal server error occured. We are sorry about that :/
</p>
<p>
Entry redirect_loop.html is a redirect entry.
</p>
</body>
</html>
)";
const auto r = zfs1_->GET("/ROOT/poor/A/redirect_loop.html");
EXPECT_EQ(r->status, 500);
EXPECT_EQ(r->body, expectedBody);
}
TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle)
@@ -354,14 +844,16 @@ TEST_F(ServerTest, RawEntry)
TEST_F(ServerTest, HeadMethodIsSupported)
{
for ( const Resource& res : all200Resources() )
for ( const Resource& res : all200Resources() ) {
EXPECT_EQ(200, zfs1_->HEAD(res.url)->status) << res;
}
}
TEST_F(ServerTest, TheResponseToHeadRequestHasNoBody)
{
for ( const Resource& res : all200Resources() )
for ( const Resource& res : all200Resources() ) {
EXPECT_TRUE(zfs1_->HEAD(res.url)->body.empty()) << res;
}
}
TEST_F(ServerTest, HeadersAreTheSameInResponsesToHeadAndGetRequests)
@@ -444,7 +936,7 @@ TEST_F(ServerTest, ETagOfUncompressibleContentIsNotAffectedByAcceptEncoding)
// NOTE: The "Date" header (which should belong to that list as required
// NOTE: by RFC 7232) is not included (since the result of this function
// NOTE: will be used to check the equality of headers from the 200 and 304
// NOTe: responses).
// NOTE: responses).
Headers special304Headers(const httplib::Response& r)
{
Headers result;
@@ -625,6 +1117,149 @@ TEST_F(ServerTest, RangeHeaderIsCaseInsensitive)
}
}
TEST_F(ServerTest, suggestions)
{
typedef std::pair<std::string, std::string> UrlAndExpectedResponse;
const std::vector<UrlAndExpectedResponse> testData{
{ /* url: */ "/ROOT/suggest?content=zimfile&term=thing",
R"EXPECTEDRESPONSE([
{
"value" : "Doing His Thing",
"label" : "Doing His &lt;b&gt;Thing&lt;/b&gt;",
"kind" : "path"
, "path" : "A/Doing_His_Thing"
},
{
"value" : "We Didn&apos;t See a Thing",
"label" : "We Didn&apos;t See a &lt;b&gt;Thing&lt;/b&gt;",
"kind" : "path"
, "path" : "A/We_Didn&apos;t_See_a_Thing"
},
{
"value" : "thing ",
"label" : "containing &apos;thing&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}
]
)EXPECTEDRESPONSE"
},
{ /* url: */ "/ROOT/suggest?content=zimfile&term=old%20sun",
R"EXPECTEDRESPONSE([
{
"value" : "That Lucky Old Sun",
"label" : "That Lucky &lt;b&gt;Old&lt;/b&gt; &lt;b&gt;Sun&lt;/b&gt;",
"kind" : "path"
, "path" : "A/That_Lucky_Old_Sun"
},
{
"value" : "old sun ",
"label" : "containing &apos;old sun&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}
]
)EXPECTEDRESPONSE"
},
{ /* url: */ "/ROOT/suggest?content=zimfile&term=abracadabra",
R"EXPECTEDRESPONSE([
{
"value" : "abracadabra ",
"label" : "containing &apos;abracadabra&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}
]
)EXPECTEDRESPONSE"
},
{ // Test handling of & (%26 when url-encoded) in the search string
/* url: */ "/ROOT/suggest?content=zimfile&term=A%26B",
R"EXPECTEDRESPONSE([
{
"value" : "A&amp;B ",
"label" : "containing &apos;A&amp;B&apos;...",
"kind" : "pattern"
//EOLWHITESPACEMARKER
}
]
)EXPECTEDRESPONSE"
},
};
for ( const auto& urlAndExpectedResponse : testData ) {
const std::string url = urlAndExpectedResponse.first;
const std::string expectedResponse = urlAndExpectedResponse.second;
const TestContext ctx{ {"url", url} };
const auto r = zfs1_->GET(url.c_str());
EXPECT_EQ(r->status, 200) << ctx;
EXPECT_EQ(r->body, removeEOLWhitespaceMarkers(expectedResponse)) << ctx;
}
}
TEST_F(ServerTest, suggestions_in_range)
{
/**
* Attempt to get 50 suggestions in steps of 5
* The suggestions are returned in the json format
* [{sugg1}, {sugg2}, ... , {suggN}, {suggest ft search}]
* Assuming the number of suggestions = (occurance of "{" - 1)
*/
{
int suggCount = 0;
for (int i = 0; i < 10; i++) {
std::string url = "/ROOT/suggest?content=zimfile&term=ray&start=" + std::to_string(i*5) + "&count=5";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 5);
suggCount += currCount;
}
ASSERT_EQ(suggCount, 50);
}
// Attempt to get 10 suggestions in steps of 5 even though there are only 8
{
std::string url = "/ROOT/suggest?content=zimfile&term=song+for+you&start=0&count=5";
const auto r1 = zfs1_->GET(url.c_str());
std::string body = r1->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 5);
url = "/ROOT/suggest?content=zimfile&term=song+for+you&start=5&count=5";
const auto r2 = zfs1_->GET(url.c_str());
body = r2->body;
currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 3);
}
// Attempt to get 10 suggestions even though there is only 1
{
std::string url = "/ROOT/suggest?content=zimfile&term=strong&start=0&count=5";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 1);
}
// No Suggestion
{
std::string url = "/ROOT/suggest?content=zimfile&term=oops&start=0&count=5";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 0);
}
// Out of bound value
{
std::string url = "/ROOT/suggest?content=zimfile&term=ray&start=-2&count=-1";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 0);
}
}
////////////////////////////////////////////////////////////////////////////////
// Testing of the library-related functionality of the server
////////////////////////////////////////////////////////////////////////////////
@@ -679,8 +1314,9 @@ std::string maskVariableOPDSFeedData(std::string s)
}
#define OPDS_FEED_TAG \
"<feed xmlns=\"http://www.w3.org/2005/Atom\"" \
" xmlns:opds=\"http://opds-spec.org/2010/catalog\">\n"
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\n" \
" xmlns:dc=\"http://purl.org/dc/terms/\"\n" \
" xmlns:opds=\"http://opds-spec.org/2010/catalog\">\n"
#define CATALOG_LINK_TAGS \
" <link rel=\"self\" href=\"\" type=\"application/atom+xml\" />\n" \
@@ -708,6 +1344,7 @@ std::string maskVariableOPDSFeedData(std::string s)
" <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%26other.zim\" length=\"569344\" />\n" \
" </entry>\n"
@@ -734,6 +1371,7 @@ std::string maskVariableOPDSFeedData(std::string s)
" <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"
@@ -757,6 +1395,7 @@ std::string maskVariableOPDSFeedData(std::string s)
" <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"
@@ -929,6 +1568,24 @@ TEST_F(LibraryServerTest, catalog_search_by_category)
TEST_F(LibraryServerTest, catalog_search_results_pagination)
{
{
const auto r = zfs1_->GET("/ROOT/catalog/search?count=0");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Filtered zims (count=0)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>3</itemsPerPage>\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/ROOT/catalog/search?count=1");
EXPECT_EQ(r->status, 200);
@@ -1154,6 +1811,7 @@ TEST_F(LibraryServerTest, catalog_v2_languages)
#define CATALOG_V2_ENTRIES_PREAMBLE0(x) \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\n" \
" xmlns:dc=\"http://purl.org/dc/terms/\"\n" \
" xmlns:opds=\"https://specs.opds.io/opds-1.2\"\n" \
" xmlns:opensearch=\"http://a9.com/-/spec/opensearch/1.1/\">\n" \
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n" \
@@ -1258,70 +1916,6 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
);
}
TEST_F(LibraryServerTest, suggestions_in_range)
{
/**
* Attempt to get 50 suggestions in steps of 5
* The suggestions are returned in the json format
* [{sugg1}, {sugg2}, ... , {suggN}, {suggest ft search}]
* Assuming the number of suggestions = (occurance of "{" - 1)
*/
{
int suggCount = 0;
for (int i = 0; i < 10; i++) {
std::string url = "/ROOT/suggest?content=zimfile&term=ray&start=" + std::to_string(i*5) + "&count=5";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 5);
suggCount += currCount;
}
ASSERT_EQ(suggCount, 50);
}
// Attempt to get 10 suggestions in steps of 5 even though there are only 8
{
std::string url = "/ROOT/suggest?content=zimfile&term=song+for+you&start=0&count=5";
const auto r1 = zfs1_->GET(url.c_str());
std::string body = r1->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 5);
url = "/ROOT/suggest?content=zimfile&term=song+for+you&start=5&count=5";
const auto r2 = zfs1_->GET(url.c_str());
body = r2->body;
currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 3);
}
// Attempt to get 10 suggestions even though there is only 1
{
std::string url = "/ROOT/suggest?content=zimfile&term=strong&start=0&count=5";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 1);
}
// No Suggestion
{
std::string url = "/ROOT/suggest?content=zimfile&term=oops&start=0&count=5";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 0);
}
// Out of bound value
{
std::string url = "/ROOT/suggest?content=zimfile&term=ray&start=-2&count=-1";
const auto r = zfs1_->GET(url.c_str());
std::string body = r->body;
int currCount = std::count(body.begin(), body.end(), '{') - 1;
ASSERT_EQ(currCount, 0);
}
}
TEST_F(LibraryServerTest, catalog_v2_individual_entry_access)
{
const auto r = zfs1_->GET("/ROOT/catalog/v2/entry/raycharles");