Compare commits

..

176 Commits

Author SHA1 Message Date
Matthieu Gautier
9f19327023 Fix maybe initialized warning in httplib.
Patch has been send upstream :
https://github.com/yhirose/cpp-httplib/pull/1085
2021-11-05 17:12:33 +01:00
Matthieu Gautier
b2b907f813 Update httplib to last version (0.9.7)
Release url is https://github.com/yhirose/cpp-httplib/releases/tag/v0.9.7
2021-11-05 17:12:33 +01:00
Kelson
9479c0685d Merge pull request #623 from kiwix/update-ci
Re-introduce Ubuntu Impish in CI
2021-10-21 12:41:23 +02:00
Emmanuel Engelhart
09a55d71d6 Re-introduce Ubuntu Impish in CI 2021-10-21 12:36:05 +02:00
Matthieu Gautier
503eb5c4ce Merge pull request #621 from kiwix/fix_ci_docker_version 2021-10-19 11:49:20 +02:00
Matthieu Gautier
f714ff8d3e New docker image version is 31. 2021-10-18 18:09:36 +02:00
Matthieu Gautier
08e3d52957 Merge pull request #607 from kiwix/issue/571 2021-10-12 17:40:27 +02:00
Manan Jethwani
30e4c549e4 exposed fileExist, getMimeTypeForFile and getFileCoontent functions 2021-10-12 19:44:38 +05:30
Manan Jethwani
b7b385d87b added custom index template 2021-10-12 19:44:05 +05:30
Matthieu Gautier
e46b0c07b5 Merge pull request #617 from kiwix/adapt_new_libzim_api 2021-09-30 14:52:17 +02:00
Matthieu Gautier
cd9fb541fc Fix method call for new libzim API.
`add_archive` is now `addArchive`.
2021-09-29 11:55:22 +02:00
Matthieu Gautier
3b942bb745 Merge pull request #602 from kiwix/partial_opds_entries
OPDS feed with partial entries
2021-09-09 12:07:02 +02:00
Veloman Yunkan
c0bda426b4 Removed duplication across two mustache templates
Deduplicated the mustache templates static/templates/catalog_v2_entries.xml
and static/templates/catalog_v2_complete_entry.xml (the latter was
renamed to static/templates/catalog_v2_entry.xml).
2021-09-09 12:19:22 +04:00
Veloman Yunkan
b3f7556096 Added partial entries feed to the OPDS root feed 2021-09-09 12:19:22 +04:00
Veloman Yunkan
4c657c082e /catalog/v2/partial_entries OPDS API endpoint 2021-09-09 12:19:22 +04:00
Veloman Yunkan
e773a29f29 Rearranged elements in OPDS entry XML 2021-09-09 12:19:22 +04:00
Veloman Yunkan
e15a0f4338 /catalog/v2/entry/<entry_id> OPDS API endpoint 2021-09-09 12:19:22 +04:00
Veloman Yunkan
12d9b69806 OPDSDumper::dumpOPDSCompleteEntry() 2021-09-09 12:19:22 +04:00
Veloman Yunkan
027854e4f4 Extracted getSingleBookData() in opds_dumper.cpp 2021-09-09 12:19:22 +04:00
Matthieu Gautier
417e7471ac Merge pull request #614 from kiwix/disable-impish 2021-09-09 10:10:50 +02:00
Matthieu Gautier
51ac1240f8 PPA: Temporarily disable Impish builds 2021-09-09 10:05:18 +02:00
Kelson
ea6413ff88 Merge pull request #591 from kiwix/suggestion_range
Allow kiwix-serve to get suggestions of custom range
2021-08-20 08:09:35 +02:00
Maneesh P M
61209ea0d7 Allow kiwix-serve to get suggestions of custom range
This will allow handle_suggest API to accept two arguments `start` and
`suggestionLength` that will allow handle_suggest to retrieve
suggestions in the given range rather than the default 0-10 range.
2021-08-19 21:05:39 +05:30
Kelson
e9eaadde9e Merge pull request #567 from kiwix/suggestion_api_fix 2021-08-14 19:21:29 +02:00
Maneesh P M
8a4080baba Update libkiwix with new libzim api 2021-08-14 22:26:39 +05:30
Kelson
ba05999cba Merge pull request #604 from kiwix/issue/603 2021-08-11 06:50:30 +02:00
Manan Jethwani
a4c3cad018 fixed books availablity on larger screens and added zoom level support 2021-08-10 21:45:10 +05:30
Kelson
83e757a530 Merge pull request #600 from kiwix/dynamic_select_box_value
Use OPDS API to populate categories/languages select boxes on Kiwix Serve welcome page
2021-08-07 15:33:52 +02:00
Manan Jethwani
5e8f3a5505 added use of lang and category api for select boxes on welcome page 2021-08-07 02:39:50 +05:30
Manan Jethwani
fe93035a4c updated welcome page to support OPDS multiple Icon 2021-08-07 02:35:29 +05:30
Kelson
6e26c5aa75 Merge pull request #577 from kiwix/opds_multiple_icons
Support for multiple illustrations in OPDS entry
2021-08-05 23:22:07 +02:00
Veloman Yunkan
452283cfe6 Handling of /meta?name=Illustration_WxH@1 requests 2021-08-05 22:28:09 +04:00
Veloman Yunkan
e5168d8b3d Support for multiple illustrations in OPDS entry 2021-08-05 22:21:13 +04:00
Matthieu Gautier
b8aee8a42c Merge pull request #597 from kiwix/fix_get_results 2021-08-04 15:57:58 +02:00
Maneesh P M
9addd82d2d Fix usage of zim::Searcher::getResults() in libkiwix
The correct usage does not require the user to calculate an `end` using
the `pageLength`. We can directly use getResults(start, pageLength)
2021-08-04 19:20:50 +05:30
Maneesh P M
e74e7f5623 Add unit test for incremental searching
With this, we eventually want to see the usage of getResults giving
a FAILING TEST. This happens because the second argument to
getResults is NOT `end` of the range, but `maxResultCount` to retrieve.
This will be fixed in the next commit.
2021-08-04 19:20:05 +05:30
Matthieu Gautier
a032d65eb8 Merge pull request #576 from kiwix/extend_libkiwix_structures_to_use_libzim 2021-08-03 11:50:24 +02:00
Maneesh P M
19afe9442f Remove OriginId functions since they are not useful right now 2021-08-03 11:42:58 +02:00
Maneesh P M
a3ba7619df Update Manager to use Archive instead of Reader
kiwix::Manager uses Reader to import a zim file, it should be using
zim::Archive directly.
2021-08-03 11:42:58 +02:00
Maneesh P M
8b12434ff2 Update kiwix::book to use libzim structure
Some methods in kiwix::Book uses wrapper structure reader. This usage should
be extended from the native libzim structure zim::Archive
2021-08-03 11:42:58 +02:00
Matthieu Gautier
b4f7dfa5a2 Merge pull request #553 from kiwix/catalog_languages_endpoint 2021-08-03 11:41:31 +02:00
Veloman Yunkan
ab3095745e Languages OPDS feed includes book counts 2021-08-03 11:32:38 +02:00
Veloman Yunkan
45adda44b3 Got rid of <content> node in languages OPDS entry 2021-08-03 11:32:38 +02:00
Veloman Yunkan
96cf7e78a5 OPDSDumper::categoriesOPDSFeed() with no args 2021-08-03 11:32:38 +02:00
Veloman Yunkan
dd118df612 Got rid of langMap in opds_dumper.cpp
Language code to human friendly name translation is now done with the
help of the ICU library. It works if the line

```
-include $(LANGSRCDIR)/resfiles.mk
```

in the file `source/data/Makefile.in` of the icu4c dependency is not
commented out. Currently, the said line is commented out (along with
some other include's) by the `icu4c_custom_data.patch` patch of the
`kiwix-build` tool.
2021-08-03 11:32:38 +02:00
Veloman Yunkan
8a4248e48e Language code in /catalog/v2/languages entries 2021-08-03 11:32:38 +02:00
Veloman Yunkan
5f90f5ee2a Preliminary version of /catalog/v2/languages 2021-08-03 11:32:38 +02:00
Veloman Yunkan
64b55dbdc7 Made test library.xml a multi-language library 2021-08-03 11:32:38 +02:00
Veloman Yunkan
18871b4b15 Helper function Library::getBookPropValueSet()
Introduced a helper function `Library::getBookPropValueSet()` and
deduplicated Library::getBooks{Languages,Creators,Publishers}() methods.
2021-08-03 11:32:38 +02:00
Veloman Yunkan
b2027b397c List of languages entry in /catalog/v2/root.xml
Added a new entry in /catalog/v2/root.xml that points to a
not-yet-existing list of languages navigation feed.
2021-08-03 11:32:38 +02:00
Kelson
49322f5961 Merge pull request #596 from kiwix/better_filter
improved browser lang filter working
2021-07-31 23:24:14 +02:00
Manan Jethwani
0466b9759c improved browser lang filter working 2021-07-30 12:57:59 +05:30
Kelson
20cdefcdb8 Merge pull request #593 from kiwix/remove_groovy_package
Remove groovy deb package
2021-07-28 21:40:02 +02:00
Emmanuel Engelhart
6ea40f57da Remove groovy deb package 2021-07-28 21:34:24 +02:00
Kelson
a312d2218d Merge pull request #590 from kiwix/root_prefix_addition
corrected relative links in preview and icon url
2021-07-25 08:59:24 +02:00
Manan Jethwani
15839df594 corrected relative links in preview and icon url 2021-07-24 19:26:22 +05:30
Kelson
03a929e88e Merge pull request #583 from kiwix/download-modal
Modal download  box on Kiwix Serve welcome page
2021-07-13 16:47:58 +02:00
Manan Jethwani
646502f9cf changed font style for modal 2021-07-13 20:00:43 +05:30
Manan Jethwani
a8a96a99f4 corrected working of magnet link 2021-07-13 00:23:38 +05:30
Manan Jethwani
a517d3b529 added modal for downloading zim file on welcome page 2021-07-12 17:59:26 +05:30
Kelson
60f0f81286 Merge pull request #559 from kiwix/Css_revamp
Revamped Kiwix Serve Welcome page layout
2021-07-08 12:36:19 +02:00
Manan Jethwani
2ed9a50eca fixed button allignment 2021-07-08 12:33:28 +02:00
Manan Jethwani
bce922ab89 bug fix for loader 2021-07-08 12:33:28 +02:00
Manan Jethwani
ad7a63a471 minor change in UI 2021-07-08 12:33:28 +02:00
Manan Jethwani
6e8200637e corrected search button in mobile view 2021-07-08 12:33:28 +02:00
Manan Jethwani
cc45c840d1 fixed minor codefactor issue 2021-07-08 12:33:28 +02:00
Manan Jethwani
0590f27fa1 corrected select box and search bar design 2021-07-08 12:33:28 +02:00
Manan Jethwani
dd27c3a873 changed tile background color 2021-07-08 12:33:28 +02:00
Manan Jethwani
736841818d fixed font and other minor issues in title cards 2021-07-08 12:33:28 +02:00
Manan Jethwani
c1868e22f4 minor codefactor fix 2021-07-08 12:33:28 +02:00
Manan Jethwani
aabfc1d82e fixed card design 2021-07-08 12:33:28 +02:00
Manan Jethwani
2effb3490e minoor changes in responsive behaviour 2021-07-08 12:33:28 +02:00
Manan Jethwani
55672b0288 revamped basic layout and cards 2021-07-08 12:33:28 +02:00
Kelson
0abbeabfe2 Merge pull request #568 from kiwix/ppa-impish
PPA: Build for Ubuntu Impish
2021-07-08 10:24:52 +02:00
Kunal Mehta
1bf52e8ebe PPA: Build for Ubuntu Impish 2021-07-08 09:50:55 +02:00
Kelson
e2db1b3688 Merge pull request #574 from kiwix/remove_mustache_public_header
Fix public headers inclusion (+ small other fixes)
2021-07-07 18:00:17 +02:00
Matthieu Gautier
0b6b6716de Rename split argument from trimEmpty to dropEmpty. 2021-07-07 14:43:13 +02:00
Matthieu Gautier
18b6433322 Correct method declaration in SuggestionItem 2021-07-07 14:43:13 +02:00
Matthieu Gautier
b70c92cade Move back used helper functions to the public API.
- Add docstring
- Move the declaration in kiwix namespace.
- Adapt our include to include the right headers.
2021-07-07 14:43:13 +02:00
Matthieu Gautier
09d843da3a Add a (empty) include/tools.h header.
This header will contain our public tool functions.
2021-07-07 14:43:13 +02:00
Matthieu Gautier
fa83a61a54 Move all public *Tools.h in src.
This by definition remove all the tool functions from the public API.
2021-07-07 14:43:13 +02:00
Matthieu Gautier
967eb10cbf Merge pull request #578 from kiwix/fix_ci_deps
Use correct deps archive in the CI.
2021-07-07 10:59:53 +02:00
Matthieu Gautier
feeee25eac Use correct deps archive in the CI.
Now that project is named libkiwix, the dependencies archive is also
renamed.
2021-07-07 10:53:07 +02:00
Matthieu Gautier
1c0b4502cd Merge pull request #536 from kiwix/internally_drop_reader_searcher 2021-07-06 16:18:10 +02:00
Maneesh P M
6f639144ab Add unit tests for Searcher and Reader
Even though we will be removing the wrappers soon, the test coverage
should be complete and we could simply remove these files later.
2021-07-03 14:07:14 +05:30
Maneesh P M
a94a03cd22 Remove unwanted reader functions
Removing the functions in InternalServer that are no longer needed.
2021-07-03 14:07:14 +05:30
Maneesh P M
bc821638da Drop wrapper structures from handle_search
Since we now have SearcherRenderer that can work with native libzim
structure, we will drop the wrapper and use them instead.
2021-07-03 14:07:12 +05:30
Maneesh P M
bcece66960 Add SearchRenderer handles for libzim structures
Introduces a new member mp_search that houses the zim::Search object,
adds a new constructor for this purpose. This commit also add an
overload for getHtml that takes start and end integers as arguments
since they are not part of the search object we include.
2021-07-03 14:05:50 +05:30
Maneesh P M
c046f64d83 Drop Reader and Entry wrappers from handle_content 2021-07-03 14:05:50 +05:30
Maneesh P M
75b4d311d7 Drop Reader from InternalServer::handle_random 2021-07-03 14:04:04 +05:30
Maneesh P M
a236751c74 Drop usage of Reader from InternalServer::handle_suggest 2021-07-03 14:04:04 +05:30
Maneesh P M
7d68926539 Drop usage of Reader from InternalServer::handle_meta
This is essentially a code move of meta handlers from using Reader
functions to directly using Archive.
2021-07-03 14:04:02 +05:30
Maneesh P M
940368b8ac Add m_archives and getArchiveById to Library
These members will mirror the functionality offered by equivalent usage
of Reader class.
2021-07-03 14:02:31 +05:30
Kelson
0594e60df3 Merge pull request #527 from kiwix/catalog_search_url_generation
OpdsCatalog::getSearchUrl()
2021-06-30 21:35:43 +02:00
Veloman Yunkan
b5c1b26761 OpdsCatalog::getSearchUrl() 2021-06-30 18:27:00 +02:00
Kelson
4124ad30d5 Merge pull request #561 from kiwix/issue/557
Keep Kiwix Serve filter values over time
2021-06-25 08:05:21 +02:00
Manan Jethwani
3c5d73027d created separate variable for time delta 2021-06-24 15:41:28 +05:30
Manan Jethwani
d88bdd3ebf corrected filter on no results 2021-06-23 14:15:22 +05:30
Manan Jethwani
5cfe34a5c2 corrected filter working 2021-06-22 19:36:22 +05:30
Manan Jethwani
ad133bc9a3 added cookies for filter effect 2021-06-21 19:57:52 +05:30
Kelson
8eabae6286 Merge pull request #560 from kiwix/fix_compilation_illustration 2021-06-19 13:15:44 +02:00
Maneesh P M
f3c96b23fd Use getIllustrationItem instead of getFaviconEntry method
With openzim/libzim#540 we now have a new function to get
illustration(previously favicon in 48x48 size and unity scale) in
multiple sizes. We need to replace getFaviconEntry with this new
getIllustrationItem method.
2021-06-19 10:23:24 +05:30
Kelson
a92e9d8756 Merge pull request #490 from soumyankar/404ContentHomeButton
404 content home button
2021-06-17 09:59:45 +02:00
Vertigo
8d39b2c4c1 Added content ZIM home button on 404 2021-06-17 12:51:27 +05:30
Kelson
6d237ff1d5 Merge pull request #492 from kiwix/opds_categories_feed 2021-06-08 20:01:37 +02:00
Veloman Yunkan
78083f1f4a Moved OPDS templates under static/templates 2021-06-08 20:37:00 +04:00
Veloman Yunkan
dd60235010 Fixed the self link in the output of /catalog/v2/entries 2021-06-08 20:37:00 +04:00
Veloman Yunkan
e799f2ff1e OPDSDumper::dumpOPDSFeed() works via mustache
This changes the output of `/catalog/search` as follows:

- Entire search query (rather than only the value of the `q` parameter)
  is put in the <title> node.

- Search performed with an empty query presents itself as "All zims".

- The feed id remains stable for identical searches on the same
  library.
2021-06-08 20:37:00 +04:00
Veloman Yunkan
312f2cb560 Moved handle_catalog_v2*() methods into a new file 2021-06-08 20:37:00 +04:00
Veloman Yunkan
fa42cbc48f Pagination info in /catalog/v2/entries 2021-06-08 20:37:00 +04:00
Veloman Yunkan
f1797993af Reused InternalServer::search_catalog() 2021-06-08 20:37:00 +04:00
Veloman Yunkan
f886c8c07b Root url is normalized once in the constructor 2021-06-08 20:37:00 +04:00
Veloman Yunkan
9ca6bd006f /catalog/v2/categories goes through OPDSDumper too 2021-06-08 20:37:00 +04:00
Veloman Yunkan
cdacc0caf1 /catalog/v2/entries going through OPDSDumper
OPDSDumper sensed threats to its job security, so it lobbied to be
involved in handling the /catalog/v2 endpoints, too.
2021-06-08 20:37:00 +04:00
Veloman Yunkan
dfad1c3815 /catalog/v2/searchdescription.xml 2021-06-08 20:37:00 +04:00
Veloman Yunkan
07252a127a /catalog/v2/entries is also a search endpoint 2021-06-08 20:37:00 +04:00
Veloman Yunkan
b60e3ffb26 RequestContext::get_optional_param() 2021-06-08 20:37:00 +04:00
Veloman Yunkan
70d42aec98 A small simplification 2021-06-08 20:37:00 +04:00
Veloman Yunkan
4aa3c792aa Extracted get_search_filter() 2021-06-08 20:37:00 +04:00
Veloman Yunkan
208dece7e3 Reordered several statements
Reordered several statements so that the next couple of commits are a
little simpler.
2021-06-08 20:37:00 +04:00
Veloman Yunkan
19b59fd72f Serving /catalog/v2/entries
/catalog/v2/entries is intended to play the combined role of
/catalog/root.xml and /catalog/search of the old OPDS API. Currently,
the latter role is not yet implemented.

Implementation note: instead of tweaking and reusing
`OPDSDumper::dumpOPDSFeed()`, the generation of the OPDS feed is done via `mustache`
and a new template `static/catalog_v2_entries.xml`.
2021-06-08 20:37:00 +04:00
Veloman Yunkan
92c2de8d46 Enter InternalServer::m_library_id
The new field is intended to serve as a seed for generating semi-stable
OPDS feed ids that only need to change when the library is updated.
2021-06-08 20:37:00 +04:00
Veloman Yunkan
feeb9f206e /catalog/v2/* XMLs are OPDS 1.2 2021-06-08 20:37:00 +04:00
Veloman Yunkan
a1520ce7f1 Fixing the xenial build
Under Ubuntu 16.04/xenial, ccache seems to have issues with multiline
raw string literals used inside macros.
2021-06-08 20:37:00 +04:00
Veloman Yunkan
2e53b51696 Serving /catalog/v2/categories 2021-06-08 20:37:00 +04:00
Veloman Yunkan
b259afa408 Library::getBooksCategories()
Note: no unit test added
2021-06-08 20:37:00 +04:00
Veloman Yunkan
3c3cf08a1a Serving /catalog/v2/root.xml
Note: This commit somewhat relaxes validation of non variable
`<updated>` elements in the OPDS feed - the contents of any `<updated>`
element is replaced with the YYYY-MM-DDThh:mm:ssZ placeholder.
2021-06-08 16:03:43 +04:00
Veloman Yunkan
54b78eaf56 Moved gen_date_str() to tools/otherTools.cpp 2021-06-08 16:03:43 +04:00
Veloman Yunkan
1e0ff1fbb0 Fixed the double colon in OPDS date string 2021-06-08 16:03:43 +04:00
Veloman Yunkan
5b272ac49c Fixed handling of /catalogBLABLA/root.xml & alike
Also removed an unneeded namespace qualifier.
2021-06-08 16:03:43 +04:00
Veloman Yunkan
0a3d293ae0 Broke Server.404 with /catalogBLABLABLA/root.xml
The new negative test-point demonstrates that Kiwix server doesn't
distinguish /catalogBLABLABLA from /catalog.
2021-06-08 16:03:43 +04:00
Kelson
86ef2e2199 Merge pull request #550 from kiwix/remove-bintray
Remove Bintray badge
2021-06-07 16:01:37 +02:00
Emmanuel Engelhart
a0332e7599 Remove Bintray badge 2021-06-07 15:55:19 +02:00
Kelson
2ef488816c Merge pull request #534 from kiwix/filter_library
Add filters to kiwix-serve welcome page
2021-06-07 15:46:37 +02:00
Manan Jethwani
1ccafe2d97 minor changes in fadeout effect 2021-06-07 15:38:31 +02:00
Manan Jethwani
d6c62b3cd3 corrected spinner and fadeout effect 2021-06-07 15:37:20 +02:00
Manan Jethwani
f39c558d2a added fade out 2021-06-07 15:37:20 +02:00
Manan Jethwani
5b46ad5934 added spinned 2021-06-07 15:37:20 +02:00
Manan Jethwani
49dbd0aa52 fixed reset filters link 2021-06-07 15:37:20 +02:00
Manan Jethwani
179f0faeb1 added minor features 2021-06-07 15:37:20 +02:00
Manan Jethwani
bb92f26b60 added filter functionality 2021-06-07 15:37:20 +02:00
Matthieu Gautier
3a4e8303a0 Merge pull request #541 from kiwix/adding_dynamic_and_subset_loading
Dynamic and subset loading of catalogue in kiwix-serve
2021-06-07 15:35:13 +02:00
Manan Jethwani
063bb8cd65 added dynamic and subset loading of zim-files in kiwix-serve 2021-06-01 19:33:42 +05:30
Kelson
b54e5ab969 Merge pull request #543 from kiwix/add-libmicrohttpd-compilation-hint
Add libmicrohttpd compilation hint
2021-05-30 15:51:40 +02:00
Emmanuel Engelhart
2632a21d24 Move Repology to wiki 2021-05-30 15:46:45 +02:00
Emmanuel Engelhart
5c97b1fff9 gtest is need for testing 2021-05-30 15:43:21 +02:00
Emmanuel Engelhart
4f7175ad59 Libkiwix, not Kiwix library 2021-05-30 15:42:28 +02:00
Emmanuel Engelhart
f4b8d0c303 Add libmicrohttpd compilation hint 2021-05-30 15:35:40 +02:00
Matthieu Gautier
188694f2a1 Merge pull request #510 from kiwix/add_function_zimId 2021-05-26 15:15:13 +02:00
Maneesh P M
e2f6d91d51 Remove get_readerIndex in favor of get_zimId
The function get_readerIndex was used to get the zimId using an ordered
vector of readers. Now we can use get_zimId directly.
2021-05-26 14:45:25 +02:00
Maneesh P M
c35f6f9142 Add get_zimId method to Result
get_zimId method allows the user to get the uuid of the archive from
which a result is retrieved directly from the search result itself.
2021-05-26 14:45:25 +02:00
Matthieu Gautier
7f0d3004c9 Merge pull request #505 from kiwix/suggestion_snippets 2021-05-26 11:04:52 +02:00
Maneesh P M
5567d8ca49 Replace std::vector<std::string> with SuggestionItem
Each sugestions used to be stored as vector of strings to hold various values
such as title, path etc inside them. With this commit, we use the new
dedicated class `SuggestionItem` to do the same.
2021-05-26 10:53:39 +02:00
Maneesh P M
5315034afe Introduce SuggestionItem class
This is a helper class that allows to create and manage individual
suggestion item and their data.
2021-05-26 10:53:00 +02:00
Maneesh P M
3288cd80e5 Render suggestion snippet properly
To render the snippets properly, we need to use the _renderItem property
of the autocomple ui.
2021-05-26 10:53:00 +02:00
Maneesh P M
56434de79e Set label to title snippet if present
With openzim/libzim#545 we now support snippet generation of titles
which can be used as the display label on the ui for highlighted titles
via the "label" field.
The old version used plain title which is still available in the value
field.
2021-05-26 10:52:58 +02:00
Matthieu Gautier
e9ba151e6f Merge pull request #539 from kiwix/fix_windows_build
Avoid windows header to define min/max macros.
2021-05-26 09:52:43 +02:00
Matthieu Gautier
5f83944699 Avoid windows header to define min/max macros.
PR #507 use std::min.
But on windows, the header define min and max macros and so the
compilation is broken.
Add `-DNOMINMAX` define to avoid that.
2021-05-26 09:20:17 +02:00
Kelson
9c0ae835e2 Merge pull request #537 from kiwix/search_iterator_api_rename
Update libkiwix with search iterator rename in libzim
2021-05-26 08:49:26 +02:00
Maneesh P M
e5fac30cee Update libkiwix with search iterator rename in libzim
Search iterator API in libzim has been shifted to use camel case naming.
This has to be accomodated in libkiwix as well.
2021-05-26 08:39:13 +02:00
Matthieu Gautier
7ef08b670b Merge pull request #538 from kiwix/revert-530-adding_dynamic_and_subset_loading
Revert "Kiwix Serve welcome page dynamic and subset loading (OPDS based)"
2021-05-25 17:33:23 +02:00
Matthieu Gautier
2736a46cfe Revert "Kiwix Serve welcome page dynamic and subset loading (OPDS based)" 2021-05-25 17:30:05 +02:00
Kelson
672b4fc907 Merge pull request #530 from kiwix/adding_dynamic_and_subset_loading
Kiwix Serve welcome page dynamic and subset loading (OPDS based)
2021-05-25 16:22:54 +02:00
Manan Jethwani
012973d14a added dynamic and subset loading of zim-files in kiwix-serve 2021-05-25 02:41:12 +05:30
Kelson
67984cca5b Merge pull request #535 from kiwix/further-kiwix-lib-renaming
Rename kiwix-lib in libkiwix
2021-05-23 21:57:03 +02:00
Emmanuel Engelhart
d4e35c7067 Rename kiwix-lib in libkiwix 2021-05-23 21:46:52 +02:00
Matthieu Gautier
6e37cabaea Merge pull request #529 from kiwix/libkiwix-github-url-fix
Fix Libkiwix Github repository URLS
2021-05-21 16:09:15 +02:00
Emmanuel Engelhart
c8b7f8772a Fix Libkiwix Github repository URLS 2021-05-20 08:56:44 +02:00
Veloman Yunkan
fc7484ac86 Merge pull request #528 from kiwix/fix_suggestion_search_result_page
Check if bookName is available in url parameters
2021-05-19 00:36:15 +04:00
Maneesh P M
c236f3a32b Check if bookName is available in url parameters
In certain pages like the search result page, bookName is not of the
form `/bookName/endpoint?parameters`. Rather it is available as a query
parameter. From these pages bookName should be assigned from parameters.
2021-05-19 01:12:29 +05:30
Matthieu Gautier
3c7faddb6e Merge pull request #526 from kiwix/lizim_search_api_change
Fixed the libkiwix build broken by the changed libzim search API
2021-05-17 15:06:15 +02:00
Veloman Yunkan
cd02b4de3b Dummy application of new libzim search API
Didn't take any advantage of the new libzim search API. Just fixed the
libkiwix build in the most straightforward way.
2021-05-15 23:34:51 +04:00
Kelson
5188355878 Merge pull request #525 from kiwix/html-only-root-link
Insert root link only if html content
2021-05-14 16:51:33 +02:00
Emmanuel Engelhart
05cc3d015f Insert root link only if html content 2021-05-14 14:49:28 +02:00
Kelson
39b62c6108 Merge pull request #522 from kiwix/libkiwix_ci_fix
Fixed CI after repository rename
2021-05-13 11:09:57 +02:00
Veloman Yunkan
9c43353b72 Fixed CI after repository rename 2021-05-13 12:49:44 +04:00
85 changed files with 8570 additions and 2809 deletions

View File

@@ -21,7 +21,7 @@ jobs:
- name: Install deps
shell: bash
run: |
ARCHIVE_NAME=deps2_osx_native_dyn_kiwix-lib.tar.xz
ARCHIVE_NAME=deps2_osx_native_dyn_libkiwix.tar.xz
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C $HOME
- name: Compile
shell: bash
@@ -94,7 +94,7 @@ jobs:
HOME: /home/runner
runs-on: ubuntu-latest
container:
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-26"
image: "kiwix/kiwix-build_ci:${{matrix.image_variant}}-31"
steps:
- name: Extract branch name
shell: bash
@@ -115,7 +115,7 @@ jobs:
- name: Install deps
shell: bash
run: |
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_kiwix-lib.tar.xz
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_libkiwix.tar.xz
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C /home/runner
- name: Compile
shell: bash
@@ -134,7 +134,7 @@ jobs:
if [[ "${{matrix.target}}" =~ android_.* ]]; then
MESON_OPTION="$MESON_OPTION -Dandroid=true"
fi
cd $HOME/kiwix-lib
cd $HOME/libkiwix
meson . build ${MESON_OPTION}
cd build
ninja
@@ -145,7 +145,7 @@ jobs:
if: startsWith(matrix.target, 'native_')
shell: bash
run: |
cd $HOME/kiwix-lib/build
cd $HOME/libkiwix/build
meson test --verbose
ninja coverage
env:
@@ -154,7 +154,7 @@ jobs:
- name: Publish coverage
shell: bash
run: |
cd $HOME/kiwix-lib
cd $HOME/libkiwix
curl https://codecov.io/bash -o codecov.sh
bash codecov.sh -n "${OS_NAME}_${{matrix.target}}" -Z
rm codecov.sh

View File

@@ -7,7 +7,11 @@ jobs:
strategy:
fail-fast: false
matrix:
distro: [ubuntu-hirsute, ubuntu-groovy, ubuntu-focal, ubuntu-bionic]
distro:
- ubuntu-impish
- ubuntu-hirsute
- ubuntu-focal
- ubuntu-bionic
steps:
- uses: actions/checkout@v2
@@ -30,18 +34,18 @@ jobs:
email: release+launchpad@kiwix.org
distro: ${{ matrix.distro }}
- uses: legoktm/gh-action-build-deb@ubuntu-hirsute
if: matrix.distro == 'ubuntu-hirsute'
name: Build package for ubuntu-hirsute
id: build-ubuntu-hirsute
- uses: legoktm/gh-action-build-deb@ubuntu-impish
if: matrix.distro == 'ubuntu-impish'
name: Build package for ubuntu-impish
id: build-ubuntu-impish
with:
args: --no-sign
ppa: ${{ steps.ppa.outputs.ppa }}
- uses: legoktm/gh-action-build-deb@ubuntu-groovy
if: matrix.distro == 'ubuntu-groovy'
name: Build package for ubuntu-groovy
id: build-ubuntu-groovy
- 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 }}

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ subprojects/googletest-release*
*.class
build/
.vscode/
builddir/

View File

@@ -1,3 +1,8 @@
libkiwix 10.0.0
===============
* ...
kiwix-lib 9.4.1
===============

107
README.md
View File

@@ -1,50 +1,50 @@
Kiwix library
=============
Libkiwix
========
The Kiwix library provides the [Kiwix](https://kiwix.org) software
suite core. It contains the code shared by all Kiwix ports (Windows,
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, ...).
[![Download](https://api.bintray.com/packages/kiwix/kiwix/kiwixlib/images/download.svg)](https://bintray.com/kiwix/kiwix/kiwixlib/_latestVersion)
[![Build Status](https://github.com/kiwix/kiwix-lib/workflows/CI/badge.svg?query=branch%3Amaster)](https://github.com/kiwix/kiwix-lib/actions?query=branch%3Amaster)
[![CodeFactor](https://www.codefactor.io/repository/github/kiwix/kiwix-lib/badge)](https://www.codefactor.io/repository/github/kiwix/kiwix-lib)
[![Codecov](https://codecov.io/gh/kiwix/kiwix-lib/branch/master/graph/badge.svg)](https://codecov.io/gh/kiwix/kiwix-lib)
[![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)
[![Codecov](https://codecov.io/gh/kiwix/libkiwix/branch/master/graph/badge.svg)](https://codecov.io/gh/kiwix/libkiwix)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Packaging status](https://repology.org/badge/vertical-allrepos/kiwix-lib.svg)](https://repology.org/project/kiwix-lib/versions)
Disclaimer
----------
This document assumes you have a little knowledge about software
compilation. If you experience difficulties with the dependencies or
with the Kiwix libary compilation itself, we recommend to have a look
to [kiwix-build](https://github.com/kiwix/kiwix-build).
with the Libkiwix compilation itself, we recommend to have a look to
[kiwix-build](https://github.com/kiwix/kiwix-build).
Preamble
--------
Although the Kiwix library can be (cross-)compiled on/for many
sytems, the following documentation explains how to do it on POSIX
ones. It is primarly thought for GNU/Linux systems and has been tested
on recent releases of Ubuntu and Fedora.
Although the Libkiwix can be (cross-)compiled on/for many sytems, the
following documentation explains how to do it on POSIX ones. It is
primarly thought for GNU/Linux systems and has been tested on recent
releases of Ubuntu and Fedora.
Dependencies
------------
The Kiwix library relies on many third parts software libraries. They
are prerequisites to the Kiwix library compilation. Following
libraries need to be available:
The Libkiwix relies on many third party software libraries. They are
prerequisites to the Libkiwix compilation. Following libraries need to
be available:
* [ICU](https://site.icu-project.org/) (package `libicu-dev` on Ubuntu)
* [ZIM](https://openzim.org/) (package `libzim-dev` on Ubuntu)
* [Pugixml](https://pugixml.org/) (package `libpugixml-dev` on Ubuntu)
* [Mustache](https://github.com/kainjow/Mustache) (Just copy the
header `mustache.hpp` somewhere it can be found by the compiler and/or
set CPPFLAGS with correct `-I` option). Use Mustache version 4.1 or above.
* [libcurl](https://curl.se/libcurl) (`libcurl4-gnutls-dev`, `libcurl4-nss-dev` or `libcurl4-openssl-dev` on Ubuntu)
* [microhttpd](https://www.gnu.org/software/libmicrohttpd) (package `libmicrohttpd-dev` on Ubuntu)
* [zlib](https://zlib.net/) (package `zlib1g-dev` on Ubuntu)
* [Libcurl](https://curl.se/libcurl) (`libcurl4-gnutls-dev`, `libcurl4-nss-dev` or `libcurl4-openssl-dev` on Ubuntu)
* [Microhttpd](https://www.gnu.org/software/libmicrohttpd) (package `libmicrohttpd-dev` on Ubuntu)
* [Zlib](https://zlib.net/) (package `zlib1g-dev` on Ubuntu)
To test the code:
* [Google Test](https://github.com/google/googletest) (package `googletest` on Ubuntu)
The following dependency needs to be available at runtime:
* [Aria2](https://aria2.github.io/) (package `aria2` on Ubuntu)
@@ -56,12 +56,12 @@ In the worse case, you will have to download and compile bleeding edge
version by hand.
If you want to install these dependencies locally, then use the
`kiwix-lib` directory as install prefix.
`libkiwix` directory as install prefix.
Environment
-------------
The Kiwix library builds using [Meson](https://mesonbuild.com/) version
The Libkiwix builds using [Meson](https://mesonbuild.com/) version
0.45 or higher. Meson relies itself on Ninja, pkg-config and few other
compilation tools.
@@ -77,7 +77,7 @@ section.
Compilation
-----------
Once all dependencies are installed, you can compile the Kiwix library
Once all dependencies are installed, you can compile the Libkiwix
with:
```bash
meson . build
@@ -85,7 +85,7 @@ ninja -C build
```
By default, it will compile dynamic linked libraries. All binary files
will be created in the "build" directory created automatically by
will be created in the `build` directory created automatically by
Meson. If you want statically linked libraries, you can add
`--default-library=static` option to the Meson command.
@@ -103,7 +103,7 @@ meson test
Installation
------------
If you want to install the Kiwix library and the headers you just have
If you want to install the Libkiwix and the headers you just have
compiled on your system, here we go:
```bash
ninja -C build install
@@ -146,6 +146,57 @@ cp ninja ../bin
cd ..
```
Custom Index Page
-----------------
to use custom welcome page mention `customIndexPage` argument in `kiwix::internalServer()` or use `kiwix::server->setCustomIndexTemplate()`.
(note - while using custom html file please mention all external links as absolute path.)
to create a HTML template with custom JS you need to have a look at various OPDS based endpoints as mentioned [here](https://wiki.kiwix.org/wiki/OPDS) to load books.
To use JS provided by kiwix-serve you can use the following template to start with ->
```
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title><-- Custom Tittle --></title>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/external/jquery/jquery.js"
></script>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/jquery-ui.min.js"
></script>
<script src="{{root}}/skin/isotope.pkgd.min.js" defer></script>
<script src="{{root}}/skin/iso6391To3.js"></script>
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
</head>
<body>
</body>
</html>
```
- To get books listed using `index.js` add - `<div class="book__list"></div>` under body tag.
- To get number of books listed add - `<h3 class="kiwixHomeBody__results"></h3>` under body tag.
- To add language select box add - `<select id="languageFilter"></select>` under body tag.
- To add language select box add - `<select id="categoryFilter"></select>` under body tag.
- To add search box for books use following form -
```
<form id='kiwixSearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<input type="submit" class="searchButton" value="Search"/>
</form>
```
If you compile manually Libmicrohttpd, you might need to compile it
without GNU TLS, a bug here will empeach further compilation
otherwise.
If the compilation still fails, you might need to get a more recent
version of a dependency than the one packaged by your Linux
distribution. Try then with a source tarball distributed by the

View File

@@ -29,7 +29,7 @@ task writePom {
version '10.0.0' + (System.env.KIWIXLIB_BUILDVERSION == null ? '' : '-'+System.env.KIWIXLIB_BUILDVERSION)
packaging 'aar'
name 'kiwixlib'
url 'https://github.com/kiwix/kiwix-lib'
url 'https://github.com/kiwix/libkiwix'
licenses {
license {
name 'GPLv3'
@@ -44,9 +44,9 @@ task writePom {
}
}
scm {
connection 'https://github.com/kiwix/kiwix-lib.git'
developerConnection 'https://github.com/kiwix/kiwix-lib.git'
url 'https://github.com/kiwix/kiwix-lib'
connection 'https://github.com/kiwix/libkiwix.git'
developerConnection 'https://github.com/kiwix/libkiwix.git'
url 'https://github.com/kiwix/libkiwix'
}
}
}.withXml {

2
debian/control vendored
View File

@@ -15,7 +15,7 @@ Build-Depends: debhelper-compat (= 13),
zlib1g-dev
Standards-Version: 4.5.0
Section: libs
Homepage: https://github.com/kiwix/kiwix-lib
Homepage: https://github.com/kiwix/libkiwix
Rules-Requires-Root: no
Package: libkiwix-dev

View File

@@ -7,6 +7,7 @@ files=(
"include/common/otherTools.h"
"include/common/regexTools.h"
"include/common/networkTools.h"
"include/common/archiveTools.h"
"include/manager.h"
"include/reader.h"
"include/kiwix.h"
@@ -22,6 +23,7 @@ files=(
"src/common/pathTools.cpp"
"src/common/regexTools.cpp"
"src/common/otherTools.cpp"
"src/common/archiveTools.cpp"
"src/common/networkTools.cpp"
"src/common/stringTools.cpp"
"src/xapianSearcher.cpp"

View File

@@ -26,6 +26,10 @@ namespace pugi {
class xml_node;
}
namespace zim {
class Archive;
}
namespace kiwix
{
@@ -43,6 +47,7 @@ class Book
bool update(const Book& other);
void update(const Reader& reader);
void update(const zim::Archive& archive);
void updateFromXml(const pugi::xml_node& node, const std::string& baseDir);
void updateFromOpds(const pugi::xml_node& node, const std::string& urlHost);
std::string getHumanReadableIdFromPath() const;

View File

@@ -103,7 +103,7 @@ class Entry
* Some entry (ie binary ones) have their content plain stored
* in the zim file. Knowing the offset where the content is stored
* an user can directly read the content in the zim file bypassing the
* kiwix-lib/libzim.
* libkiwix/libzim.
*
* @return A pair specifying where to read the content.
* The string is the real file to read (may be different that .zim

View File

@@ -24,6 +24,7 @@
#include <vector>
#include <map>
#include <memory>
#include <zim/archive.h>
#include "book.h"
#include "bookmark.h"
@@ -146,12 +147,14 @@ class Library
{
std::map<std::string, kiwix::Book> m_books;
std::map<std::string, std::shared_ptr<Reader>> m_readers;
std::map<std::string, std::shared_ptr<zim::Archive>> m_archives;
std::vector<kiwix::Bookmark> m_bookmarks;
class BookDB;
std::unique_ptr<BookDB> m_bookDB;
public:
typedef std::vector<std::string> BookIdCollection;
typedef std::map<std::string, int> AttributeCounts;
public:
Library();
@@ -198,6 +201,7 @@ class Library
const Book& getBookByPath(const std::string& path) const;
Book& getBookByPath(const std::string& path);
std::shared_ptr<Reader> getReaderById(const std::string& id);
std::shared_ptr<zim::Archive> getArchiveById(const std::string& id);
/**
* Remove a book from the library.
@@ -233,12 +237,26 @@ class Library
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;
/**
* Get all langagues of the books in the library.
* Get all languagues of the books in the library.
*
* @return A list of languages.
*/
std::vector<std::string> getBooksLanguages() const;
/**
* Get all languagues of the books in the library with counts.
*
* @return A list of languages with the count of books in each language.
*/
AttributeCounts getBooksLanguagesWithCounts() const;
/**
* Get all categories of the books in the library.
*
* @return A list of categories.
*/
std::vector<std::string> getBooksCategories() const;
/**
* Get all book creators of the books in the library.
*
@@ -331,7 +349,12 @@ class Library
friend class OPDSDumper;
friend class libXMLDumper;
private: // types
typedef const std::string& (Book::*BookStrPropMemFn)() const;
private: // functions
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
BookIdCollection filterViaBookDB(const Filter& filter) const;
void updateBookDB(const Book& book);
};

View File

@@ -13,18 +13,8 @@ headers = [
'search_renderer.h',
'server.h',
'kiwixserve.h',
'name_mapper.h'
'name_mapper.h',
'tools.h'
]
install_headers(headers, subdir:'kiwix')
install_headers(
'tools/base64.h',
'tools/networkTools.h',
'tools/otherTools.h',
'tools/pathTools.h',
'tools/regexTools.h',
'tools/stringTools.h',
subdir:'kiwix/tools'
)

33
include/opds_catalog.h Normal file
View File

@@ -0,0 +1,33 @@
/*
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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 Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_OPDS_CATALOG_H
#define KIWIX_OPDS_CATALOG_H
#include "library.h"
namespace kiwix
{
std::string getSearchUrl(const Filter& f);
} // namespace kiwix
#endif // KIWIX_OPDS_CATALOG_H

View File

@@ -26,9 +26,6 @@
#include <pugixml.hpp>
#include "tools/base64.h"
#include "tools/pathTools.h"
#include "tools/regexTools.h"
#include "library.h"
#include "reader.h"
@@ -51,24 +48,50 @@ class OPDSDumper
/**
* Dump the OPDS feed.
*
* @param id The id of the library.
* @param bookIds the ids of the books to include in the feed
* @param query the query used to obtain the list of book ids
* @return The OPDS feed.
*/
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds);
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const;
/**
* Set the id of the opds stream.
* Dump the OPDS feed.
*
* @param bookIds the ids of the books to include in the feed
* @param query the query used to obtain the list of book ids
* @param partial whether the feed should include partial or complete entries
* @return The OPDS feed.
*/
std::string dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const;
/**
* Dump the OPDS complete entry document.
*
* @param bookId the id of the book
* @return The OPDS complete entry document.
*/
std::string dumpOPDSCompleteEntry(const std::string& bookId) const;
/**
* Dump the categories OPDS feed.
*
* @return The OPDS feed.
*/
std::string categoriesOPDSFeed() const;
/**
* Dump the languages OPDS feed.
*
* @return The OPDS feed.
*/
std::string languagesOPDSFeed() const;
/**
* Set the id of the library.
*
* @param id the id to use.
*/
void setId(const std::string& id) { this->id = id;}
/**
* Set the title oft the opds stream.
*
* @param title the title to use.
*/
void setTitle(const std::string& title) { this->title = title; }
void setLibraryId(const std::string& id) { this->libraryId = id;}
/**
* Set the root location used when generating url.
@@ -77,13 +100,6 @@ class OPDSDumper
*/
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
/**
* Set the search url.
*
* @param searchUrl the search url to use.
*/
void setSearchDescriptionUrl(const std::string& searchDescriptionUrl) { this->searchDescriptionUrl = searchDescriptionUrl; }
/**
* Set some informations about the search results.
*
@@ -93,27 +109,13 @@ class OPDSDumper
*/
void setOpenSearchInfo(int totalResult, int startIndex, int count);
/**
* Set the library to dump.
*
* @param library The library to dump.
*/
void setLibrary(Library* library) { this->library = library; }
protected:
kiwix::Library* library;
std::string id;
std::string title;
std::string date;
std::string libraryId;
std::string rootLocation;
std::string searchDescriptionUrl;
int m_totalResults;
int m_startIndex;
int m_count;
bool m_isSearchResult = false;
private:
pugi::xml_node handleBook(Book book, pugi::xml_node root_node);
};
}

View File

@@ -29,20 +29,53 @@
#include <string>
#include "common.h"
#include "entry.h"
#include "tools/pathTools.h"
#include "tools/stringTools.h"
using namespace std;
namespace kiwix
{
/**
* The SuggestionItem is a helper class that contains the info about a single
* suggestion item.
*/
class SuggestionItem
{
// Functions
// Temporarily making the constructor public until the code move is complete
public:
// Create a sugggestion item.
explicit SuggestionItem(const std::string& title, const std::string& normalizedTitle,
const std::string& path, const std::string& snippet = "") :
title(title),
normalizedTitle(normalizedTitle),
path(path),
snippet(snippet) {}
public:
const std::string& getTitle() const { return title;}
const std::string& getNormalizedTitle() const { return normalizedTitle;}
const std::string& getPath() const { return path;}
const std::string& getSnippet() const { return snippet;}
bool hasSnippet() const { return !snippet.empty();}
// Data
private:
std::string title;
std::string normalizedTitle;
std::string path;
std::string snippet;
friend class Reader;
};
/**
* The Reader class is the class who allow to get an entry content from a zim
* file.
*/
using SuggestionsList_t = std::vector<std::vector<std::string>>;
using SuggestionsList_t = std::vector<SuggestionItem>;
class Reader
{
public:
@@ -56,6 +89,13 @@ class Reader
* (.zim extesion).
*/
explicit Reader(const string zimFilePath);
/**
* Create a Reader to read a zim file given by the Archive.
*
* @param archive The shared pointer to the Archive object.
*/
explicit Reader(const std::shared_ptr<zim::Archive> archive);
#ifndef _WIN32
explicit Reader(int fd);
Reader(int fd, zim::offset_type offset, zim::size_type size);
@@ -252,16 +292,6 @@ class Reader
*/
string getScraper() const;
/**
* Get the origId of the zim file.
*
* The origId is only used in the case of patch zim file and is the Id
* of the original zim file.
*
* @return The origId of the zim file as specified in the zim metadata.
*/
string getOrigId() const;
/**
* Get the favicon of the zim file.
*
@@ -453,7 +483,7 @@ class Reader
zim::Archive* getZimArchive() const;
protected:
std::unique_ptr<zim::Archive> zimArchive;
std::shared_ptr<zim::Archive> zimArchive;
std::string zimFilePath;
SuggestionsList_t suggestions;

View File

@@ -21,6 +21,7 @@
#define KIWIX_SEARCH_RENDERER_H
#include <string>
#include <zim/search.h>
namespace kiwix
{
@@ -40,6 +41,8 @@ class SearchRenderer
* Used to generate pagination links.
*/
SearchRenderer(Searcher* searcher, NameMapper* mapper);
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount);
~SearchRenderer();
@@ -74,7 +77,7 @@ class SearchRenderer
protected:
std::string beautifyInteger(const unsigned int number);
Searcher* mp_searcher;
zim::SearchResultSet m_srs;
NameMapper* mp_nameMapper;
std::string searchContent;
std::string searchPattern;

View File

@@ -27,10 +27,10 @@
#include <cctype>
#include <locale>
#include <string>
#include <memory>
#include <vector>
#include <vector>
#include "tools/pathTools.h"
#include "tools/stringTools.h"
#include <zim/search.h>
using namespace std;
@@ -48,10 +48,11 @@ class Result
virtual std::string get_content() = 0;
virtual int get_wordCount() = 0;
virtual int get_size() = 0;
virtual int get_readerIndex() = 0;
virtual std::string get_zimId() = 0;
};
struct SearcherInternal;
struct SuggestionInternal;
/**
* The Searcher class is reponsible to do different kind of search using the
* fulltext index.
@@ -85,12 +86,12 @@ class Searcher
*
* @param search The search query.
* @param resultStart the start offset of the search results (used for pagination).
* @param resultEnd the end offset of the search results (used for pagination).
* @param maxResultCount Maximum results to get from start (used for pagination).
* @param verbose print some info on stdout if true.
*/
void search(const std::string& search,
unsigned int resultStart,
unsigned int resultEnd,
unsigned int maxResultCount,
const bool verbose = false);
/**
@@ -104,12 +105,12 @@ class Searcher
* @param longitude The longitude of the center point.
* @param distance The radius of the disc.
* @param resultStart the start offset of the search results (used for pagination).
* @param resultEnd the end offset of the search results (used for pagination).
* @param maxResultCount Maximum number of results to get from start (used for pagination).
* @param verbose print some info on stdout if true.
*/
void geo_search(float latitude, float longitude, float distance,
unsigned int resultStart,
unsigned int resultEnd,
unsigned int maxResultCount,
const bool verbose = false);
/**
@@ -142,23 +143,29 @@ class Searcher
*/
unsigned int getEstimatedResultCount();
/**
* Get a SearchResultSet object for current search
*/
zim::SearchResultSet getSearchResultSet();
unsigned int getResultStart() { return resultStart; }
unsigned int getResultEnd() { return resultEnd; }
unsigned int getMaxResultCount() { return maxResultCount; }
protected:
std::string beautifyInteger(const unsigned int number);
void closeIndex();
void searchInIndex(string& search,
const unsigned int resultStart,
const unsigned int resultEnd,
const unsigned int maxResultCount,
const bool verbose = false);
std::vector<Reader*> readers;
SearcherInternal* internal;
std::unique_ptr<SearcherInternal> internal;
std::unique_ptr<SuggestionInternal> suggestionInternal;
std::string searchPattern;
unsigned int estimatedResultCount;
unsigned int resultStart;
unsigned int resultEnd;
unsigned int maxResultCount;
private:
void reset();

View File

@@ -55,6 +55,7 @@ namespace kiwix
void setPort(int port) { m_port = port; }
void setNbThreads(int threads) { m_nbThreads = threads; }
void setVerbose(bool verbose) { m_verbose = verbose; }
void setIndexTemplateString(const std::string& indexTemplateString) { m_indexTemplateString = indexTemplateString; }
void setTaskbar(bool withTaskbar, bool withLibraryButton)
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
void setBlockExternalLinks(bool blockExternalLinks)
@@ -65,6 +66,7 @@ namespace kiwix
NameMapper* mp_nameMapper;
std::string m_root = "";
std::string m_addr = "";
std::string m_indexTemplateString = "";
int m_port = 80;
int m_nbThreads = 1;
bool m_verbose = false;

198
include/tools.h Normal file
View File

@@ -0,0 +1,198 @@
/*
* Copyright 2021 Matthieu Gautier <mgautier@kymeria.fr>
*
* 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 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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 Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_TOOLS_H
#define KIWIX_TOOLS_H
#include <string>
#include <vector>
namespace kiwix {
/**
* Return the current directory.
*
* @return the current directory (utf8 encoded)
*/
std::string getCurrentDirectory();
/**
* Return the data directory.
*
* The data directory is a directory where to put data (zim files, ...)
* It depends of the platform and it may be changed by user using environment variable.
*
* The resolution order is :
* - `KIWIX_DATA_DIR` env variable (if set).
* - On Windows :
* . `$APPDATA/kiwix` if $APPDATA is set
* . `$USERPROFILE/kiwix` if $USERPROFILE is set
* - Else :
* . `$XDG_DATA_HOME/kiwix`if $XDG_DATA_HOME is set
* . `$HOME/.local/share/kiwx` if $HOWE is set
* - current directory
*
* @return the path of the data directory (utf8 encoded)
*/
std::string getDataDirectory();
/** Return the path of the executable
*
* Some application may be packaged in auto extractible archive (Appimage) and the
* real executable is different of the path of the archive.
* If `realPathOnly` is true, return the path of the real executable instead of the
* archive launched by the user.
*
* @param realPathOnly If we must return the real path of the executable.
* @return the path of the executable (utf8 encoded)
*/
std::string getExecutablePath(bool realPathOnly = false);
/** Tell if the path is a relative path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path A utf8 encoded path.
* @return true if the path is relative.
*/
bool isRelativePath(const std::string& path);
/** Append a path to another one.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param basePath the base path.
* @param relativePath a path to add to the base path, must be a relative path.
* @return The concatenation of the paths, using the right separator.
*/
std::string appendToDirectory(const std::string& basePath, const std::string& relativePath);
/** Remove the last element of a path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path a path.
* @return The parent directory (or empty string if none).
*/
std::string removeLastPathElement(const std::string& path);
/** Get the last element of a path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path a path.
* @return The base name of the path or empty string if none (ending with a separator).
*/
std::string getLastPathElement(const std::string& path);
/** Compute the absolute path of a relative path based on another one
*
* Equivalent to appendToDirectory followed by a normalization of the path.
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path the base path (if empty, current directory is taken).
* @param relativePath the relative path.
* @return a absolute path.
*/
std::string computeAbsolutePath(const std::string& path, const std::string& relativePath);
/** Compute the relative path of a path relative to another one
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate paths.
*
* @param path the base path.
* @param absolutePath the absolute path to find the relative path for.
* @return a relative path (pointing to absolutePath, relative to path).
*/
std::string computeRelativePath(const std::string& path, const std::string& absolutePath);
/** Sleep the current thread.
*
* This function is provided as a small helper. It is probably better to use native tools.
*
* @param milliseconds The number of milliseconds to wait for.
*/
void sleep(unsigned int milliseconds);
/** Split a string
*
* This function is provided as a small helper. It is probably better to use native tools.
*
* Assuming text = "foo:;bar;baz,oups;"
*
* split(text, ":;", true, true) => ["foo", ":", ";", "bar", ";", "baz,oups", ";"]
* split(text, ":;", true, false) => ["foo", "bar", "baz,oups"] (default)
* split(text, ":;", false, true) => ["foo", ":", "", ";", "bar", ";", "baz,oups", ";", ""]
* split(text, ":;", false, false) => ["foo", "", "bar", "baz,oups", ""]
*
* @param str The string to split.
* @param delims A string of potential delimiters.
* Each charater in the string can be a individual delimiters.
* @param dropEmpty true if empty part must be dropped from the result.
* @param keepDelim true if delimiter must be included from the result.
* @return a list of part (potentially containing delimiters)
*/
std::vector<std::string> split(const std::string& str, const std::string& delims, bool dropEmpty=true, bool keepDelim = false);
/** Convert language code from iso2 code to iso3
*
* This function is provided as a small helper. It is probably better to use native tools
* to manipulate locales.
*
* @param a2code a iso2 code string.
* @return the corresponding iso3 code.
* @throw std::out_of_range if iso2 code is not known.
*/
std::string converta2toa3(const std::string& a2code);
/** Extracts content from given file.
*
* This function provides content of a file provided it's path.
*
* @param path The absolute path provided in string format.
* @return Content of corresponding file in string format.
*/
std::string getFileContent(const std::string& path);
/** checks if file exists.
*
* This function returns boolean stating if file exists or not.
*
* @param path The absolute path provided in string format.
* @return Boolean representing if file exists or not.
*/
bool fileExists(const std::string& path);
/** provides mimetype from filename.
*
* This function provides mimetype from file-name.
*
* @param filename string containing filename.
* @return mimetype from filename in string format.
*/
std::string getMimeTypeForFile(const std::string& filename);
}
#endif // KIWIX_TOOLS_H

View File

@@ -1,4 +1,4 @@
project('kiwix-lib', 'cpp',
project('libkiwix', 'cpp',
version : '10.0.0', # Also change this in android-kiwix-lib-publisher/kiwixLibAndroid/build.gradle
license : 'GPLv3+',
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
@@ -18,7 +18,7 @@ if wrapper.contains('java')
add_languages('java')
endif
# See https://github.com/kiwix/kiwix-lib/issues/371
# See https://github.com/kiwix/libkiwix/issues/371
if ['arm', 'mips', 'm68k', 'ppc', 'sh4'].contains(target_machine.cpu_family())
extra_libs += '-latomic'
endif
@@ -56,6 +56,10 @@ if target_machine.system() == 'windows' and static_deps
extra_cflags += '-DCURL_STATICLIB'
endif
if target_machine.system() == 'windows'
add_project_arguments('-DNOMINMAX', language: 'cpp')
endif
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep, zlib_dep, xapian_dep]
inc = include_directories('include', extra_include)

View File

@@ -6,10 +6,10 @@
#include <sstream>
#include <thread>
#include <chrono>
#include <tools/otherTools.h>
#include <tools/pathTools.h>
#include <tools/stringTools.h>
#include <downloader.h> // For AriaError
#include "tools.h"
#include "tools/pathTools.h"
#include "tools/stringTools.h"
#include "downloader.h" // For AriaError
#ifdef _WIN32
# define ARIA2_CMD "aria2c.exe"

View File

@@ -20,10 +20,16 @@
#include "book.h"
#include "reader.h"
#include "tools.h"
#include "tools/base64.h"
#include "tools/regexTools.h"
#include "tools/networkTools.h"
#include "tools/otherTools.h"
#include "tools/stringTools.h"
#include "tools/pathTools.h"
#include "tools/archiveTools.h"
#include <zim/archive.h>
#include <pugixml.hpp>
@@ -77,26 +83,28 @@ bool Book::update(const kiwix::Book& other)
void Book::update(const kiwix::Reader& reader)
{
m_path = reader.getZimFilePath();
m_pathValid = true;
m_id = reader.getId();
m_title = reader.getTitle();
m_description = reader.getDescription();
m_language = reader.getLanguage();
m_creator = reader.getCreator();
m_publisher = reader.getPublisher();
m_date = reader.getDate();
m_name = reader.getName();
m_flavour = reader.getFlavour();
m_tags = reader.getTags();
m_category = getCategoryFromTags();
m_origId = reader.getOrigId();
m_articleCount = reader.getArticleCount();
m_mediaCount = reader.getMediaCount();
m_size = static_cast<uint64_t>(reader.getFileSize()) << 10;
m_pathValid = true;
update(*reader.getZimArchive());
}
reader.getFavicon(m_favicon, m_faviconMimeType);
void Book::update(const zim::Archive& archive) {
m_path = archive.getFilename();
m_pathValid = true;
m_id = getArchiveId(archive);
m_title = getArchiveTitle(archive);
m_description = getMetaDescription(archive);
m_language = getMetaLanguage(archive);
m_creator = getMetaCreator(archive);
m_publisher = getMetaPublisher(archive);
m_date = getMetaDate(archive);
m_name = getMetaName(archive);
m_flavour = getMetaFlavour(archive);
m_tags = getMetaTags(archive);
m_category = getCategoryFromTags();
m_articleCount = archive.getArticleCount();
m_mediaCount = getArchiveMediaCount(archive);
m_size = static_cast<uint64_t>(getArchiveFileSize(archive)) << 10;
getArchiveFavicon(archive, 48, m_favicon, m_faviconMimeType);
}
#define ATTR(name) node.attribute(name).value()

View File

@@ -9,6 +9,7 @@
# include <unistd.h>
#endif
#include "tools.h"
#include "tools/pathTools.h"
#include "tools/stringTools.h"

View File

@@ -22,6 +22,7 @@
#include "reader.h"
#include "libxml_dumper.h"
#include "tools.h"
#include "tools/base64.h"
#include "tools/regexTools.h"
#include "tools/pathTools.h"
@@ -108,6 +109,7 @@ bool Library::removeBookById(const std::string& id)
{
m_bookDB->delete_document("Q" + id);
m_readers.erase(id);
m_archives.erase(id);
return m_books.erase(id) == 1;
}
@@ -146,11 +148,35 @@ std::shared_ptr<Reader> Library::getReaderById(const std::string& id)
return m_readers.at(id);
} catch (std::out_of_range& e) {}
try {
auto reader = make_shared<Reader>(m_archives.at(id));
m_readers[id] = reader;
return reader;
} catch (std::out_of_range& e) {}
auto book = getBookById(id);
if (!book.isPathValid())
return nullptr;
auto sptr = make_shared<Reader>(book.getPath());
m_readers[id] = sptr;
auto archive = make_shared<zim::Archive>(book.getPath());
m_archives[id] = archive;
auto reader = make_shared<Reader>(archive);
m_readers[id] = reader;
return reader;
}
std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
{
try {
return m_archives.at(id);
} catch (std::out_of_range& e) {}
auto book = getBookById(id);
if (!book.isPathValid())
return nullptr;
auto sptr = make_shared<zim::Archive>(book.getPath());
m_archives[id] = sptr;
return sptr;
}
@@ -182,61 +208,61 @@ bool Library::writeBookmarksToFile(const std::string& path) const
return writeTextFile(path, dumper.dumpLibXMLBookmark());
}
Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) const
{
AttributeCounts propValueCounts;
for (const auto& pair: m_books) {
const auto& book = pair.second;
if (book.getOrigId().empty()) {
propValueCounts[(book.*p)()] += 1;
}
}
return propValueCounts;
}
std::vector<std::string> Library::getBookPropValueSet(BookStrPropMemFn p) const
{
std::vector<std::string> result;
for ( const auto& kv : getBookAttributeCounts(p) ) {
result.push_back(kv.first);
}
return result;
}
std::vector<std::string> Library::getBooksLanguages() const
{
std::vector<std::string> booksLanguages;
std::map<std::string, bool> booksLanguagesMap;
return getBookPropValueSet(&Book::getLanguage);
}
for (auto& pair: m_books) {
auto& book = pair.second;
auto& language = book.getLanguage();
if (booksLanguagesMap.find(language) == booksLanguagesMap.end()) {
if (book.getOrigId().empty()) {
booksLanguagesMap[language] = true;
booksLanguages.push_back(language);
}
Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
{
return getBookAttributeCounts(&Book::getLanguage);
}
std::vector<std::string> Library::getBooksCategories() const
{
std::set<std::string> categories;
for (const auto& pair: m_books) {
const auto& book = pair.second;
const auto& c = book.getCategory();
if ( !c.empty() ) {
categories.insert(c);
}
}
return booksLanguages;
return std::vector<std::string>(categories.begin(), categories.end());
}
std::vector<std::string> Library::getBooksCreators() const
{
std::vector<std::string> booksCreators;
std::map<std::string, bool> booksCreatorsMap;
for (auto& pair: m_books) {
auto& book = pair.second;
auto& creator = book.getCreator();
if (booksCreatorsMap.find(creator) == booksCreatorsMap.end()) {
if (book.getOrigId().empty()) {
booksCreatorsMap[creator] = true;
booksCreators.push_back(creator);
}
}
}
return booksCreators;
return getBookPropValueSet(&Book::getCreator);
}
std::vector<std::string> Library::getBooksPublishers() const
{
std::vector<std::string> booksPublishers;
std::map<std::string, bool> booksPublishersMap;
for (auto& pair:m_books) {
auto& book = pair.second;
auto& publisher = book.getPublisher();
if (booksPublishersMap.find(publisher) == booksPublishersMap.end()) {
if (book.getOrigId().empty()) {
booksPublishersMap[publisher] = true;
booksPublishers.push_back(publisher);
}
}
}
return booksPublishers;
return getBookPropValueSet(&Book::getPublisher);
}
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const

View File

@@ -20,10 +20,10 @@
#include "libxml_dumper.h"
#include "book.h"
#include "tools.h"
#include "tools/base64.h"
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "tools/pathTools.h"
namespace kiwix
{

View File

@@ -19,6 +19,7 @@
#include "manager.h"
#include "tools.h"
#include "tools/pathTools.h"
#include <pugixml.hpp>
@@ -214,8 +215,8 @@ bool Manager::readBookFromPath(const std::string& path, kiwix::Book* book)
tmp_path = computeAbsolutePath(getCurrentDirectory(), path);
}
try {
kiwix::Reader reader(tmp_path);
book->update(reader);
zim::Archive archive(tmp_path);
book->update(archive);
book->setPathValid(true);
} catch (const std::exception& e) {
book->setPathValid(false);

View File

@@ -19,13 +19,16 @@ kiwix_sources = [
'tools/stringTools.cpp',
'tools/networkTools.cpp',
'tools/otherTools.cpp',
'tools/archiveTools.cpp',
'kiwixserve.cpp',
'name_mapper.cpp',
'server/byte_range.cpp',
'server/etag.cpp',
'server/request_context.cpp',
'server/response.cpp',
'server/internalServer.cpp'
'server/internalServer.cpp',
'server/internalServer_catalog_v2.cpp',
'opds_catalog.cpp'
]
kiwix_sources += lib_resources

74
src/opds_catalog.cpp Normal file
View File

@@ -0,0 +1,74 @@
/*
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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 Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#include "opds_catalog.h"
#include "tools/stringTools.h"
#include <sstream>
namespace kiwix
{
namespace
{
const char opdsSearchEndpoint[] = "/catalog/v2/entries";
enum Separator { AMP };
std::ostringstream& operator<<(std::ostringstream& oss, Separator sep)
{
if ( oss.tellp() > 0 )
oss << "&";
return oss;
}
std::string buildSearchString(const Filter& f)
{
std::ostringstream oss;
if ( f.hasQuery() )
oss << AMP << "q=" << urlEncode(f.getQuery());
if ( f.hasCategory() )
oss << AMP << "category=" << urlEncode(f.getCategory());
if ( f.hasLang() )
oss << AMP << "lang=" << urlEncode(f.getLang());
if ( f.hasName() )
oss << AMP << "name=" << urlEncode(f.getName());
if ( !f.getAcceptTags().empty() )
oss << AMP << "tag=" << urlEncode(join(f.getAcceptTags(), ";"));
return oss.str();
}
} // unnamed namespace
std::string getSearchUrl(const Filter& f)
{
const std::string searchString = buildSearchString(f);
if ( searchString.empty() )
return opdsSearchEndpoint;
else
return opdsSearchEndpoint + ("?" + searchString);
}
} // namespace kiwix

View File

@@ -20,11 +20,16 @@
#include "opds_dumper.h"
#include "book.h"
#include "kiwixlib-resources.h"
#include <mustache.hpp>
#include <unicode/locid.h>
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include <iomanip>
namespace kiwix
{
/* Constructor */
OPDSDumper::OPDSDumper(Library* library)
: library(library)
@@ -35,121 +40,189 @@ OPDSDumper::~OPDSDumper()
{
}
std::string gen_date_str()
{
auto now = time(0);
auto tm = localtime(&now);
std::stringstream is;
is << std::setw(2) << std::setfill('0')
<< 1900+tm->tm_year << "-"
<< std::setw(2) << std::setfill('0') << tm->tm_mon+1 << "-"
<< std::setw(2) << std::setfill('0') << tm->tm_mday << "T"
<< std::setw(2) << std::setfill('0') << tm->tm_hour << ":"
<< std::setw(2) << std::setfill('0') << tm->tm_min << ":"
<< std::setw(2) << std::setfill('0') << tm->tm_sec << "Z";
return is.str();
}
static std::string gen_date_from_yyyy_mm_dd(const std::string& date)
{
std::stringstream is;
is << date << "T00:00::00Z";
return is.str();
}
void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
{
m_totalResults = totalResults;
m_startIndex = startIndex,
m_count = count;
m_isSearchResult = true;
}
#define ADD_TEXT_ENTRY(node, child, value) (node).append_child((child)).append_child(pugi::node_pcdata).set_value((value).c_str())
pugi::xml_node OPDSDumper::handleBook(Book book, pugi::xml_node root_node) {
auto entry_node = root_node.append_child("entry");
ADD_TEXT_ENTRY(entry_node, "id", "urn:uuid:"+book.getId());
ADD_TEXT_ENTRY(entry_node, "title", book.getTitle());
ADD_TEXT_ENTRY(entry_node, "summary", book.getDescription());
ADD_TEXT_ENTRY(entry_node, "language", book.getLanguage());
ADD_TEXT_ENTRY(entry_node, "updated", gen_date_from_yyyy_mm_dd(book.getDate()));
ADD_TEXT_ENTRY(entry_node, "name", book.getName());
ADD_TEXT_ENTRY(entry_node, "flavour", book.getFlavour());
ADD_TEXT_ENTRY(entry_node, "category", book.getCategory());
ADD_TEXT_ENTRY(entry_node, "tags", book.getTags());
ADD_TEXT_ENTRY(entry_node, "articleCount", to_string(book.getArticleCount()));
ADD_TEXT_ENTRY(entry_node, "mediaCount", to_string(book.getMediaCount()));
ADD_TEXT_ENTRY(entry_node, "icon", rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath());
auto content_node = entry_node.append_child("link");
content_node.append_attribute("type") = "text/html";
content_node.append_attribute("href") = (rootLocation + "/" + book.getHumanReadableIdFromPath()).c_str();
auto author_node = entry_node.append_child("author");
ADD_TEXT_ENTRY(author_node, "name", book.getCreator());
auto publisher_node = entry_node.append_child("publisher");
ADD_TEXT_ENTRY(publisher_node, "name", book.getPublisher());
if (! book.getUrl().empty()) {
auto acquisition_link = entry_node.append_child("link");
acquisition_link.append_attribute("rel") = "http://opds-spec.org/acquisition/open-access";
acquisition_link.append_attribute("type") = "application/x-zim";
acquisition_link.append_attribute("href") = book.getUrl().c_str();
acquisition_link.append_attribute("length") = to_string(book.getSize()).c_str();
}
if (! book.getFaviconMimeType().empty() ) {
auto image_link = entry_node.append_child("link");
image_link.append_attribute("rel") = "http://opds-spec.org/image/thumbnail";
image_link.append_attribute("type") = book.getFaviconMimeType().c_str();
image_link.append_attribute("href") = (rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath()).c_str();
}
return entry_node;
}
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds)
namespace
{
date = gen_date_str();
pugi::xml_document doc;
auto root_node = doc.append_child("feed");
root_node.append_attribute("xmlns") = "http://www.w3.org/2005/Atom";
root_node.append_attribute("xmlns:opds") = "http://opds-spec.org/2010/catalog";
typedef kainjow::mustache::data MustacheData;
typedef kainjow::mustache::list BooksData;
typedef kainjow::mustache::list IllustrationInfo;
ADD_TEXT_ENTRY(root_node, "id", id);
ADD_TEXT_ENTRY(root_node, "title", title);
ADD_TEXT_ENTRY(root_node, "updated", date);
if (m_isSearchResult) {
ADD_TEXT_ENTRY(root_node, "totalResults", to_string(m_totalResults));
ADD_TEXT_ENTRY(root_node, "startIndex", to_string(m_startIndex));
ADD_TEXT_ENTRY(root_node, "itemsPerPage", to_string(m_count));
}
auto self_link_node = root_node.append_child("link");
self_link_node.append_attribute("rel") = "self";
self_link_node.append_attribute("href") = "";
self_link_node.append_attribute("type") = "application/atom+xml";
if (!searchDescriptionUrl.empty() ) {
auto search_link = root_node.append_child("link");
search_link.append_attribute("rel") = "search";
search_link.append_attribute("type") = "application/opensearchdescription+xml";
search_link.append_attribute("href") = searchDescriptionUrl.c_str();
}
if (library) {
for (auto& bookId: bookIds) {
handleBook(library->getBookById(bookId), root_node);
IllustrationInfo getBookIllustrationInfo(const Book& book)
{
kainjow::mustache::list illustrations;
if ( book.isPathValid() ) {
for ( auto illustration_size : zim::Archive(book.getPath()).getIllustrationSizes() ) {
illustrations.push_back(kainjow::mustache::object{
{"icon_width", to_string(illustration_size)},
{"icon_height", to_string(illustration_size)},
{"icon_scale", "1"},
});
}
}
return illustrations;
}
kainjow::mustache::object getSingleBookData(const Book& book)
{
const MustacheData bookUrl = book.getUrl().empty()
? MustacheData(false)
: MustacheData(book.getUrl());
return kainjow::mustache::object{
{"id", book.getId()},
{"name", book.getName()},
{"title", book.getTitle()},
{"description", book.getDescription()},
{"language", book.getLanguage()},
{"content_id", book.getHumanReadableIdFromPath()},
{"updated", book.getDate() + "T00:00:00Z"},
{"category", book.getCategory()},
{"flavour", book.getFlavour()},
{"tags", book.getTags()},
{"article_count", to_string(book.getArticleCount())},
{"media_count", to_string(book.getMediaCount())},
{"author_name", book.getCreator()},
{"publisher_name", book.getPublisher()},
{"url", bookUrl},
{"size", to_string(book.getSize())},
{"icons", getBookIllustrationInfo(book)},
};
}
std::string getSingleBookEntryXML(const Book& book, bool withXMLHeader, const std::string& endpointRoot, bool partial)
{
auto data = getSingleBookData(book);
data["with_xml_header"] = MustacheData(withXMLHeader);
data["dump_partial_entries"] = MustacheData(partial);
data["endpoint_root"] = endpointRoot;
return render_template(RESOURCE::templates::catalog_v2_entry_xml, data);
}
BooksData getBooksData(const Library* library, const std::vector<std::string>& bookIds, const std::string& endpointRoot, bool partial)
{
BooksData booksData;
for ( const auto& bookId : bookIds ) {
const Book& book = library->getBookById(bookId);
booksData.push_back(kainjow::mustache::object{
{"entry", getSingleBookEntryXML(book, false, endpointRoot, partial)}
});
}
return nodeToString(root_node);
return booksData;
}
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;
};
} // unnamed namespace
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
{
const auto booksData = getBooksData(library, bookIds, "", false);
const kainjow::mustache::object template_data{
{"date", gen_date_str()},
{"root", rootLocation},
{"feed_id", gen_uuid(libraryId + "/catalog/search?"+query)},
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
{"totalResults", to_string(m_totalResults)},
{"startIndex", to_string(m_startIndex)},
{"itemsPerPage", to_string(m_count)},
{"books", booksData }
};
return render_template(RESOURCE::templates::catalog_entries_xml, template_data);
}
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const
{
const auto endpointRoot = rootLocation + "/catalog/v2";
const auto booksData = getBooksData(library, bookIds, endpointRoot, partial);
const char* const endpoint = partial ? "/partial_entries" : "/entries";
const kainjow::mustache::object template_data{
{"date", gen_date_str()},
{"endpoint_root", endpointRoot},
{"feed_id", gen_uuid(libraryId + endpoint + "?" + query)},
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
{"query", query.empty() ? "" : "?" + urlEncode(query)},
{"totalResults", to_string(m_totalResults)},
{"startIndex", to_string(m_startIndex)},
{"itemsPerPage", to_string(m_count)},
{"books", booksData },
{"dump_partial_entries", MustacheData(partial)}
};
return render_template(RESOURCE::templates::catalog_v2_entries_xml, template_data);
}
std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
{
return getSingleBookEntryXML(library->getBookById(bookId), true, "", false);
}
std::string OPDSDumper::categoriesOPDSFeed() const
{
const auto now = gen_date_str();
kainjow::mustache::list categoryData;
for ( const auto& category : library->getBooksCategories() ) {
const auto urlencodedCategoryName = urlEncode(category);
categoryData.push_back(kainjow::mustache::object{
{"name", category},
{"urlencoded_name", urlencodedCategoryName},
{"updated", now},
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
});
}
return render_template(
RESOURCE::templates::catalog_v2_categories_xml,
kainjow::mustache::object{
{"date", now},
{"endpoint_root", rootLocation + "/catalog/v2"},
{"feed_id", gen_uuid(libraryId + "/categories")},
{"categories", categoryData }
}
);
}
std::string OPDSDumper::languagesOPDSFeed() const
{
const auto now = gen_date_str();
kainjow::mustache::list languageData;
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
const std::string languageCode = langAndBookCount.first;
const int bookCount = langAndBookCount.second;
const auto languageSelfName = getLanguageSelfName(languageCode);
languageData.push_back(kainjow::mustache::object{
{"lang_code", languageCode},
{"lang_self_name", languageSelfName},
{"book_count", to_string(bookCount)},
{"updated", now},
{"id", gen_uuid(libraryId + "/languages/" + languageCode)}
});
}
return render_template(
RESOURCE::templates::catalog_v2_languages_xml,
kainjow::mustache::object{
{"date", now},
{"endpoint_root", rootLocation + "/catalog/v2"},
{"feed_id", gen_uuid(libraryId + "/languages")},
{"languages", languageData }
}
);
}
}

View File

@@ -21,48 +21,14 @@
#include <time.h>
#include <zim/search.h>
#include <zim/suggestion.h>
#include <zim/item.h>
#include <zim/error.h>
#include "tools.h"
#include "tools/stringTools.h"
#include "tools/otherTools.h"
inline char hi(char v)
{
char hex[] = "0123456789abcdef";
return hex[(v >> 4) & 0xf];
}
inline char lo(char v)
{
char hex[] = "0123456789abcdef";
return hex[v & 0xf];
}
std::string hexUUID(std::string in)
{
std::ostringstream out;
for (unsigned n = 0; n < 4; ++n) {
out << hi(in[n]) << lo(in[n]);
}
out << '-';
for (unsigned n = 4; n < 6; ++n) {
out << hi(in[n]) << lo(in[n]);
}
out << '-';
for (unsigned n = 6; n < 8; ++n) {
out << hi(in[n]) << lo(in[n]);
}
out << '-';
for (unsigned n = 8; n < 10; ++n) {
out << hi(in[n]) << lo(in[n]);
}
out << '-';
for (unsigned n = 10; n < 16; ++n) {
out << hi(in[n]) << lo(in[n]);
}
std::string op = out.str();
return op;
}
#include "tools/archiveTools.h"
namespace kiwix
{
@@ -86,6 +52,11 @@ Reader::Reader(const string zimFilePath)
srand(time(nullptr));
}
Reader::Reader(const std::shared_ptr<zim::Archive> archive)
: zimArchive(archive),
zimFilePath(archive->getFilename())
{}
#ifndef _WIN32
Reader::Reader(int fd)
: zimArchive(new zim::Archive(fd)),
@@ -111,12 +82,7 @@ zim::Archive* Reader::getZimArchive() const
MimeCounterType Reader::parseCounterMetadata() const
{
try {
auto counterContent = zimArchive->getMetadata("Counter");
return parseMimetypeCounter(counterContent);
} catch (zim::EntryNotFound& e) {
return {};
}
return kiwix::parseArchiveCounter(*zimArchive);
}
/* Get the count of articles which can be indexed/displayed */
@@ -138,19 +104,7 @@ unsigned int Reader::getArticleCount() const
/* Get the count of medias content in the ZIM file */
unsigned int Reader::getMediaCount() const
{
std::map<const std::string, unsigned int> counterMap
= this->parseCounterMetadata();
unsigned int counter = 0;
for (auto &pair:counterMap) {
if (startsWith(pair.first, "image/") ||
startsWith(pair.first, "video/") ||
startsWith(pair.first, "audio/")) {
counter += pair.second;
}
}
return counter;
return kiwix::getArchiveMediaCount(*zimArchive);
}
/* Get the total of all items of a ZIM file, redirects included */
@@ -162,9 +116,7 @@ unsigned int Reader::getGlobalCount() const
/* Return the UID of the ZIM file */
string Reader::getId() const
{
std::ostringstream s;
s << zimArchive->getUuid();
return s.str();
return kiwix::getArchiveId(*zimArchive);
}
Entry Reader::getRandomPage() const
@@ -183,15 +135,7 @@ Entry Reader::getMainPage() const
bool Reader::getFavicon(string& content, string& mimeType) const
{
try {
auto entry = zimArchive->getFaviconEntry();
auto item = entry.getItem(true);
content = item.getData();
mimeType = item.getMimetype();
return true;
} catch(zim::EntryNotFound& e) {};
return false;
return kiwix::getArchiveFavicon(*zimArchive, 48, content, mimeType);
}
string Reader::getZimFilePath() const
@@ -213,47 +157,32 @@ bool Reader::getMetadata(const string& name, string& value) const
string Reader::getName() const
{
METADATA("Name")
return kiwix::getMetaName(*zimArchive);
}
string Reader::getTitle() const
{
string value = zimArchive->getMetadata("Title");
if (value.empty()) {
value = getLastPathElement(zimFilePath);
std::replace(value.begin(), value.end(), '_', ' ');
size_t pos = value.find(".zim");
value = value.substr(0, pos);
}
return value;
return kiwix::getArchiveTitle(*zimArchive);
}
string Reader::getCreator() const
{
METADATA("Creator")
return kiwix::getMetaCreator(*zimArchive);
}
string Reader::getPublisher() const
{
METADATA("Publisher")
return kiwix::getMetaPublisher(*zimArchive);
}
string Reader::getDate() const
{
METADATA("Date")
return kiwix::getMetaDate(*zimArchive);
}
string Reader::getDescription() const
{
string value;
this->getMetadata("Description", value);
/* Mediawiki Collection tends to use the "Subtitle" name */
if (value.empty()) {
this->getMetadata("Subtitle", value);
}
return value;
return kiwix::getMetaDescription(*zimArchive);
}
string Reader::getLongDescription() const
@@ -263,7 +192,7 @@ string Reader::getLongDescription() const
string Reader::getLanguage() const
{
METADATA("Language")
return kiwix::getMetaLanguage(*zimArchive);
}
string Reader::getLicense() const
@@ -273,13 +202,7 @@ string Reader::getLicense() const
string Reader::getTags(bool original) const
{
string tags_str;
getMetadata("Tags", tags_str);
if (original) {
return tags_str;
}
auto tags = convertTags(tags_str);
return join(tags, ";");
return kiwix::getMetaTags(*zimArchive, original);
}
@@ -302,7 +225,7 @@ string Reader::getRelation() const
string Reader::getFlavour() const
{
METADATA("Flavour")
return kiwix::getMetaFlavour(*zimArchive);
}
string Reader::getSource() const
@@ -316,39 +239,10 @@ string Reader::getScraper() const
}
#undef METADATA
string Reader::getOrigId() const
{
string value;
this->getMetadata("startfileuid", value);
if (value.empty()) {
return "";
}
std::string id = value;
std::string origID;
std::string temp = "";
unsigned int k = 0;
char tempArray[16] = "";
for (unsigned int i = 0; i < id.size(); i++) {
if (id[i] == '\n') {
tempArray[k] = atoi(temp.c_str());
temp = "";
k++;
} else {
temp += id[i];
}
}
origID = hexUUID(tempArray);
return origID;
}
Entry Reader::getEntryFromPath(const std::string& path) const
{
if (path.empty() || path == "/") {
return getMainPage();
}
try {
return zimArchive->getEntryByPath(path);
return kiwix::getEntryFromPath(*zimArchive, path);
} catch (zim::EntryNotFound& e) {
throw NoEntry();
}
@@ -432,12 +326,12 @@ bool Reader::searchSuggestions(const string& prefix,
article is already in the suggestions list (with an other
title) */
bool insert = true;
std::vector<std::vector<std::string>>::iterator suggestionItr;
std::vector<SuggestionItem>::iterator suggestionItr;
for (suggestionItr = results.begin();
suggestionItr != results.end();
suggestionItr++) {
int result = normalizedArticleTitle.compare((*suggestionItr)[2]);
if (result == 0 && articleFinalUrl.compare((*suggestionItr)[1]) == 0) {
int result = normalizedArticleTitle.compare((*suggestionItr).getNormalizedTitle());
if (result == 0 && articleFinalUrl.compare((*suggestionItr).getPath()) == 0) {
insert = false;
break;
} else if (result < 0) {
@@ -447,10 +341,7 @@ bool Reader::searchSuggestions(const string& prefix,
/* Insert if possible */
if (insert) {
std::vector<std::string> suggestion;
suggestion.push_back(entry.getTitle());
suggestion.push_back(articleFinalUrl);
suggestion.push_back(normalizedArticleTitle);
SuggestionItem suggestion(entry.getTitle(), normalizedArticleTitle, articleFinalUrl);
results.insert(suggestionItr, suggestion);
}
@@ -464,12 +355,7 @@ bool Reader::searchSuggestions(const string& prefix,
std::vector<std::string> Reader::getTitleVariants(
const std::string& title) const
{
std::vector<std::string> variants;
variants.push_back(title);
variants.push_back(kiwix::ucFirst(title));
variants.push_back(kiwix::lcFirst(title));
variants.push_back(kiwix::toTitle(title));
return variants;
return kiwix::getTitleVariants(title);
}
@@ -492,35 +378,36 @@ bool Reader::searchSuggestionsSmart(const string& prefix,
SuggestionsList_t& results)
{
std::vector<std::string> variants = this->getTitleVariants(prefix);
bool retVal = false;
/* Try to search in the title using fulltext search database */
auto suggestionSearch = zim::Search(*zimArchive);
suggestionSearch.set_query(prefix);
suggestionSearch.set_range(0, suggestionsCount);
suggestionSearch.set_suggestion_mode(true);
auto suggestionSearcher = zim::SuggestionSearcher(*zimArchive);
if (suggestionSearch.get_matches_estimated()) {
for (auto current = suggestionSearch.begin();
current != suggestionSearch.end();
current++) {
std::vector<std::string> suggestion;
suggestion.push_back(current->getTitle());
suggestion.push_back(current->getPath());
suggestion.push_back(kiwix::normalize(current->getTitle()));
if (zimArchive->hasTitleIndex()) {
auto suggestionSearch = suggestionSearcher.suggest(prefix);
const auto suggestions = suggestionSearch.getResults(0, suggestionsCount);
for (auto current : suggestions) {
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
current.getPath(), current.getSnippet());
results.push_back(suggestion);
}
retVal = true;
} else {
// Check some of the variants of the prefix
for (std::vector<std::string>::iterator variantsItr = variants.begin();
variantsItr != variants.end();
variantsItr++) {
retVal = this->searchSuggestions(*variantsItr, suggestionsCount, results)
|| retVal;
auto suggestionSearch = suggestionSearcher.suggest(*variantsItr);
for (auto current : suggestionSearch.getResults(0, suggestionsCount)) {
if (results.size() >= suggestionsCount) {
break;
}
SuggestionItem suggestion(current.getTitle(), kiwix::normalize(current.getTitle()),
current.getPath(), current.getSnippet());
results.push_back(suggestion);
}
}
}
return retVal;
return results.size() > 0;
}
/* Get next suggestion */
@@ -528,7 +415,7 @@ bool Reader::getNextSuggestion(string& title)
{
if (this->suggestionsOffset != this->suggestions.end()) {
/* title */
title = (*(this->suggestionsOffset))[0];
title = (*(this->suggestionsOffset)).getTitle();
/* increment the cursor for the next call */
this->suggestionsOffset++;
@@ -543,8 +430,8 @@ bool Reader::getNextSuggestion(string& title, string& url)
{
if (this->suggestionsOffset != this->suggestions.end()) {
/* title */
title = (*(this->suggestionsOffset))[0];
url = (*(this->suggestionsOffset))[1];
title = (*(this->suggestionsOffset)).getTitle();
url = (*(this->suggestionsOffset)).getPath();
/* increment the cursor for the next call */
this->suggestionsOffset++;
@@ -579,7 +466,7 @@ bool Reader::isCorrupted() const
/* Return the file size, works also for splitted files */
unsigned int Reader::getFileSize() const
{
return zimArchive->getFilesize() / 1024;
return kiwix::getArchiveFileSize(*zimArchive);
}
}

View File

@@ -30,17 +30,29 @@
#include <mustache.hpp>
#include "kiwixlib-resources.h"
#include "tools/stringTools.h"
namespace kiwix
{
/* Constructor */
SearchRenderer::SearchRenderer(Searcher* searcher, NameMapper* mapper)
: mp_searcher(searcher),
: m_srs(searcher->getSearchResultSet()),
mp_nameMapper(mapper),
protocolPrefix("zim://"),
searchProtocolPrefix("search://?")
searchProtocolPrefix("search://?"),
estimatedResultCount(searcher->getEstimatedResultCount()),
resultStart(searcher->getResultStart())
{}
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
unsigned int start, unsigned int estimatedResultCount)
: m_srs(srs),
mp_nameMapper(mapper),
protocolPrefix("zim://"),
searchProtocolPrefix("search://?"),
estimatedResultCount(estimatedResultCount),
resultStart(start)
{}
/* Destructor */
@@ -70,31 +82,26 @@ std::string SearchRenderer::getHtml()
{
kainjow::mustache::data results{kainjow::mustache::data::type::list};
mp_searcher->restart_search();
Result* p_result = NULL;
while ((p_result = mp_searcher->getNextResult())) {
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
kainjow::mustache::data result;
result.set("title", p_result->get_title());
result.set("url", p_result->get_url());
result.set("snippet", p_result->get_snippet());
auto readerIndex = p_result->get_readerIndex();
auto reader = mp_searcher->get_reader(readerIndex);
result.set("resultContentId", mp_nameMapper->getNameForId(reader->getId()));
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()));
if (p_result->get_wordCount() >= 0) {
result.set("wordCount", kiwix::beautifyInteger(p_result->get_wordCount()));
if (it.getWordCount() >= 0) {
result.set("wordCount", kiwix::beautifyInteger(it.getWordCount()));
}
results.push_back(result);
delete p_result;
}
// pages
kainjow::mustache::data pages{kainjow::mustache::data::type::list};
auto resultStart = mp_searcher->getResultStart();
auto resultEnd = 0U;
auto estimatedResultCount = mp_searcher->getEstimatedResultCount();
auto currentPage = 0U;
auto pageStart = 0U;
auto pageEnd = 0U;
@@ -148,4 +155,4 @@ std::string SearchRenderer::getHtml()
return ss.str();
}
}
}

View File

@@ -18,14 +18,16 @@
*/
#include <cmath>
#include "searcher.h"
#include "reader.h"
#include <zim/search.h>
#include <zim/suggestion.h>
#include <mustache.hpp>
#include <cmath>
#include "tools/stringTools.h"
#include "kiwixlib-resources.h"
#define MAX_SEARCH_LEN 140
@@ -35,7 +37,8 @@ namespace kiwix
class _Result : public Result
{
public:
_Result(zim::Search::iterator& iterator);
_Result(zim::SearchResultSet::iterator iterator);
_Result(SuggestionItem suggestionItem);
virtual ~_Result(){};
virtual std::string get_url();
@@ -45,32 +48,38 @@ class _Result : public Result
virtual std::string get_content();
virtual int get_wordCount();
virtual int get_size();
virtual int get_readerIndex();
virtual std::string get_zimId();
private:
zim::Search::iterator iterator;
zim::SearchResultSet::iterator iterator;
SuggestionItem suggestionItem;
bool isSuggestion;
};
struct SearcherInternal {
const zim::Search* _search;
zim::Search::iterator current_iterator;
SearcherInternal() : _search(NULL) {}
~SearcherInternal()
struct SearcherInternal : zim::SearchResultSet {
explicit SearcherInternal(const zim::SearchResultSet& srs)
: zim::SearchResultSet(srs)
, current_iterator(srs.begin())
{
if (_search != NULL) {
delete _search;
}
}
zim::SearchResultSet::iterator current_iterator;
};
struct SuggestionInternal : zim::SuggestionResultSet {
explicit SuggestionInternal(const zim::SuggestionResultSet& srs)
: zim::SuggestionResultSet(srs),
currentIterator(srs.begin()) {}
zim::SuggestionResultSet::iterator currentIterator;
};
/* Constructor */
Searcher::Searcher()
: internal(new SearcherInternal()),
searchPattern(""),
: searchPattern(""),
estimatedResultCount(0),
resultStart(0),
resultEnd(0)
maxResultCount(0)
{
loadICUExternalTables();
}
@@ -78,7 +87,6 @@ Searcher::Searcher()
/* Destructor */
Searcher::~Searcher()
{
delete internal;
}
bool Searcher::add_reader(Reader* reader)
@@ -99,7 +107,7 @@ Reader* Searcher::get_reader(int readerIndex)
/* Search strings in the database */
void Searcher::search(const std::string& search,
unsigned int resultStart,
unsigned int resultEnd,
unsigned int maxResultCount,
const bool verbose)
{
this->reset();
@@ -110,9 +118,9 @@ void Searcher::search(const std::string& search,
this->searchPattern = search;
this->resultStart = resultStart;
this->resultEnd = resultEnd;
this->maxResultCount = maxResultCount;
/* Try to find results */
if (resultStart != resultEnd) {
if (maxResultCount != 0) {
/* Perform the search */
string unaccentedSearch = removeAccents(search);
std::vector<zim::Archive> archives;
@@ -122,13 +130,13 @@ void Searcher::search(const std::string& search,
archives.push_back(*(*current)->getZimArchive());
}
}
zim::Search* search = new zim::Search(archives);
search->set_verbose(verbose);
search->set_query(unaccentedSearch);
search->set_range(resultStart, resultEnd);
internal->_search = search;
internal->current_iterator = internal->_search->begin();
this->estimatedResultCount = internal->_search->get_matches_estimated();
zim::Searcher searcher(archives);
searcher.setVerbose(verbose);
zim::Query query;
query.setQuery(unaccentedSearch);
zim::Search search = searcher.search(query);
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
this->estimatedResultCount = search.getEstimatedMatches();
}
return;
@@ -137,7 +145,7 @@ void Searcher::search(const std::string& search,
void Searcher::geo_search(float latitude, float longitude, float distance,
unsigned int resultStart,
unsigned int resultEnd,
unsigned int maxResultCount,
const bool verbose)
{
this->reset();
@@ -151,10 +159,10 @@ void Searcher::geo_search(float latitude, float longitude, float distance,
oss << "Articles located less than " << distance << " meters of " << latitude << ";" << longitude;
this->searchPattern = oss.str();
this->resultStart = resultStart;
this->resultEnd = resultEnd;
this->maxResultCount = maxResultCount;
/* Try to find results */
if (resultStart == resultEnd) {
if (maxResultCount == 0) {
return;
}
@@ -163,31 +171,41 @@ void Searcher::geo_search(float latitude, float longitude, float distance,
current++) {
archives.push_back(*(*current)->getZimArchive());
}
zim::Search* search = new zim::Search(archives);
search->set_verbose(verbose);
search->set_query("");
search->set_georange(latitude, longitude, distance);
search->set_range(resultStart, resultEnd);
internal->_search = search;
internal->current_iterator = internal->_search->begin();
this->estimatedResultCount = internal->_search->get_matches_estimated();
zim::Searcher searcher(archives);
searcher.setVerbose(verbose);
zim::Query query;
query.setQuery("");
query.setGeorange(latitude, longitude, distance);
zim::Search search = searcher.search(query);
internal.reset(new SearcherInternal(search.getResults(resultStart, maxResultCount)));
this->estimatedResultCount = search.getEstimatedMatches();
}
void Searcher::restart_search()
{
if (internal->_search) {
internal->current_iterator = internal->_search->begin();
if (internal.get()) {
internal->current_iterator = internal->begin();
}
}
Result* Searcher::getNextResult()
{
if (internal->_search &&
internal->current_iterator != internal->_search->end()) {
if (internal.get() && internal->current_iterator != internal->end()) {
Result* result = new _Result(internal->current_iterator);
internal->current_iterator++;
return result;
} else if (suggestionInternal.get() &&
suggestionInternal->currentIterator != suggestionInternal->end()) {
SuggestionItem item(
suggestionInternal->currentIterator->getTitle(),
normalize(suggestionInternal->currentIterator->getTitle()),
suggestionInternal->currentIterator->getPath(),
suggestionInternal->currentIterator->getSnippet()
);
Result* result = new _Result(item);
suggestionInternal->currentIterator++;
return result;
}
return NULL;
}
@@ -210,22 +228,16 @@ void Searcher::suggestions(std::string& searchPattern, const bool verbose)
this->searchPattern = searchPattern;
this->resultStart = 0;
this->resultEnd = 10;
this->maxResultCount = 10;
string unaccentedSearch = removeAccents(searchPattern);
std::vector<zim::Archive> archives;
for (auto current = this->readers.begin(); current != this->readers.end();
current++) {
archives.push_back(*(*current)->getZimArchive());
}
zim::Search* search = new zim::Search(archives);
search->set_verbose(verbose);
search->set_query(unaccentedSearch);
search->set_range(resultStart, resultEnd);
search->set_suggestion_mode(true);
internal->_search = search;
internal->current_iterator = internal->_search->begin();
this->estimatedResultCount = internal->_search->get_matches_estimated();
// Multizim suggestion is not supported as of now! taking only one archive
zim::Archive archive = *(*this->readers.begin())->getZimArchive();
zim::SuggestionSearcher searcher(archive);
searcher.setVerbose(verbose);
zim::SuggestionSearch search = searcher.suggest(searchPattern);
suggestionInternal.reset(new SuggestionInternal(search.getResults(resultStart, maxResultCount)));
this->estimatedResultCount = search.getEstimatedMatches();
}
/* Return the result count estimation */
@@ -234,42 +246,78 @@ unsigned int Searcher::getEstimatedResultCount()
return this->estimatedResultCount;
}
_Result::_Result(zim::Search::iterator& iterator)
: iterator(iterator)
zim::SearchResultSet Searcher::getSearchResultSet()
{
return *(this->internal);
}
_Result::_Result(zim::SearchResultSet::iterator iterator)
: iterator(iterator),
suggestionItem("", "", ""),
isSuggestion(false)
{}
_Result::_Result(SuggestionItem item)
: iterator(),
suggestionItem(item.getTitle(), item.getNormalizedTitle(), item.getPath(), item.getSnippet()),
isSuggestion(true)
{}
std::string _Result::get_url()
{
return iterator.get_path();
if (isSuggestion) {
return suggestionItem.getPath();
}
return iterator.getPath();
}
std::string _Result::get_title()
{
return iterator.get_title();
if (isSuggestion) {
return suggestionItem.getTitle();
}
return iterator.getTitle();
}
int _Result::get_score()
{
return iterator.get_score();
if (isSuggestion) {
return 0;
}
return iterator.getScore();
}
std::string _Result::get_snippet()
{
return iterator.get_snippet();
if (isSuggestion) {
return suggestionItem.getSnippet();
}
return iterator.getSnippet();
}
std::string _Result::get_content()
{
if (isSuggestion) return "";
return iterator->getItem(true).getData();
}
int _Result::get_size()
{
return iterator.get_size();
if (isSuggestion) {
return 0;
}
return iterator.getSize();
}
int _Result::get_wordCount()
{
return iterator.get_wordCount();
if (isSuggestion) {
return 0;
}
return iterator.getWordCount();
}
int _Result::get_readerIndex()
std::string _Result::get_zimId()
{
return iterator.get_fileIndex();
if (isSuggestion) {
return "";
}
std::ostringstream s;
s << iterator.getZimId();
return s.str();
}

View File

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

View File

@@ -43,10 +43,11 @@ extern "C" {
#include "microhttpd_wrapper.h"
}
#include "tools/otherTools.h"
#include "tools.h"
#include "tools/pathTools.h"
#include "tools/regexTools.h"
#include "tools/stringTools.h"
#include "tools/archiveTools.h"
#include "library.h"
#include "name_mapper.h"
#include "entry.h"
@@ -55,6 +56,11 @@ extern "C" {
#include "opds_dumper.h"
#include <zim/uuid.h>
#include <zim/error.h>
#include <zim/search.h>
#include <zim/suggestion.h>
#include <zim/entry.h>
#include <zim/item.h>
#include <mustache.hpp>
@@ -76,6 +82,31 @@ extern "C" {
namespace kiwix {
namespace
{
inline std::string normalizeRootUrl(std::string rootUrl)
{
while ( !rootUrl.empty() && rootUrl.back() == '/' )
rootUrl.pop_back();
while ( !rootUrl.empty() && rootUrl.front() == '/' )
rootUrl = rootUrl.substr(1);
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
}
unsigned parseIllustration(const std::string& s)
{
int nw(0), nh(0), nEnd(0);
long int w(-1), h(-1);
if ( sscanf(s.c_str(), "Illustration_%n%ldx%n%ld@1%n)", &nw, &w, &nh, &h, &nEnd) == 2
&& nEnd == (int)s.size() && !isspace(s[nw]) && !isspace(s[nh]) && w == h && w >= 0) {
return w;
}
return 0;
}
} // unnamed namespace
static IdNameMapper defaultNameMapper;
static MHD_Result staticHandlerCallback(void* cls,
@@ -97,15 +128,17 @@ InternalServer::InternalServer(Library* library,
bool verbose,
bool withTaskbar,
bool withLibraryButton,
bool blockExternalLinks) :
bool blockExternalLinks,
std::string indexTemplateString) :
m_addr(addr),
m_port(port),
m_root(root),
m_root(normalizeRootUrl(root)),
m_nbThreads(nbThreads),
m_verbose(verbose),
m_withTaskbar(withTaskbar),
m_withLibraryButton(withLibraryButton),
m_blockExternalLinks(blockExternalLinks),
m_indexTemplateString(indexTemplateString.empty() ? RESOURCE::templates::index_html : indexTemplateString),
mp_daemon(nullptr),
mp_library(library),
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper)
@@ -153,6 +186,7 @@ bool InternalServer::start() {
}
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
m_server_id = kiwix::to_string(server_start_time.count());
m_library_id = m_server_id;
return true;
}
@@ -237,16 +271,16 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
{
try {
if (! request.is_valid_url())
return Response::build_404(*this, request, "");
return Response::build_404(*this, request, "", "");
const ETag etag = get_matching_if_none_match_etag(request);
if ( etag )
return Response::build_304(*this, etag);
if (kiwix::startsWith(request.get_url(), "/skin/"))
if (startsWith(request.get_url(), "/skin/"))
return handle_skin(request);
if (startsWith(request.get_url(), "/catalog"))
if (startsWith(request.get_url(), "/catalog/"))
return handle_catalog(request);
if (request.get_url() == "/meta")
@@ -281,27 +315,6 @@ MustacheData InternalServer::get_default_data() const
return data;
}
MustacheData InternalServer::homepage_data() const
{
auto data = get_default_data();
MustacheData books{MustacheData::type::list};
for (auto& bookId: mp_library->filter(kiwix::Filter().local(true).valid(true))) {
auto& currentBook = mp_library->getBookById(bookId);
MustacheData book;
book.set("name", mp_nameMapper->getNameForId(bookId));
book.set("title", currentBook.getTitle());
book.set("description", currentBook.getDescription());
book.set("articleCount", beautifyInteger(currentBook.getArticleCount()));
book.set("mediaCount", beautifyInteger(currentBook.getMediaCount()));
books.push_back(book);
}
data.set("books", books);
return data;
}
bool InternalServer::etag_not_needed(const RequestContext& request) const
{
const std::string url = request.get_url();
@@ -325,51 +338,94 @@ InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
{
return ContentResponse::build(*this, RESOURCE::templates::index_html, homepage_data(), "text/html; charset=utf-8", true);
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8", true);
}
/**
* 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 suggestions;
auto searcher = zim::SuggestionSearcher(*archive);
if (archive->hasTitleIndex()) {
auto search = searcher.suggest(queryString);
auto srs = search.getResults(start, suggestionCount);
for (auto it : srs) {
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
it.getPath(), it.getSnippet());
suggestions.push_back(suggestion);
}
} else {
// TODO: This case should be handled by libzim
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 srs = search.getResults(0, suggestionCount);
for (auto it : srs) {
SuggestionItem suggestion(it.getTitle(), kiwix::normalize(it.getTitle()),
it.getPath());
suggestions.push_back(suggestion);
currCount++;
}
}
}
return suggestions;
}
/**
* Archive and Zim handlers end
**/
std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& request)
{
std::string bookName;
std::string bookId;
std::string meta_name;
std::shared_ptr<Reader> reader;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
bookId = mp_nameMapper->getIdForName(bookName);
meta_name = request.get_argument("name");
reader = mp_library->getReaderById(bookId);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range& e) {
return Response::build_404(*this, request, bookName);
return Response::build_404(*this, request, bookName, "");
}
if (reader == nullptr) {
return Response::build_404(*this, request, bookName);
if (archive == nullptr) {
return Response::build_404(*this, request, bookName, "");
}
std::string content;
std::string mimeType = "text";
if (meta_name == "title") {
content = reader->getTitle();
content = getArchiveTitle(*archive);
} else if (meta_name == "description") {
content = reader->getDescription();
content = getMetaDescription(*archive);
} else if (meta_name == "language") {
content = reader->getLanguage();
content = getMetaLanguage(*archive);
} else if (meta_name == "name") {
content = reader->getName();
content = getMetaName(*archive);
} else if (meta_name == "tags") {
content = reader->getTags();
content = getMetaTags(*archive);
} else if (meta_name == "date") {
content = reader->getDate();
content = getMetaDate(*archive);
} else if (meta_name == "creator") {
content = reader->getCreator();
content = getMetaCreator(*archive);
} else if (meta_name == "publisher") {
content = reader->getPublisher();
content = getMetaPublisher(*archive);
} else if (meta_name == "favicon") {
reader->getFavicon(content, mimeType);
getArchiveFavicon(*archive, 48, content, mimeType);
} else if (const unsigned illustrationSize = parseIllustration(meta_name)) {
getArchiveFavicon(*archive, illustrationSize, content, mimeType);
} else {
return Response::build_404(*this, request, bookName);
return Response::build_404(*this, request, bookName, "");
}
auto response = ContentResponse::build(*this, content, mimeType);
@@ -385,51 +441,70 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
std::string content;
std::string mimeType;
unsigned int maxSuggestionCount = 10;
unsigned int suggestionCount = 0;
std::string bookName;
std::string bookId;
std::string term;
std::shared_ptr<Reader> reader;
std::string queryString;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
bookId = mp_nameMapper->getIdForName(bookName);
term = request.get_argument("term");
reader = mp_library->getReaderById(bookId);
queryString = request.get_argument("term");
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request, bookName);
return Response::build_404(*this, request, bookName, "");
}
auto start = 0;
try {
start = request.get_argument<unsigned int>("start");
} catch (const std::exception&) {}
unsigned int count = 10;
try {
count = request.get_argument<unsigned int>("count");
} catch (const std::exception&) {}
if (count == 0) {
count = 10;
}
if (archive == nullptr) {
return Response::build_404(*this, request, bookName, "");
}
if (m_verbose.load()) {
printf("Searching suggestions for: \"%s\"\n", term.c_str());
printf("Searching suggestions for: \"%s\"\n", queryString.c_str());
}
MustacheData results{MustacheData::type::list};
bool first = true;
if (reader != nullptr) {
/* Get the suggestions */
SuggestionsList_t suggestions;
reader->searchSuggestionsSmart(term, maxSuggestionCount, suggestions);
for(auto& suggestion:suggestions) {
MustacheData result;
result.set("label", suggestion[0]);
result.set("value", suggestion[0]);
result.set("kind", "path");
result.set("path", suggestion[1]);
result.set("first", first);
first = false;
results.push_back(result);
suggestionCount++;
/* Get the suggestions */
SuggestionsList_t suggestions = getSuggestions(archive.get(), queryString, start, count);
for(auto& suggestion:suggestions) {
MustacheData result;
result.set("label", suggestion.getTitle());
if (suggestion.hasSnippet()) {
result.set("label", suggestion.getSnippet());
}
result.set("value", suggestion.getTitle());
result.set("kind", "path");
result.set("path", suggestion.getPath());
result.set("first", first);
first = false;
results.push_back(result);
}
/* Propose the fulltext search if possible */
if (reader->hasFulltextIndex()) {
if (archive->hasFulltextIndex()) {
MustacheData result;
result.set("label", "containing '" + term + "'...");
result.set("value", term + " ");
result.set("label", "containing '" + queryString + "'...");
result.set("value", queryString + " ");
result.set("kind", "pattern");
result.set("first", first);
results.push_back(result);
@@ -457,7 +532,7 @@ 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, "");
return Response::build_404(*this, request, "", "");
}
}
@@ -492,30 +567,34 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
} catch(const std::out_of_range&) {}
catch(const std::invalid_argument&) {}
std::shared_ptr<Reader> reader(nullptr);
std::shared_ptr<zim::Archive> archive;
try {
reader = mp_library->getReaderById(bookId);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {}
/* Make the search */
if ( (!reader && !bookName.empty())
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, reader ? reader->getTitle() : "");
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
response->set_code(MHD_HTTP_NOT_FOUND);
return std::move(response);
}
Searcher searcher;
if (reader) {
searcher.add_reader(reader.get());
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 currentReader = mp_library->getReaderById(bookId);
if (currentReader) {
searcher.add_reader(currentReader.get());
auto currentArchive = mp_library->getArchiveById(bookId);
if (currentArchive) {
if (! searcher) {
searcher = std::make_shared<zim::Searcher>(*currentArchive);
} else {
searcher->addArchive(*currentArchive);
}
}
}
}
@@ -536,25 +615,37 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
pageLength = 25;
}
auto end = start + pageLength;
/* Get the results */
try {
zim::Query query;
if (patternString.empty()) {
searcher.geo_search(latitude, longitude, distance,
start, end, m_verbose.load());
// Execute geo-search
if (m_verbose.load()) {
cout << "Performing geo query `" << distance << "&(" << latitude << ";" << longitude << ")'" << endl;
}
query.setQuery("");
query.setGeorange(latitude, longitude, distance);
} else {
searcher.search(patternString,
start, end, m_verbose.load());
// Execute Ft search
if (m_verbose.load()) {
cout << "Performing query `" << patternString << "'" << endl;
}
std::string queryString = removeAccents(patternString);
query.setQuery(queryString);
}
SearchRenderer renderer(&searcher, mp_nameMapper);
zim::Search search = searcher->search(query);
SearchRenderer renderer(search.getResults(start, pageLength), mp_nameMapper, start,
search.getEstimatedMatches());
renderer.setSearchPattern(patternString);
renderer.setSearchContent(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, reader ? reader->getTitle() : "");
response->set_taskbar(bookName, archive ? getArchiveTitle(*archive) : "");
return std::move(response);
} catch (const std::exception& e) {
@@ -571,24 +662,24 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
std::string bookName;
std::string bookId;
std::shared_ptr<Reader> reader;
std::shared_ptr<zim::Archive> archive;
try {
bookName = request.get_argument("content");
bookId = mp_nameMapper->getIdForName(bookName);
reader = mp_library->getReaderById(bookId);
archive = mp_library->getArchiveById(bookId);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request, bookName);
return Response::build_404(*this, request, bookName, "");
}
if (reader == nullptr) {
return Response::build_404(*this, request, bookName);
if (archive == nullptr) {
return Response::build_404(*this, request, bookName, "");
}
try {
auto entry = reader->getRandomPage();
return build_redirect(bookName, entry.getFinalEntry());
} catch(kiwix::NoEntry& e) {
return Response::build_404(*this, request, bookName);
auto entry = archive->getRandomEntry();
return build_redirect(bookName, getFinalItem(*archive, entry));
} catch(zim::EntryNotFound& e) {
return Response::build_404(*this, request, bookName, "");
}
}
@@ -600,7 +691,7 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
} catch (const std::out_of_range& e) {}
if (source.empty())
return Response::build_404(*this, request, "");
return Response::build_404(*this, request, "", "");
auto data = get_default_data();
data.set("source", source);
@@ -619,11 +710,15 @@ 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, "");
return Response::build_404(*this, request, "", "");
}
if (url == "v2") {
return handle_catalog_v2(request);
}
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
return Response::build_404(*this, request, "");
return Response::build_404(*this, request, "", "");
}
if (url == "searchdescription.xml") {
@@ -632,13 +727,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
}
zim::Uuid uuid;
kiwix::OPDSDumper opdsDumper;
kiwix::OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setSearchDescriptionUrl("catalog/searchdescription.xml");
opdsDumper.setLibrary(mp_library);
opdsDumper.setLibraryId(m_library_id);
std::vector<std::string> bookIdsToDump;
if (url == "root.xml") {
opdsDumper.setTitle("All zims");
uuid = zim::Uuid::generate(host);
bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
} else if (url == "search") {
@@ -646,28 +739,24 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
uuid = zim::Uuid::generate();
}
opdsDumper.setId(kiwix::to_string(uuid));
auto response = ContentResponse::build(
*this,
opdsDumper.dumpOPDSFeed(bookIdsToDump),
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
"application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8");
return std::move(response);
}
std::vector<std::string>
InternalServer::search_catalog(const RequestContext& request,
kiwix::OPDSDumper& opdsDumper)
namespace
{
auto filter = kiwix::Filter().valid(true).local(true).remote(true);
string query("<Empty query>");
size_t count(10);
size_t startIndex(0);
Filter get_search_filter(const RequestContext& request)
{
auto filter = kiwix::Filter().valid(true).local(true);
try {
query = request.get_argument("q");
filter.query(query);
filter.query(request.get_argument("q"));
} catch (const std::out_of_range&) {}
try {
filter.maxSize(extractFromString<unsigned long>(request.get_argument("maxsize")));
filter.maxSize(request.get_argument<unsigned long>("maxsize"));
} catch (...) {}
try {
filter.name(request.get_argument("name"));
@@ -678,26 +767,37 @@ InternalServer::search_catalog(const RequestContext& request,
try {
filter.lang(request.get_argument("lang"));
} catch (const std::out_of_range&) {}
try {
count = extractFromString<unsigned long>(request.get_argument("count"));
} catch (...) {}
try {
startIndex = extractFromString<unsigned long>(request.get_argument("start"));
} catch (...) {}
try {
filter.acceptTags(kiwix::split(request.get_argument("tag"), ";"));
} catch (...) {}
try {
filter.rejectTags(kiwix::split(request.get_argument("notag"), ";"));
} catch (...) {}
opdsDumper.setTitle("Search result for " + query);
return filter;
}
template<class T>
std::vector<T> subrange(const std::vector<T>& v, size_t s, size_t n)
{
const size_t e = std::min(v.size(), s+n);
return std::vector<T>(v.begin()+std::min(v.size(), s), v.begin()+e);
}
} // unnamed namespace
std::vector<std::string>
InternalServer::search_catalog(const RequestContext& request,
kiwix::OPDSDumper& opdsDumper)
{
const auto filter = get_search_filter(request);
const std::string q = filter.hasQuery()
? filter.getQuery()
: "<Empty query>";
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
const auto totalResults = bookIdsToDump.size();
const auto s = std::min(startIndex, totalResults);
bookIdsToDump.erase(bookIdsToDump.begin(), bookIdsToDump.begin()+s);
if (count>0 && bookIdsToDump.size() > count) {
bookIdsToDump.resize(count);
}
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);
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
return bookIdsToDump;
}
@@ -725,22 +825,10 @@ std::string searchSuggestionHTML(const std::string& searchURL, const std::string
} // unnamed namespace
std::shared_ptr<Reader>
InternalServer::get_reader(const std::string& bookName) const
{
std::shared_ptr<Reader> reader;
try {
const std::string bookId = mp_nameMapper->getIdForName(bookName);
reader = mp_library->getReaderById(bookId);
} catch (const std::out_of_range& e) {
}
return reader;
}
std::unique_ptr<Response>
InternalServer::build_redirect(const std::string& bookName, const kiwix::Entry& entry) const
InternalServer::build_redirect(const std::string& bookName, const zim::Item& item) const
{
auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(entry.getPath());
auto redirectUrl = m_root + "/" + bookName + "/" + kiwix::urlEncode(item.getPath());
return Response::build_redirect(*this, redirectUrl);
}
@@ -756,12 +844,17 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
if (bookName.empty())
return build_homepage(request);
const std::shared_ptr<Reader> reader = get_reader(bookName);
if (reader == nullptr) {
std::shared_ptr<zim::Archive> archive;
try {
const std::string bookId = mp_nameMapper->getIdForName(bookName);
archive = mp_library->getArchiveById(bookId);
} 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, bookName, details);
return Response::build_404(*this, request, bookName, "", details);
}
auto urlStr = request.get_url().substr(bookName.size()+1);
@@ -770,31 +863,31 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
}
try {
auto entry = reader->getEntryFromPath(urlStr);
auto entry = getEntryFromPath(*archive, urlStr);
if (entry.isRedirect() || urlStr.empty()) {
// If urlStr is empty, we want to mainPage.
// We must do a redirection to the real page.
return build_redirect(bookName, entry.getFinalEntry());
return build_redirect(bookName, getFinalItem(*archive, entry));
}
auto response = ItemResponse::build(*this, request, entry.getZimEntry().getItem());
auto response = ItemResponse::build(*this, request, entry.getItem());
try {
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, reader->getTitle());
dynamic_cast<ContentResponse&>(*response).set_taskbar(bookName, getArchiveTitle(*archive));
} catch (std::bad_cast& e) {}
if (m_verbose.load()) {
printf("Found %s\n", entry.getPath().c_str());
printf("mimeType: %s\n", entry.getMimetype().c_str());
printf("mimeType: %s\n", entry.getItem(true).getMimetype().c_str());
}
return response;
} catch(kiwix::NoEntry& e) {
} catch(zim::EntryNotFound& e) {
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, bookName, details);
return Response::build_404(*this, request, bookName, getArchiveTitle(*archive), details);
}
}

View File

@@ -54,7 +54,8 @@ class InternalServer {
bool verbose,
bool withTaskbar,
bool withLibraryButton,
bool blockExternalLinks);
bool blockExternalLinks,
std::string indexTemplateString);
virtual ~InternalServer() = default;
MHD_Result handlerCallback(struct MHD_Connection* connection,
@@ -69,10 +70,16 @@ class InternalServer {
private: // functions
std::unique_ptr<Response> handle_request(const RequestContext& request);
std::unique_ptr<Response> build_redirect(const std::string& bookName, const kiwix::Entry& entry) const;
std::unique_ptr<Response> build_redirect(const std::string& bookName, const zim::Item& item) const;
std::unique_ptr<Response> build_homepage(const RequestContext& request);
std::unique_ptr<Response> handle_skin(const RequestContext& request);
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
std::unique_ptr<Response> handle_catalog_v2(const RequestContext& request);
std::unique_ptr<Response> handle_catalog_v2_root(const RequestContext& request);
std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request, bool partial);
std::unique_ptr<Response> handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId);
std::unique_ptr<Response> handle_catalog_v2_categories(const RequestContext& request);
std::unique_ptr<Response> handle_catalog_v2_languages(const RequestContext& request);
std::unique_ptr<Response> handle_meta(const RequestContext& request);
std::unique_ptr<Response> handle_search(const RequestContext& request);
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
@@ -84,9 +91,7 @@ class InternalServer {
kiwix::OPDSDumper& opdsDumper);
MustacheData get_default_data() const;
MustacheData homepage_data() const;
std::shared_ptr<Reader> get_reader(const std::string& bookName) const;
bool etag_not_needed(const RequestContext& r) const;
ETag get_matching_if_none_match_etag(const RequestContext& request) const;
@@ -99,12 +104,14 @@ class InternalServer {
bool m_withTaskbar;
bool m_withLibraryButton;
bool m_blockExternalLinks;
std::string m_indexTemplateString;
struct MHD_Daemon* mp_daemon;
Library* mp_library;
NameMapper* mp_nameMapper;
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);

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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 Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#include "internalServer.h"
#include "library.h"
#include "opds_dumper.h"
#include "request_context.h"
#include "response.h"
#include "tools/otherTools.h"
#include "kiwixlib-resources.h"
#include <mustache.hpp>
#include <string>
#include <vector>
namespace kiwix {
std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext& request)
{
if (m_verbose.load()) {
printf("** running handle_catalog_v2");
}
std::string url;
try {
url = request.get_url_part(2);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request, "", "");
}
if (url == "root.xml") {
return handle_catalog_v2_root(request);
} else if (url == "searchdescription.xml") {
const std::string endpoint_root = m_root + "/catalog/v2";
return ContentResponse::build(*this,
RESOURCE::catalog_v2_searchdescription_xml,
kainjow::mustache::object({{"endpoint_root", endpoint_root}}),
"application/opensearchdescription+xml"
);
} else if (url == "entry") {
const std::string entryId = request.get_url_part(3);
return handle_catalog_v2_complete_entry(request, entryId);
} else if (url == "entries") {
return handle_catalog_v2_entries(request, /*partial=*/false);
} else if (url == "partial_entries") {
return handle_catalog_v2_entries(request, /*partial=*/true);
} else if (url == "categories") {
return handle_catalog_v2_categories(request);
} else if (url == "languages") {
return handle_catalog_v2_languages(request);
} else {
return Response::build_404(*this, request, "", "");
}
}
std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestContext& request)
{
return ContentResponse::build(
*this,
RESOURCE::templates::catalog_v2_root_xml,
kainjow::mustache::object{
{"date", gen_date_str()},
{"endpoint_root", m_root + "/catalog/v2"},
{"feed_id", gen_uuid(m_library_id)},
{"all_entries_feed_id", gen_uuid(m_library_id + "/entries")},
{"partial_entries_feed_id", gen_uuid(m_library_id + "/partial_entries")},
{"category_list_feed_id", gen_uuid(m_library_id + "/categories")},
{"language_list_feed_id", gen_uuid(m_library_id + "/languages")}
},
"application/atom+xml;profile=opds-catalog;kind=navigation"
);
}
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
{
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(m_library_id);
const auto bookIds = search_catalog(request, opdsDumper);
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
return ContentResponse::build(
*this,
opdsFeed,
"application/atom+xml;profile=opds-catalog;kind=acquisition"
);
}
std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId)
{
try {
mp_library->getBookById(entryId);
} catch (const std::out_of_range&) {
return Response::build_404(*this, request, "", "");
}
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(m_library_id);
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
return ContentResponse::build(
*this,
opdsFeed,
"application/atom+xml;type=entry;profile=opds-catalog"
);
}
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
{
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(m_library_id);
return ContentResponse::build(
*this,
opdsDumper.categoriesOPDSFeed(),
"application/atom+xml;profile=opds-catalog;kind=navigation"
);
}
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
{
OPDSDumper opdsDumper(mp_library);
opdsDumper.setRootLocation(m_root);
opdsDumper.setLibraryId(m_library_id);
return ContentResponse::build(
*this,
opdsDumper.languagesOPDSFeed(),
"application/atom+xml;profile=opds-catalog;kind=navigation"
);
}
} // namespace kiwix

View File

@@ -183,4 +183,14 @@ std::string RequestContext::get_header(const std::string& name) const {
return headers.at(lcAll(name));
}
std::string RequestContext::get_query() const {
std::string q;
const char* sep = "";
for ( const auto& a : arguments ) {
q += sep + a.first + '=' + a.second;
sep = "&";
}
return q;
}
}

View File

@@ -74,11 +74,21 @@ class RequestContext {
return v;
}
template<class T>
T get_optional_param(const std::string& name, T default_value) const
{
try {
return get_argument<T>(name);
} catch (...) {}
return default_value;
}
RequestMethod get_method() const;
std::string get_url() const;
std::string get_url_part(int part) const;
std::string get_full_url() const;
std::string get_query() const;
ByteRange get_range() const;

View File

@@ -24,6 +24,7 @@
#include "tools/regexTools.h"
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "string.h"
#include <mustache.hpp>
@@ -38,17 +39,6 @@ namespace
{
// some utilities
std::string render_template(const std::string& template_str, kainjow::mustache::data data)
{
kainjow::mustache::mustache tmpl(template_str);
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
data.set("urlencoded", urlencode);
std::stringstream ss;
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
return ss.str();
}
std::string get_mime_type(const zim::Item& item)
{
try {
@@ -93,7 +83,7 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
return response;
}
std::unique_ptr<Response> Response::build_404(const InternalServer& server, const RequestContext& request, const std::string& bookName, const std::string& details)
std::unique_ptr<Response> Response::build_404(const InternalServer& server, const RequestContext& request, const std::string& bookName, const std::string& bookTitle, const std::string& details)
{
MustacheData results;
results.set("url", request.get_full_url());
@@ -101,7 +91,7 @@ std::unique_ptr<Response> Response::build_404(const InternalServer& server, cons
auto response = ContentResponse::build(server, RESOURCE::templates::_404_html, results, "text/html");
response->set_code(MHD_HTTP_NOT_FOUND);
response->set_taskbar(bookName, "");
response->set_taskbar(bookName, bookTitle);
return std::move(response);
}
@@ -200,7 +190,7 @@ void ContentResponse::introduce_taskbar()
kainjow::mustache::data data;
data.set("root", m_root);
data.set("content", m_bookName);
data.set("hascontent", !m_bookName.empty());
data.set("hascontent", (!m_bookName.empty() && !m_bookTitle.empty()));
data.set("title", m_bookTitle);
data.set("withlibrarybutton", m_withLibraryButton);
auto head_content = render_template(RESOURCE::templates::head_taskbar_html, data);
@@ -260,9 +250,9 @@ Response::create_mhd_response(const RequestContext& request)
MHD_Response*
ContentResponse::create_mhd_response(const RequestContext& request)
{
inject_root_link();
if (contentDecorationAllowed()) {
inject_root_link();
if (m_withTaskbar) {
introduce_taskbar();
}

View File

@@ -47,7 +47,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 RequestContext& request, const std::string& bookName, const std::string& details="");
static std::unique_ptr<Response> build_404(const InternalServer& server, const RequestContext& request, 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);

158
src/tools/archiveTools.cpp Normal file
View File

@@ -0,0 +1,158 @@
/*
* Copyright 2021 Maneesh P M <manu.pm55@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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 Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#include "archiveTools.h"
#include "tools.h"
#include "pathTools.h"
#include "otherTools.h"
#include "stringTools.h"
#include <zim/error.h>
#include <zim/item.h>
namespace kiwix
{
std::string getMetadata(const zim::Archive& archive, const std::string& name) {
try {
return archive.getMetadata(name);
} catch (zim::EntryNotFound& e) {
return "";
}
}
std::string getArchiveTitle(const zim::Archive& archive) {
std::string value = getMetadata(archive, "Title");
if (value.empty()) {
value = getLastPathElement(archive.getFilename());
std::replace(value.begin(), value.end(), '_', ' ');
size_t pos = value.find(".zim");
value = value.substr(0, pos);
}
return value;
}
std::string getMetaDescription(const zim::Archive& archive) {
std::string value;
value = getMetadata(archive, "Description");
/* Mediawiki Collection tends to use the "Subtitle" name */
if (value.empty()) {
value = getMetadata(archive, "Subtitle");
}
return value;
}
std::string getMetaTags(const zim::Archive& archive, bool original) {
std::string tags_str = getMetadata(archive, "Tags");
if (original) {
return tags_str;
}
auto tags = convertTags(tags_str);
return join(tags, ";");
}
std::string getMetaLanguage(const zim::Archive& archive) {
return getMetadata(archive, "Language");
}
std::string getMetaName(const zim::Archive& archive) {
return getMetadata(archive, "Name");
}
std::string getMetaDate(const zim::Archive& archive) {
return getMetadata(archive, "Date");
}
std::string getMetaCreator(const zim::Archive& archive) {
return getMetadata(archive, "Creator");
}
std::string getMetaPublisher(const zim::Archive& archive) {
return getMetadata(archive, "Publisher");
}
std::string getMetaFlavour(const zim::Archive& archive) {
return getMetadata(archive, "Flavour");
}
std::string getArchiveId(const zim::Archive& archive) {
return (std::string) archive.getUuid();
}
bool getArchiveFavicon(const zim::Archive& archive, unsigned size,
std::string& content, std::string& mimeType){
try {
auto item = archive.getIllustrationItem(size);
content = item.getData();
mimeType = item.getMimetype();
return true;
} catch(zim::EntryNotFound& e) {};
return false;
}
// should this be in libzim
unsigned int getArchiveMediaCount(const zim::Archive& archive) {
std::map<const std::string, unsigned int> counterMap = parseArchiveCounter(archive);
unsigned int counter = 0;
for (auto &pair:counterMap) {
if (startsWith(pair.first, "image/") ||
startsWith(pair.first, "video/") ||
startsWith(pair.first, "audio/")) {
counter += pair.second;
}
}
return counter;
}
unsigned int getArchiveFileSize(const zim::Archive& archive) {
return archive.getFilesize() / 1024;
}
zim::Item getFinalItem(const zim::Archive& archive, const zim::Entry& entry)
{
return entry.getItem(true);
}
zim::Entry getEntryFromPath(const zim::Archive& archive, const std::string& path)
{
try {
return archive.getEntryByPath(path);
} catch (zim::EntryNotFound& e) {
if (path.empty() || path == "/") {
return archive.getMainEntry();
}
}
throw zim::EntryNotFound("Cannot find entry for non empty path");
}
MimeCounterType parseArchiveCounter(const zim::Archive& archive) {
try {
auto counterContent = archive.getMetadata("Counter");
return parseMimetypeCounter(counterContent);
} catch (zim::EntryNotFound& e) {
return {};
}
}
} // kiwix

59
src/tools/archiveTools.h Normal file
View File

@@ -0,0 +1,59 @@
/*
* Copyright 2021 Maneesh P M <manu.pm55@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 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 Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_ARCHIVETOOLS_H
#define KIWIX_ARCHIVETOOLS_H
#include <zim/archive.h>
#include <tools/otherTools.h>
/**
* This file contains all the functions that would make handling data related to
* an archive easier.
**/
namespace kiwix
{
std::string getMetadata(const zim::Archive& archive, const std::string& name);
std::string getArchiveTitle(const zim::Archive& archive);
std::string getMetaDescription(const zim::Archive& archive);
std::string getMetaTags(const zim::Archive& archive, bool original = false);
std::string getMetaLanguage(const zim::Archive& archive);
std::string getMetaName(const zim::Archive& archive);
std::string getMetaDate(const zim::Archive& archive);
std::string getMetaCreator(const zim::Archive& archive);
std::string getMetaPublisher(const zim::Archive& archive);
std::string getMetaFlavour(const zim::Archive& archive);
std::string getArchiveId(const zim::Archive& archive);
bool getArchiveFavicon(const zim::Archive& archive, unsigned size,
std::string& content, std::string& mimeType);
unsigned int getArchiveMediaCount(const zim::Archive& archive);
unsigned int getArchiveFileSize(const zim::Archive& archive);
zim::Item getFinalItem(const zim::Archive& archive, const zim::Entry& entry);
zim::Entry getEntryFromPath(const zim::Archive& archive, const std::string& path);
MimeCounterType parseArchiveCounter(const zim::Archive& archive);
}
#endif

View File

@@ -17,8 +17,13 @@
* MA 02110-1301, USA.
*/
// Implement function declared in tools.h and tools/otherTools.h
#include "tools.h"
#include "tools/otherTools.h"
#include <algorithm>
#include <iomanip>
#ifdef _WIN32
#include <windows.h>
@@ -32,6 +37,8 @@
#include <sstream>
#include <pugixml.hpp>
#include <zim/uuid.h>
static std::map<std::string, std::string> codeisomapping {
{ "aa", "aar" },
@@ -341,3 +348,35 @@ kiwix::MimeCounterType kiwix::parseMimetypeCounter(const std::string& counterDat
return counters;
}
std::string kiwix::gen_date_str()
{
auto now = std::time(0);
auto tm = std::localtime(&now);
std::stringstream is;
is << std::setw(2) << std::setfill('0')
<< 1900+tm->tm_year << "-"
<< std::setw(2) << std::setfill('0') << tm->tm_mon+1 << "-"
<< std::setw(2) << std::setfill('0') << tm->tm_mday << "T"
<< std::setw(2) << std::setfill('0') << tm->tm_hour << ":"
<< std::setw(2) << std::setfill('0') << tm->tm_min << ":"
<< std::setw(2) << std::setfill('0') << tm->tm_sec << "Z";
return is.str();
}
std::string kiwix::gen_uuid(const std::string& s)
{
return kiwix::to_string(zim::Uuid::generate(s));
}
std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data)
{
kainjow::mustache::mustache tmpl(template_str);
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
data.set("urlencoded", urlencode);
std::stringstream ss;
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
return ss.str();
}

View File

@@ -24,6 +24,7 @@
#include <vector>
#include <map>
#include <zim/zim.h>
#include <mustache.hpp>
namespace pugi {
class xml_node;
@@ -31,9 +32,7 @@ namespace pugi {
namespace kiwix
{
void sleep(unsigned int milliseconds);
std::string nodeToString(const pugi::xml_node& node);
std::string converta2toa3(const std::string& a2code);
/*
* Convert all format tag string to new format
@@ -45,6 +44,11 @@ namespace kiwix
using MimeCounterType = std::map<const std::string, zim::entry_index_type>;
MimeCounterType parseMimetypeCounter(const std::string& counterData);
std::string gen_date_str();
std::string gen_uuid(const std::string& s);
std::string render_template(const std::string& template_str, kainjow::mustache::data data);
}
#endif

View File

@@ -17,7 +17,10 @@
* MA 02110-1301, USA.
*/
// Implement method defined in <kiwix/tools.h> and "tools/pathTools.h"
#include "tools.h"
#include "tools/pathTools.h"
#include <stdexcept>
#ifdef __APPLE__
@@ -59,7 +62,6 @@
#define PATH_MAX 1024
#endif
#ifdef _WIN32
std::string WideToUtf8(const std::wstring& wstr)
{
@@ -78,7 +80,7 @@ std::wstring Utf8ToWide(const std::string& str)
}
#endif
bool isRelativePath(const std::string& path)
bool kiwix::isRelativePath(const std::string& path)
{
#ifdef _WIN32
if (path.size() < 3 ) {
@@ -173,7 +175,7 @@ std::vector<std::string> normalizeParts(std::vector<std::string>& parts, bool ab
return ret;
}
std::string computeRelativePath(const std::string& path, const std::string& absolutePath)
std::string kiwix::computeRelativePath(const std::string& path, const std::string& absolutePath)
{
auto parts = kiwix::split(path, SEPARATOR, false);
auto pathParts = normalizeParts(parts, false);
@@ -198,11 +200,11 @@ std::string computeRelativePath(const std::string& path, const std::string& abso
return ret;
}
std::string computeAbsolutePath(const std::string& path, const std::string& relativePath)
std::string kiwix::computeAbsolutePath(const std::string& path, const std::string& relativePath)
{
std::string absolutePath = path;
if (path.empty()) {
absolutePath = getCurrentDirectory();
absolutePath = kiwix::getCurrentDirectory();
}
auto parts = kiwix::split(absolutePath, SEPARATOR, false);
@@ -215,7 +217,7 @@ std::string computeAbsolutePath(const std::string& path, const std::string& rela
return ret;
}
std::string removeLastPathElement(const std::string& path)
std::string kiwix::removeLastPathElement(const std::string& path)
{
auto parts_ = kiwix::split(path, SEPARATOR, false);
auto parts = normalizeParts(parts_, false);
@@ -226,7 +228,7 @@ std::string removeLastPathElement(const std::string& path)
return ret;
}
std::string appendToDirectory(const std::string& directoryPath, const std::string& filename)
std::string kiwix::appendToDirectory(const std::string& directoryPath, const std::string& filename)
{
std::string newPath = directoryPath;
if (!directoryPath.empty() && directoryPath.back() != SEPARATOR[0]) {
@@ -236,7 +238,7 @@ std::string appendToDirectory(const std::string& directoryPath, const std::strin
return newPath;
}
std::string getLastPathElement(const std::string& path)
std::string kiwix::getLastPathElement(const std::string& path)
{
auto parts_ = kiwix::split(path, SEPARATOR);
auto parts = normalizeParts(parts_, false);
@@ -267,7 +269,7 @@ std::string getFileSizeAsString(const std::string& path)
return convert.str();
}
std::string getFileContent(const std::string& path)
std::string kiwix::getFileContent(const std::string& path)
{
#ifdef _WIN32
auto wpath = Utf8ToWide(path);
@@ -300,7 +302,7 @@ std::string getFileContent(const std::string& path)
return content;
}
bool fileExists(const std::string& path)
bool kiwix::fileExists(const std::string& path)
{
#ifdef _WIN32
return PathFileExistsW(Utf8ToWide(path).c_str());
@@ -339,7 +341,7 @@ std::string makeTmpDirectory()
_wmkdir(ctmp);
return WideToUtf8(ctmp);
#else
char _template_array[] = {"/tmp/kiwix-lib_XXXXXX"};
char _template_array[] = {"/tmp/libkiwix_XXXXXX"};
std::string dir = mkdtemp(_template_array);
return dir;
#endif
@@ -366,7 +368,7 @@ bool copyFile(const std::string& sourcePath, const std::string& destPath)
#endif
}
std::string getExecutablePath(bool realPathOnly)
std::string kiwix::getExecutablePath(bool realPathOnly)
{
if (!realPathOnly) {
char* cAppImage = ::getenv("APPIMAGE");
@@ -420,7 +422,7 @@ bool writeTextFile(const std::string& path, const std::string& content)
return true;
}
std::string getCurrentDirectory()
std::string kiwix::getCurrentDirectory()
{
#ifdef _WIN32
wchar_t* a_cwd = _wgetcwd(NULL, 0);
@@ -434,7 +436,7 @@ std::string getCurrentDirectory()
return ret;
}
std::string getDataDirectory()
std::string kiwix::getDataDirectory()
{
// Try to get the dataDir from the `KIWIX_DATA_DIR` env var
#ifdef _WIN32
@@ -503,7 +505,7 @@ static std::map<std::string, std::string> extMimeTypes = {
};
/* Try to get the mimeType from the file extension */
std::string getMimeTypeForFile(const std::string& filename)
std::string kiwix::getMimeTypeForFile(const std::string& filename)
{
std::string mimeType = "text/plain";
auto pos = filename.find_last_of(".");
@@ -524,4 +526,3 @@ std::string getMimeTypeForFile(const std::string& filename)
return mimeType;
}

View File

@@ -26,23 +26,13 @@
std::string WideToUtf8(const std::wstring& wstr);
std::wstring Utf8ToWide(const std::string& str);
#endif
bool isRelativePath(const std::string& path);
std::string computeAbsolutePath(const std::string& path, const std::string& relativePath);
std::string computeRelativePath(const std::string& path, const std::string& absolutePath);
std::string removeLastPathElement(const std::string& path);
std::string appendToDirectory(const std::string& directoryPath, const std::string& filename);
unsigned int getFileSize(const std::string& path);
std::string getFileSizeAsString(const std::string& path);
std::string getFileContent(const std::string& path);
bool fileExists(const std::string& path);
bool makeDirectory(const std::string& path);
std::string makeTmpDirectory();
bool copyFile(const std::string& sourcePath, const std::string& destPath);
std::string getLastPathElement(const std::string& path);
std::string getExecutablePath(bool realPathOnly = false);
std::string getCurrentDirectory();
std::string getDataDirectory();
bool writeTextFile(const std::string& path, const std::string& content);
std::string getMimeTypeForFile(const std::string& filename);
#endif

View File

@@ -17,9 +17,11 @@
* MA 02110-1301, USA.
*/
#include <tools/stringTools.h>
// Implement function declared in tools.h and tools/stringTools.h
#include "tools.h"
#include "tools/stringTools.h"
#include <tools/pathTools.h>
#include "tools/pathTools.h"
#include <unicode/normlzr.h>
#include <unicode/rep.h>
#include <unicode/translit.h>
@@ -268,7 +270,7 @@ std::string kiwix::urlDecode(const std::string& value, bool component)
/* Split string in a token array */
std::vector<std::string> kiwix::split(const std::string& str,
const std::string& delims,
bool trimEmpty,
bool dropEmpty,
bool keepDelim)
{
std::string::size_type lastPos = 0;
@@ -277,7 +279,7 @@ std::vector<std::string> kiwix::split(const std::string& str,
while( (pos = str.find_first_of(delims, lastPos)) < str.length() )
{
auto token = str.substr(lastPos, pos - lastPos);
if (!trimEmpty || !token.empty()) {
if (!dropEmpty || !token.empty()) {
tokens.push_back(token);
}
if (keepDelim) {
@@ -287,7 +289,7 @@ std::vector<std::string> kiwix::split(const std::string& str,
}
auto token = str.substr(lastPos);
if (!trimEmpty || !token.empty()) {
if (!dropEmpty || !token.empty()) {
tokens.push_back(token);
}
return tokens;
@@ -395,3 +397,11 @@ bool kiwix::startsWith(const std::string& base, const std::string& start)
&& std::equal(start.begin(), start.end(), base.begin());
}
std::vector<std::string> kiwix::getTitleVariants(const std::string& title) {
std::vector<std::string> variants;
variants.push_back(title);
variants.push_back(kiwix::ucFirst(title));
variants.push_back(kiwix::lcFirst(title));
variants.push_back(kiwix::toTitle(title));
return variants;
}

View File

@@ -43,7 +43,6 @@ void loadICUExternalTables();
std::string urlEncode(const std::string& value, bool encodeReserved = false);
std::string urlDecode(const std::string& value, bool component = false);
std::vector<std::string> split(const std::string& str, const std::string& delims, bool trimEmpty = true, bool keepDelim = false);
std::string join(const std::vector<std::string>& list, const std::string& sep);
std::string ucAll(const std::string& word);
@@ -70,5 +69,7 @@ T extractFromString(const std::string& str) {
}
bool startsWith(const std::string& base, const std::string& start);
std::vector<std::string> getTitleVariants(const std::string& title);
} //namespace kiwix
#endif

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Zim catalog search</ShortName>
<Description>Search zim files in the catalog.</Description>
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:k="http://kiwix.org/opensearchextension/1.0"
indexOffset="0"
template="{{endpoint_root}}/entries?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}"/>
</OpenSearchDescription>

View File

@@ -6,5 +6,5 @@
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:k="http://kiwix.org/opensearchextension/1.0"
indexOffset="0"
template="/{{root}}/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&notag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}"/>
template="{{root}}/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&notag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}"/>
</OpenSearchDescription>

View File

@@ -18,9 +18,19 @@ skin/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png
skin/jquery-ui/jquery-ui.theme.min.css
skin/jquery-ui/jquery-ui.min.css
skin/caret.png
skin/bittorrent.png
skin/magnet.png
skin/download.png
skin/hash.png
skin/search-icon.svg
skin/taskbar.js
skin/iso6391To3.js
skin/isotope.pkgd.min.js
skin/index.js
skin/taskbar.css
skin/home.css
skin/fonts/Poppins.ttf
skin/fonts/Roboto.ttf
skin/block_external.js
templates/search_result.html
templates/no_search_result.html
@@ -32,4 +42,11 @@ templates/head_taskbar.html
templates/taskbar_part.html
templates/external_blocker_part.html
templates/captured_external.html
templates/catalog_entries.xml
templates/catalog_v2_root.xml
templates/catalog_v2_entries.xml
templates/catalog_v2_entry.xml
templates/catalog_v2_categories.xml
templates/catalog_v2_languages.xml
opensearchdescription.xml
catalog_v2_searchdescription.xml

BIN
static/skin/bittorrent.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/skin/download.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

View File

Binary file not shown.

View File

Binary file not shown.

BIN
static/skin/hash.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

456
static/skin/home.css Normal file
View File

@@ -0,0 +1,456 @@
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
font-size: 62.5%;
}
body {
position: relative;
box-sizing: border-box;
}
::selection {
background-color: #00b4e4;
color: white;
}
.kiwixNav {
background-color: #f4f6f8;
width: 100%;
padding: 20px 11vw 25px;
height: 140px;
}
.kiwixHomeBody__results {
font-size: 1.6rem;
color: #333333;
font-family: poppins;
margin: 10px 0 -5px;
position: relative;
top: -10px;
left: 13px;
}
.kiwixNav__filters {
display: grid;
grid-gap: 20px;
grid-template-columns: 231px 231px;
margin: 10px 0;
}
.kiwixNav__kiwixFilter {
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
outline: 0;
box-shadow: none;
border: 0 !important;
background: white;
background-image: none;
border-radius: 1px;
width: 195px;
height: 35px;
flex: 1;
color: black;
padding: 7px 10px 10px;
cursor: pointer;
}
.kiwixNav__kiwixFilter:-ms-expand {
display: none;
}
.kiwixNav__select {
position: relative;
display: flex;
width: 231px;
height: 35px;
line-height: 3;
background: #909090;
overflow: hidden;
border-radius: 10px;
border: 1px solid #b5b2b2;
}
.kiwixNav__select::after {
content: '\25BC';
color: #fff;
background-color: #909090;
height: 100%;
position: absolute;
top: 0;
right: 0;
padding: 0 1em;
cursor: pointer;
pointer-events: none;
-webkit-transition: .25s all ease;
-o-transition: .25s all ease;
transition: .25s all ease;
}
.kiwixNav__kiwixFilter option {
background-color: #fff;
color: #000;
}
.kiwixSearch {
height: 35px;
width: 229px;
border-radius: 10px;
border: solid 1px #b5b2b2;
padding: 10px;
background-image: url('./search-icon.svg');
background-repeat: no-repeat;
background-position: right center;
background-origin: content-box;
background-size: 15px;
}
.searchButton {
margin: 0 17px;
height: 35px;
width: 100px;
border-radius: 10px;
color: white;
background-color: #909090;
border: solid 1px #b5b2b2;
}
.searchButton:hover {
background-color: #858585;
}
.kiwixHomeBody {
padding: 20px 15vw;
min-height: calc(100vh - 170px);
position: relative;
}
.book__list {
position: relative;
}
.book {
border-color: #acacac;
}
.book__wrapper {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
display: grid;
grid-template-columns: 65px 1fr;
grid-template-rows: 70px 120px 1fr 1fr;
grid-gap: 5px;
transition: transform 0.25s;
}
.book__wrapper:hover {
transform: scale(1.07);
}
.book__icon {
display: flex;
align-items: center;
align-content: center;
justify-content: center;
margin: 10px 0 0 10px;
}
.book__icon--image {
max-width: 100%;
max-height: 100%;
}
.book__title {
display: grid;
font-family: poppins;
color: black;
padding: 12px 10px 0 2px;
width: 100%;
height: 100%;
align-items: center;
align-content: center;
}
#bookSize {
font-size: 1.1rem;
margin: 3px 0;
}
#bookTitle {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 1.45rem;
}
.book__description {
grid-column: 1 / 3;
margin: 10px 10px 5px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 7;
-webkit-box-orient: vertical;
font-family: roboto;
font-weight: 300;
font-size: 1.2rem;
color: #4d4d4d;
line-height: 1.25;
}
.book__languageTag {
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 2px #acacac;
background-color: #ffffff;
font-weight: bold;
font-family: roboto;
color: black;
height: 25px;
width: 25px;
margin: 10px auto 0 10px;
border-radius: 5px;
font-size: 0.85rem;
}
.book__tags {
display: flex;
text-align: end;
font-size: 1.1rem;
justify-content: flex-end;
color: #909090;
font-family: roboto;
margin-right: 10px;
margin-top: 10px;
overflow: hidden;
}
.book__tags--wrapper {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.book__links {
display: flex;
text-align: end;
justify-content: flex-end;
grid-column: 2 / 3;
font-size: 1.1rem;
margin: 10px 4px;
color: #00b4e4;
}
.book__links > a, .book__links > span {
cursor: pointer;
text-decoration: none;
color: #00b4e4;
position: relative;
padding: 1px 6px 0;
font-family: roboto;
}
.book__links > a:hover, .book__links > span:hover {
background: #00b4e4;
color: white;
}
.modal-wrapper {
position: fixed;
z-index: 100;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
align-content: center;
background-color: rgba(0, 0, 0, 30%);
}
.modal {
color: #444343;
height: 280px;
width: 250px;
margin: 15px;
background-color: #f7f7f7;
border: 1px solid #ececec;
border-radius: 3px;
}
.modal-heading {
background-color: #f0f0f0;
height: 20%;
width: 100%;
border-bottom: 1px solid #ececec;
display: grid;
grid-template-columns: 3fr 1fr;
}
.modal-title {
display: flex;
font-size: 15px;
align-items: center;
padding-left: 20px;
font-family: poppins;
}
.modal-close-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
padding: 20px;
}
.modal-content div {
width: 100%;
height: 40px;
}
.modal-content a {
text-decoration: none;
}
.modal-content a>div {
position: relative;
top: -28px;
left: 50px;
text-decoration: underline;
font-size: 13px;
font-family: poppins;
color: #444343;
}
.modal-content img {
display: inline-block;
margin: 0 10px;
height: 30px;
}
.fadeOut {
position: fixed;
display: none;
bottom: 0;
left: 0;
z-index: 1;
background: linear-gradient(180deg, rgba(232, 232, 232, 0) 0%, #ffffff 100%);
height: 80px;
width: 100%;
}
.noResults {
font-size: 1.6rem;
text-align: center;
}
.noResults > a {
color: #3498db;
}
.loader-spinner {
position: absolute;
top: -50%;
left: 50%;
border: 5px solid #f3f3f3;
border-radius: 50%;
border-top: 5px solid #3498db;
width: 40px;
height: 40px;
margin: auto;
-webkit-animation: spin 1s linear infinite; /* Safari */
animation: spin 1s linear infinite;
margin-top: 35px;
margin-bottom: -35px;
z-index: 2;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loader {
display: none;
position: relative;
height: 70px;
width: 100%;
}
.kiwixfooter {
display: flex;
width: 100%;
justify-content: center;
text-align: center;
font-size: 1.6rem;
height: 30px;
}
.kiwixfooter > a {
width: auto;
}
@media screen and (max-width: 1100px) {
.kiwixHomeBody {
padding: 20px 10vw;
min-height: calc(100vh - 170px);
}
.kiwixNav__filters {
grid-template-columns: repeat(4, 1fr);
}
}
@media screen and (max-width: 590px) {
.kiwixNav {
height: 257px;
}
.kiwixHomeBody {
min-height: calc(100vh - 287px);
}
.kiwixSearch {
margin-top: 11px;
}
.searchButton {
margin: 19px 0;
width: 229px;
}
.kiwixNav__filters {
grid-template-columns: 1fr;
}
}
@media screen and (max-width: 340px) {
.kiwixHomeBody {
padding: 20px 5vw;
}
}

View File

@@ -1,95 +1,415 @@
const root = $( `link[type='root']` ).attr("href");
let viewPortHeight = window.innerHeight;
let isFetching = false;
let isEnd = false;
const params = new URLSearchParams(window.location.search);
const basicConfig = {
'start': 0,
'count': Math.floor(viewPortHeight/100 + 1) * 3
};
(function() {
const root = $(`link[type='root']`).attr('href');
const incrementalLoadingParams = {
start: 0,
count: viewPortToCount()
};
const bookOrderMap = new Map();
const filterCookieName = 'filters';
const oneDayDelta = 86400000;
let loader;
let footer;
let fadeOutDiv;
let iso;
let isFetching = false;
let noResultInjected = false;
let filters = getCookie(filterCookieName);
let params = new URLSearchParams(window.location.search || filters || '');
let timer;
function queryUrlBuilder() {
let url = `${root}/catalog/search?`
Object.keys(basicConfig).forEach((key, idx) => {
url += `${key}=${basicConfig[key]}${idx !== Object.keys(basicConfig).length - 1 ? '&' : ''}`;
});
return url + (params.toString() ? `&${params.toString()}` : '');
}
function queryUrlBuilder() {
let url = `${root}/catalog/search?`;
url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&");
params.forEach((value, key) => {url+= value ? `&${key}=${value}` : ''});
return (url);
}
async function loadAndDisplayBooks(append = false) {
isFetching = true;
await fetch(queryUrlBuilder())
.then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
const books = data.querySelectorAll("entry");
let bookHtml = '';
books.forEach((book) => {
const link = book.querySelector('link').getAttribute('href');
const title = getInnerHtml(book, 'title');
const description = getInnerHtml(book, 'summary');
bookHtml += `<a href='${link}'><div class='book'>
<div class='book__background' style="background-image: url('${getInnerHtml(book, 'icon')}');">
<div class='book__title' title='${title}'>${title}</div>
<div class='book__description' title='${description}'>${description}</div>
<div class='book__info'>${getInnerHtml(book, 'articleCount')} articles, ${getInnerHtml(book, 'mediaCount')} medias</div>
</div>
</div></a>`;
function setCookie(cookieName, cookieValue) {
const date = new Date();
date.setTime(date.getTime() + oneDayDelta);
document.cookie = `${cookieName}=${cookieValue};expires=${date.toUTCString()};sameSite=Strict`;
}
function getCookie(cookieName) {
const name = cookieName + "=";
let result;
decodeURIComponent(document.cookie).split('; ').forEach(val => {
if (val.indexOf(name) === 0) {
result = val.substring(name.length);
}
});
document.querySelector('.book__list').innerHTML = (append ? document.querySelector('.book__list').innerHTML : '') + bookHtml;
return result;
}
const humanFriendlySize = (fileSize) => {
if (fileSize === 0) {
return '';
}
const units = ['bytes', 'kB', 'MB', 'GB', 'TB'];
let quotient = Math.floor(Math.log10(fileSize) / 3);
quotient = quotient < units.length ? quotient : units.length - 1;
fileSize /= (1000 ** quotient);
return `${+fileSize.toFixed(2)} ${units[quotient]}`;
};
const humanFriendlyTitle = (title) => {
title = title.replace(/_/g, ' ');
return htmlEncode(title[0].toUpperCase() + title.slice(1));
}
function htmlEncode(str) {
return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`);
}
function viewPortToCount(){
const zoom = Math.floor((( window.outerWidth - 10 ) / window.innerWidth) * 100);
return Math.floor(window.innerHeight/(3*zoom) + 1)*(window.innerWidth/(2.5*zoom) + 1);
}
function getInnerHtml(node, query) {
return node.querySelector(query).innerHTML;
}
function generateBookHtml(book, sort = false) {
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
let iconUrl;
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
iconUrl = link.getAttribute('href');
}
});
const title = getInnerHtml(book, 'title');
const description = getInnerHtml(book, 'summary');
const id = getInnerHtml(book, 'id');
const language = getInnerHtml(book, 'language');
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)})
.join(' | ').replace(/_/g, ' ');
let downloadLink;
let zimSize = 0;
try {
const downloadBookLink = book.querySelector('link[type="application/x-zim"]')
zimSize = parseInt(downloadBookLink.getAttribute('length'));
downloadLink = downloadBookLink.getAttribute('href').split('.meta4')[0];
} catch {
downloadLink = '';
}
const humanFriendlyZimSize = humanFriendlySize(zimSize);
const divTag = document.createElement('div');
divTag.setAttribute('class', 'book');
divTag.setAttribute('data-id', id);
if (sort) {
divTag.setAttribute('data-idx', bookOrderMap.get(id));
}
divTag.innerHTML = `<div class="book__wrapper"><div class='book__icon' ><img class="book__icon--image" src='${root}${iconUrl}'></div>
<div class='book__title' title='${title}'>
<div id="bookTitle">${title}</div>
${humanFriendlyZimSize ? `<div id='bookSize'>${humanFriendlyZimSize}</div>`: ''}
</div>
<div class='book__description' title='${description}'>${description}</div>
<div class='book__languageTag'>${language.substr(0, 2).toUpperCase()}</div>
<div class='book__tags'><div class="book__tags--wrapper">${tagHtml}</div></div>
<div class='book__links'> <a href="${root}${link}" data-hover="Preview">Preview</a>${downloadLink ? `&nbsp;|&nbsp;<span class="download" data-link=${downloadLink} class="modal-button">Download</span>` : ''} </div></div>`;
return divTag;
}
function toggleFooter(show=false) {
if (show) {
footer.style.display = 'block';
} else {
footer.style.display = 'none';
fadeOutDiv.style.display = 'block';
}
}
function insertModal(button) {
const downloadLink = button.getAttribute('data-link');
button.addEventListener('click', () => {
document.body.insertAdjacentHTML('beforeend', `<div class="modal-wrapper">
<div class="modal">
<div class="modal-heading">
<div class="modal-title">
<div>
Download
</div>
</div>
<div onclick="closeModal()" class="modal-close-button">
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7071 1.70711C14.0976 1.31658 14.0976
0.683417 13.7071 0.292893C13.3166 -0.0976311 12.6834 -0.0976311 12.2929 0.292893L7 5.58579L1.70711
0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417
-0.0976311 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976311 12.6834
-0.0976311 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7
8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166
14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z" fill="black" />
</svg>
</div>
</div>
</div>
<div class="modal-content">
<div class="modal-regular-download">
<a href="${downloadLink}" download>
<img src="../skin/download.png" alt="direct download" />
<div>Direct</div>
</a>
</div>
<div class="modal-regular-download">
<a href="${downloadLink}.sha256" download>
<img src="../skin/hash.png" alt="download hash" />
<div>Sha256 hash</div>
</a>
</div>
<div class="modal-regular-download">
<a href="${downloadLink}.magnet" target="_blank">
<img src="../skin/magnet.png" alt="download magnet" />
<div>Magnet link</div>
</a>
</div>
<div class="modal-regular-download">
<a href="${downloadLink}.torrent" download>
<img src="../skin/bittorrent.png" alt="download torrent" />
<div>Torrent file</div>
</a>
</div>
</div>
</div>
</div>`);
})
}
async function getBookCount(query) {
const url = `${root}/catalog/search?${query}`;
return await fetch(url).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
return parseInt(data.querySelector('totalResults').innerHTML);
});
}
async function loadBooks() {
loader.style.display = 'block';
return await fetch(queryUrlBuilder()).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
const books = data.querySelectorAll('entry');
books.forEach((book, idx) => {
bookOrderMap.set(getInnerHtml(book, 'id'), idx);
});
incrementalLoadingParams.start += books.length;
const results = parseInt(data.querySelector('totalResults').innerHTML)
if (results === bookOrderMap.size) {
incrementalLoadingParams.count = 0;
toggleFooter(true);
} else {
toggleFooter();
}
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
loader.style.display = 'none';
return books;
});
}
async function loadAndDisplayOptions(nodeQuery, query, valueEntryNode) {
await fetch(query).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
let optionStr = '';
data.querySelectorAll('entry').forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
optionStr += `<option value='${value}'>${humanFriendlyTitle(title)}</option>`;
});
document.querySelector(nodeQuery).innerHTML += optionStr;
});
}
function checkAndInjectEmptyMessage() {
const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
if (!bookOrderMap.size) {
if (!noResultInjected) {
noResultInjected = true;
iso.remove(document.getElementsByClassName('book__list')[0].getElementsByTagName('div'));
iso.layout();
setTimeout(() => {
const divTag = document.createElement('div');
divTag.setAttribute('class', 'noResults');
divTag.innerHTML = `No result. Would you like to <a href="/?lang=">reset filter</a>?`;
kiwixHomeBody.append(divTag);
kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center');
divTag.getElementsByTagName('a')[0].onclick = (event) => {
event.preventDefault();
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?lang=`);
setCookie(filterCookieName, 'lang=');
resetAndFilter();
document.querySelectorAll('.filter').forEach(filter => {
filter.value = params.get(filter.name) || '';
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
})
};
loader.setAttribute('style', 'position: absolute; top: 50%');
}, 300);
}
return true;
} else if (noResultInjected) {
noResultInjected = false;
document.getElementsByClassName('noResults')[0].remove();
kiwixHomeBody.removeAttribute('style');
}
loader.removeAttribute('style');
return false;
}
async function loadAndDisplayBooks(sort = false) {
if (isFetching) return;
isFetching = true;
await loadAndDisplayBooksUnguarded(sort);
isFetching = false;
isEnd = !books.length;
}
async function loadAndDisplayBooksUnguarded(sort) {
let books = await loadBooks();
if (checkAndInjectEmptyMessage()) {return}
const booksToFilter = new Set();
const booksToDelete = new Set();
iso.arrange({
filter: function (idx, elem) {
const id = elem.getAttribute('data-id');
const retVal = bookOrderMap.has(id);
if (retVal) {
booksToFilter.add(id);
if (sort) {
elem.setAttribute('data-idx', bookOrderMap.get(id));
iso.updateSortData(elem);
}
} else {
booksToDelete.add(elem);
}
return retVal;
}
});
books = [...books].filter((book) => {return !booksToFilter.has(getInnerHtml(book, 'id'))});
booksToDelete.forEach(book => {iso.remove(book);});
books.forEach((book) => {
iso.insert(generateBookHtml(book, sort))
const downloadButton = document.querySelector(`[data-id="${getInnerHtml(book, 'id')}"] .download`);
if (downloadButton) {
insertModal(downloadButton);
}
});
}
async function resetAndFilter(filterType = '', filterValue = '') {
isFetching = false;
incrementalLoadingParams.start = 0;
incrementalLoadingParams.count = viewPortToCount();
fadeOutDiv.style.display = 'none';
bookOrderMap.clear();
params = new URLSearchParams(window.location.search);
if (filterType) {
params.set(filterType, filterValue);
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
setCookie(filterCookieName, params.toString());
}
document.querySelectorAll('.filter').forEach(filter => {
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
});
await loadAndDisplayBooks(true);
}
window.addEventListener('popstate', async () => {
await resetAndFilter();
document.querySelectorAll('.filter').forEach(filter => {filter.value = params.get(filter.name) || ''});
});
}
async function loadAndDisplayOptions(nodeQuery, query) {
// currently taking an array in place of query, will replace it with query while fetching data from backend later on.
query.forEach((option) => {
let value = Object.keys(option)[0];
let label = option[value];
document.querySelector(nodeQuery).innerHTML += `<option value='${value}'>${label}</option>`;
})
}
async function filterBooks(filterType, filterValue) {
isEnd = false;
isFetching = false;
basicConfig['start'] = 0;
const params = new URLSearchParams(window.location.search);
if (!filterValue) {
params.delete(filterType);
} else {
params.set(filterType, filterValue);
async function loadSubset() {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
if (incrementalLoadingParams.count) {
loadAndDisplayBooks();
}
else {
fadeOutDiv.style.display = 'none';
}
}
}
window.location.search = params.toString();
loadAndDisplayBooks();
}
function getInnerHtml(node, query) {
return node.querySelector(query).innerHTML;
}
window.addEventListener('resize', (event) => {
if (timer) {clearTimeout(timer)}
timer = setTimeout(() => {
incrementalLoadingParams.count = incrementalLoadingParams.count && viewPortToCount();
loadSubset();
}, 100, event);
});
window.addEventListener('resize', async () => {
if (isFetching || isEnd) return;
viewPortHeight = window.innerHeight;
basicConfig['count'] = Math.floor(viewPortHeight/100 + 1) * 3;
basicConfig['start'] = basicConfig['start'] + basicConfig['count'];
loadAndDisplayBooks(true);
});
window.addEventListener('scroll', loadSubset);
window.addEventListener('scroll', async () => {
if (isFetching || isEnd) return;
if (viewPortHeight + window.scrollY >= document.body.offsetHeight) {
basicConfig['start'] = basicConfig['start'] + basicConfig['count'];
loadAndDisplayBooks(true);
window.onload = async () => {
iso = new Isotope( '.book__list', {
itemSelector: '.book',
getSortData:{
weight: function( itemElem ) {
const index = itemElem.getAttribute('data-idx');
return index ? parseInt(index) : Infinity;
}
},
sortBy: 'weight',
layoutMode: 'cellsByRow',
cellsByRow: {
columnWidth: '.book',
rowHeight: '.book'
}
});
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');
document.querySelectorAll('.filter').forEach(filter => {
filter.addEventListener('change', () => {resetAndFilter(filter.name, filter.value)});
});
if (filters) {
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
}
params.forEach((value, key) => {
const selectBox = document.getElementsByName(key)[0];
if (selectBox) {
selectBox.value = value
}
});
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
if (!window.location.search) {
const browserLang = navigator.language.split('-')[0];
const langFilter = document.getElementById('languageFilter');
const lang = browserLang.length === 3 ? browserLang : iso6391To3[browserLang];
if (await getBookCount(`lang=${lang}`)) {
langFilter.value = lang;
langFilter.dispatchEvent(new Event('change'));
}
}
document.querySelectorAll('.filter').forEach(filter => {
if (filter.value) {
filter.style = 'background-color: #858585; color: #fff';
} else {
filter.style = 'background-color: #ffffff; color: black';
}
});
setCookie(filterCookieName, params.toString());
}
});
window.onload = async (event) => {
loadAndDisplayBooks();
loadAndDisplayOptions('#languageFilter', [{'eng': 'English'}, {'fra': 'french'}, {'ara': 'arab'}, {'hin': 'Hindi'}]);
loadAndDisplayOptions('#categoryFilter', [{'stack_exchange': 'stack exchange'}, {'wikipedia': 'wikipedia'}, {'phet': 'phet'}, {'youtube': 'youtube'}]);
for (const key of params.keys()) {
document.getElementsByName(key)[0].value = params.get(key);
}
}
})();

124
static/skin/iso6391To3.js Normal file
View File

@@ -0,0 +1,124 @@
// eslint-disable-next-line no-unused-vars
const iso6391To3 = {
"aa": "aar",
"af": "afr",
"ak": "aka",
"am": "amh",
"ar": "ara",
"as": "asm",
"az": "aze",
"ba": "bak",
"be": "bel",
"bg": "bul",
"bm": "bam",
"bn": "ben",
"bo": "bod",
"br": "bre",
"bs": "bos",
"ca": "cat",
"ce": "che",
"co": "cos",
"cs": "ces",
"cv": "chv",
"cy": "cym",
"da": "dan",
"de": "deu",
"dz": "dzo",
"ee": "ewe",
"en": "eng",
"es": "spa",
"et": "est",
"eu": "eus",
"fa": "fas",
"ff": "ful",
"fi": "fin",
"fo": "fao",
"fr": "fra",
"ga": "gle",
"gl": "glg",
"gn": "grn",
"gu": "guj",
"gv": "glv",
"ha": "hau",
"he": "heb",
"hi": "hin",
"hr": "hrv",
"hu": "hun",
"hy": "hye",
"id": "ind",
"ig": "ibo",
"is": "isl",
"it": "ita",
"iu": "iku",
"ja": "jpn",
"jv": "jav",
"ka": "kat",
"ki": "kik",
"kk": "kaz",
"km": "khm",
"kn": "kan",
"ko": "kor",
"ks": "kas",
"ku": "kur",
"kw": "cor",
"ky": "kir",
"lb": "ltz",
"lg": "lug",
"ln": "lin",
"lo": "lao",
"lt": "lit",
"lv": "lav",
"mg": "mlg",
"mi": "mri",
"mk": "mkd",
"ml": "mal",
"mn": "mon",
"mr": "mar",
"mt": "mlt",
"my": "mya",
"nl": "nld",
"ny": "nya",
"om": "orm",
"pl": "pol",
"pt": "por",
"qu": "que",
"rm": "roh",
"rn": "run",
"ro": "ron",
"ru": "rus",
"rw": "kin",
"sa": "san",
"sd": "snd",
"sg": "sag",
"si": "sin",
"sk": "slk",
"sl": "slv",
"sn": "sna",
"so": "som",
"sq": "sqi",
"sr": "srp",
"ss": "ssw",
"sv": "swe",
"ta": "tam",
"te": "tel",
"tg": "tgk",
"th": "tha",
"ti": "tir",
"tk": "tuk",
"tn": "tsn",
"tr": "tur",
"ts": "tso",
"tt": "tat",
"ug": "uig",
"uk": "ukr",
"ur": "urd",
"uz": "uzb",
"ve": "ven",
"vi": "vie",
"wa": "wln",
"wo": "wol",
"xh": "xho",
"yo": "yor",
"zh": "zho",
"zu": "zul"
}

78
static/skin/isotope.pkgd.min.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

BIN
static/skin/magnet.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="512.000000" height="512.000000" preserveAspectRatio="xMidYMid meet" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/>
<g class="currentLayer" style=""><title>Layer 1</title><g transform="translate(0,512) scale(0.10000000149011612,-0.10000000149011612) " fill="#484848" stroke="none" id="svg_1" class="selected" fill-opacity="1">
<path d="M1858 5104 c-341 -50 -678 -192 -963 -408 -104 -78 -304 -278 -384 -384 -124 -164 -256 -409 -315 -583 -311 -929 47 -1926 877 -2442 662 -410 1535 -405 2188 14 l76 49 125 -128 c69 -70 352 -364 630 -653 574 -597 548 -575 688 -567 85 5 123 21 176 75 52 52 77 114 77 193 0 98 -24 142 -151 273 -420 434 -1132 1177 -1132 1182 0 3 22 33 49 68 164 209 291 471 362 746 159 614 28 1255 -363 1774 -78 104 -279 305 -386 386 -288 218 -618 355 -974 406 -130 19 -448 18 -580 -1z m502 -545 c308 -38 607 -184 844 -409 476 -455 606 -1146 327 -1745 -128 -273 -356 -519 -626 -674 -387 -222 -857 -259 -1282 -102 -626 231 -1040 879 -983 1540 13 146 33 241 77 371 214 634 830 1060 1493 1032 36 -1 103 -7 150 -13z" id="svg_2" fill="#484848" fill-opacity="1"/>
</g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -7,7 +7,10 @@ const jq = jQuery.noConflict(true);
jq(document).ready(() => {
(function ($) {
const root = $( `link[type='root']` ).attr("href");
const bookName = window.location.pathname.split(`${root}/`)[1].split('/')[0];
const bookName = (window.location.pathname == `${root}/search`)
? (new URLSearchParams(window.location.search)).get('content')
: window.location.pathname.split(`${root}/`)[1].split('/')[0];
$( "#kiwixsearchbox" ).autocomplete({
@@ -31,7 +34,12 @@ jq(document).ready(() => {
$( "#kiwixsearchform" ).submit();
}
},
});
}).data( "ui-autocomplete" )._renderItem = function( ul, item ) {
return $( "<li>" )
.data( "ui-autocomplete-item", item )
.append( item.label )
.appendTo( ul );
};
/* cybook hack */
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
@@ -85,4 +93,4 @@ jq(document).ready(() => {
}
});
})(jq);
})
})

View File

@@ -0,0 +1,12 @@
<feed xmlns="http://www.w3.org/2005/Atom" 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>
{{#filter}}
<totalResults>{{totalResults}}</totalResults>
<startIndex>{{startIndex}}</startIndex>
<itemsPerPage>{{itemsPerPage}}</itemsPerPage>
{{/filter}}
<link rel="self" href="" type="application/atom+xml" />
<link rel="search" type="application/opensearchdescription+xml" href="{{root}}/catalog/searchdescription.xml" />
{{#books}}{{{entry}}}{{/books}}</feed>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2">
<id>{{feed_id}}</id>
<link rel="self"
href="{{endpoint_root}}/categories"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="{{endpoint_root}}/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of categories</title>
<updated>{{date}}</updated>
{{#categories}}
<entry>
<title>{{name}}</title>
<link rel="subsection"
href="{{endpoint_root}}/entries?category={{{urlencoded_name}}}"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>{{updated}}</updated>
<id>{{id}}</id>
<content type="text">All entries with category of '{{name}}'.</content>
</entry>
{{/categories}}
</feed>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2"
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
<id>{{feed_id}}</id>
<link rel="self"
href="{{endpoint_root}}/{{#dump_partial_entries}}partial_{{/dump_partial_entries}}entries{{{query}}}"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<link rel="start"
href="{{endpoint_root}}/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="up"
href="{{endpoint_root}}/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>{{^filter}}All Entries{{/filter}}{{#filter}}Filtered Entries ({{filter}}){{/filter}}</title>
<updated>{{date}}</updated>
{{#filter}}
<totalResults>{{totalResults}}</totalResults>
<startIndex>{{startIndex}}</startIndex>
<itemsPerPage>{{itemsPerPage}}</itemsPerPage>
{{/filter}}
{{#books}}{{{entry}}}{{/books}}</feed>

View File

@@ -0,0 +1,34 @@
{{#with_xml_header}}<?xml version="1.0" encoding="UTF-8"?>
{{/with_xml_header}} <entry>
<id>urn:uuid:{{id}}</id>
<title>{{title}}</title>
<updated>{{updated}}</updated>
{{#dump_partial_entries}}
<link rel="alternate"
href="{{endpoint_root}}/entry/{{{id}}}"
type="application/atom+xml;type=entry;profile=opds-catalog"/>
{{/dump_partial_entries}}{{^dump_partial_entries}} <summary>{{description}}</summary>
<language>{{language}}</language>
<name>{{name}}</name>
<flavour>{{flavour}}</flavour>
<category>{{category}}</category>
<tags>{{tags}}</tags>
<articleCount>{{article_count}}</articleCount>
<mediaCount>{{media_count}}</mediaCount>
{{#icons}}
<link rel="http://opds-spec.org/image/thumbnail"
href="/meta?name=Illustration_{{icon_width}}x{{icon_height}}@{{icon_scale}}&amp;content={{{content_id}}}"
type="image/png;width={{icon_width}};height={{icon_height}};scale={{icon_scale}}"/>
{{/icons}}
<link type="text/html" href="/{{{content_id}}}" />
<author>
<name>{{author_name}}</name>
</author>
<publisher>
<name>{{publisher_name}}</name>
</publisher>
{{#url}}
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
{{/url}}
{{/dump_partial_entries}}
</entry>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
xmlns:opds="https://specs.opds.io/opds-1.2"
xmlns:thr="http://purl.org/syndication/thread/1.0">
<id>{{feed_id}}</id>
<link rel="self"
href="{{endpoint_root}}/languages"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="{{endpoint_root}}/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of languages</title>
<updated>{{date}}</updated>
{{#languages}}
<entry>
<title>{{lang_self_name}}</title>
<dc:language>{{{lang_code}}}</dc:language>
<thr:count>{{book_count}}</thr:count>
<link rel="subsection"
href="{{endpoint_root}}/entries?lang={{{lang_code}}}"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>{{updated}}</updated>
<id>{{id}}</id>
</entry>
{{/languages}}
</feed>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2">
<id>{{feed_id}}</id>
<link rel="self"
href="{{endpoint_root}}/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="{{endpoint_root}}/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="search"
href="{{endpoint_root}}/searchdescription.xml"
type="application/opensearchdescription+xml"/>
<title>OPDS Catalog Root</title>
<updated>{{date}}</updated>
<entry>
<title>All entries</title>
<link rel="subsection"
href="{{endpoint_root}}/entries"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>{{date}}</updated>
<id>{{all_entries_feed_id}}</id>
<content type="text">All entries from this catalog.</content>
</entry>
<entry>
<title>All entries (partial)</title>
<link rel="subsection"
href="{{endpoint_root}}/partial_entries"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>{{date}}</updated>
<id>{{partial_entries_feed_id}}</id>
<content type="text">All entries from this catalog in partial format.</content>
</entry>
<entry>
<title>List of categories</title>
<link rel="subsection"
href="{{endpoint_root}}/categories"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<updated>{{date}}</updated>
<id>{{category_list_feed_id}}</id>
<content type="text">List of all categories in this catalog.</content>
</entry>
<entry>
<title>List of languages</title>
<link rel="subsection"
href="{{endpoint_root}}/languages"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<updated>{{date}}</updated>
<id>{{language_list_feed_id}}</id>
<content type="text">List of all languages in this catalog.</content>
</entry>
</feed>

View File

@@ -1,63 +1,79 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width">
<title>Welcome to Kiwix Server</title>
<script type="text/javascript" src="{{root}}/skin/jquery-ui/external/jquery/jquery.js"></script>
<script type="text/javascript" src="{{root}}/skin/jquery-ui/jquery-ui.min.js"></script>
<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" />
<style>
body {
background:
radial-gradient(#EEEEEE 15%, transparent 16%) 0 0,
radial-gradient(#EEEEEE 15%, transparent 16%) 8px 8px,
radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 0 1px,
radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 8px 9px;
background-color:#E8E8E8;
background-size:16px 16px;
margin-left: auto;
margin-right: auto;
max-width: 1100px;
}
.book__list { text-align: center; }
.book {
display: inline-block; vertical-align: bottom; margin: 8px; padding: 12px 15px; width: 300px;
border: 1px solid #ccc; border-radius: 8px;
text-align: left; color: #000; font-family: sans-serif; font-size: 13px;
background-color:#F1F1F1;
box-shadow: 2px 2px 5px 0px #ccc;
}
.book:hover { background-color: #F9F9F9; box-shadow: none;}
.book__background { background-repeat: no-repeat; background-size: 48px 48px; background-position: top right; }
.book__title {
padding: 0 55px 0 0;overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 18px; color: #0645ad; line-height: 1em;
}
.book__description {
padding: 5px 55px 5px 0px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 15px; line-height: 1em;
}
.book__info { color: #777; font-weight: bold; font-size: 13px; line-height: 1em; }
</style>
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
</head>
<body class="kiwix">
<div class='kiwixHomeNavbar'>
<select name="lang" id="languageFilter" onchange="filterBooks('lang', this.value)">
<option value="" disabled selected>Languages</option>
</select>
<select name="category" id="categoryFilter" onchange="filterBooks('category', this.value)">
<option value="" disabled selected>Categories</option>
</select>
<input type="text" name="q" id="searchFilter" onchange="filterBooks('q', this.value)">
</div>
<div class="kiwix">
<div class='book__list'></div>
</div>
<div id="kiwixfooter">
Powered by <a href="https://kiwix.org">Kiwix</a>
</div>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Kiwix Server</title>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/external/jquery/jquery.js"
></script>
<script
type="text/javascript"
src="{{root}}/skin/jquery-ui/jquery-ui.min.js"
></script>
<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/home.css"
rel="Stylesheet"
/>
<style>
@font-face {
font-family: "poppins";
src: url("{{root}}/skin/fonts/Poppins.ttf") format("truetype");
}
@font-face {
font-family: "roboto";
src: url("{{root}}/skin/fonts/Roboto.ttf") format("truetype");
}
</style>
<script src="{{root}}/skin/isotope.pkgd.min.js" defer></script>
<script src="{{root}}/skin/iso6391To3.js"></script>
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
</head>
<body>
<div class='kiwixNav'>
<div class="kiwixNav__filters">
<div class="kiwixNav__select">
<select name="lang" id="languageFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All languages</option>
</select>
</div>
<div class="kiwixNav__select">
<select name="category" id="categoryFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All categories</option>
</select>
</div>
</div>
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<input type="submit" class="searchButton" value="Search"/>
</form>
</div>
<div class="kiwixHomeBody">
<h3 class="kiwixHomeBody__results"></h3>
<div class="book__list"></div>
<div id="fadeOut" class="fadeOut"></div>
</div>
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
<div id="kiwixfooter" class="kiwixfooter">Powered by&nbsp;<a href="https://kiwix.org">Kiwix</a></div>
</body>
<script>
function closeModal() {
for(modal of document.getElementsByClassName('modal-wrapper')) {
modal.remove();
}
}
</script>
</html>

View File

@@ -8,18 +8,18 @@
<input autocomplete="off" class="ui-autocomplete-input" id="kiwixsearchbox" name="pattern" type="text">
</form>
</div>
{{#hascontent}}
<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">
{{#withlibrarybutton}}
<a id="kiwix_serve_taskbar_library_button" href="{{root}}/"><button>&#x1f3e0;</button></a>
{{/withlibrarybutton}}
{{#hascontent}}
<a id="kiwix_serve_taskbar_home_button" href="{{root}}/{{content}}/"><button>{{title}}</button></a>
<a id="kiwix_serve_taskbar_random_button"
href="{{root}}/random?content={{#urlencoded}}{{{content}}}{{/urlencoded}}"><button>&#x1F3B2;</button></a>
{{/hascontent}}
</div>
{{/hascontent}}
</div>
</span>
</span>

View File

@@ -2,7 +2,7 @@
<book
id="raycharles"
path="./zimfile.zim"
url="https://github.com/kiwix/kiwix-lib/raw/master/test/data/zimfile.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
title="Ray Charles"
description="Wikipedia articles about Ray Charles"
language="eng"
@@ -18,14 +18,14 @@
<book
id="raycharles_uncategorized"
path="./zimfile.zim"
url="https://github.com/kiwix/kiwix-lib/raw/master/test/data/zimfile.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
title="Ray (uncategorized) Charles"
description="No category is assigned to this library entry."
language="eng"
language="rus"
creator="Wikipedia"
publisher="Kiwix"
date="2020-03-31"
name="wikipedia_en_ray_charles"
name="wikipedia_ru_ray_charles"
tags="unittest;wikipedia;_pictures:no;_videos:no;_details:no"
articleCount="284"
mediaCount="2"
@@ -34,14 +34,14 @@
<book
id="charlesray"
path="./zimfile.zim"
url="https://github.com/kiwix/kiwix-lib/raw/master/test/data/zimfile.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
title="Charles, Ray"
description="Wikipedia articles about Ray Charles"
language="eng"
language="fra"
creator="Wikipedia"
publisher="Kiwix"
date="2020-03-31"
name="wikipedia_en_ray_charles"
name="wikipedia_fr_ray_charles"
tags="unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes"
articleCount="284"
mediaCount="2"

View File

File diff suppressed because it is too large Load Diff

View File

@@ -186,7 +186,7 @@ const char sampleLibraryXML[] = R"(
<book
id="raycharles"
path="./zimfile.zim"
url="https://github.com/kiwix/kiwix-lib/raw/master/test/data/zimfile.zim"
url="https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim"
title="Ray Charles"
description="Wikipedia articles about Ray Charles"
language="eng"
@@ -275,9 +275,25 @@ TEST_F(LibraryTest, getBookMarksTest)
TEST_F(LibraryTest, sanityCheck)
{
EXPECT_EQ(lib.getBookCount(true, true), 12U);
EXPECT_EQ(lib.getBooksLanguages().size(), 3U);
EXPECT_EQ(lib.getBooksCreators().size(), 9U);
EXPECT_EQ(lib.getBooksPublishers().size(), 3U);
EXPECT_EQ(lib.getBooksLanguages(),
std::vector<std::string>({"deu", "eng", "fra"})
);
EXPECT_EQ(lib.getBooksCreators(), std::vector<std::string>({
"Islam Stack Exchange",
"Movies & TV Stack Exchange",
"Mythology & Folklore Stack Exchange",
"TED",
"Tania Louis",
"Wiki",
"Wikibooks",
"Wikipedia",
"Wikiquote"
}));
EXPECT_EQ(lib.getBooksPublishers(), std::vector<std::string>({
"",
"Kiwix",
"Kiwix & Some Enthusiasts"
}));
}
TEST_F(LibraryTest, categoryHandling)

View File

@@ -2,6 +2,7 @@
#include "../include/manager.h"
#include "../include/library.h"
#include "../include/book.h"
#include "../include/tools.h"
#include <iostream>
#include <fstream>
@@ -13,13 +14,13 @@ TEST(ManagerTest, addBookFromPathAndGetIdTest)
auto bookId = manager.addBookFromPathAndGetId("./test/example.zim");
ASSERT_NE(bookId, "");
kiwix::Book book = lib.getBookById(bookId);
EXPECT_EQ(book.getPath(), computeAbsolutePath("", "./test/example.zim"));
EXPECT_EQ(book.getPath(), kiwix::computeAbsolutePath("", "./test/example.zim"));
const std::string pathToSave = "./pathToSave";
const std::string url = "url";
bookId = manager.addBookFromPathAndGetId("./test/example.zim", pathToSave, url, true);
book = lib.getBookById(bookId);
auto savedPath = computeAbsolutePath(removeLastPathElement(manager.writableLibraryPath), pathToSave);
auto savedPath = kiwix::computeAbsolutePath(kiwix::removeLastPathElement(manager.writableLibraryPath), pathToSave);
EXPECT_EQ(book.getPath(), savedPath);
EXPECT_EQ(book.getUrl(), url);
}

View File

@@ -8,6 +8,9 @@ tests = [
'kiwixserve',
'book',
'manager',
'opds_catalog',
'reader',
'searcher'
]
if build_machine.system() != 'windows'

85
test/opds_catalog.cpp Normal file
View File

@@ -0,0 +1,85 @@
/*
* Copyright 2021 Veloman Yunkan <veloman.yunkan@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
* NON-INFRINGEMENT. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "../include/opds_catalog.h"
#include "gtest/gtest.h"
using namespace kiwix;
TEST(OpdsCatalog, getSearchUrl)
{
#define EXPECT_SEARCH_URL(url) EXPECT_EQ(url, getSearchUrl(f))
{
Filter f;
EXPECT_SEARCH_URL("/catalog/v2/entries");
}
{
Filter f;
f.query("abc");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc");
}
{
Filter f;
f.query("abc def");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def");
}
{
Filter f;
f.category("ted");
EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted");
}
{
Filter f;
f.lang("eng");
EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng");
}
{
Filter f;
f.name("second");
EXPECT_SEARCH_URL("/catalog/v2/entries?name=second");
}
{
Filter f;
f.acceptTags({"paper", "plastic"});
EXPECT_SEARCH_URL("/catalog/v2/entries?tag=paper;plastic");
}
{
Filter f;
f.query("abc");
f.category("ted");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc&category=ted");
}
{
Filter f;
f.category("ted");
f.query("abc");
EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc&category=ted");
}
{
Filter f;
f.query("peru");
f.category("scifi");
f.lang("html");
f.name("edsonarantesdonascimento");
f.acceptTags({"body", "script"});
EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body;script");
}
#undef EXPECT_SEARCH_URL
}

View File

@@ -20,7 +20,8 @@
#include "gtest/gtest.h"
#include <string>
#include <vector>
#include "../include/tools/pathTools.h"
#include "../include/tools.h"
#include "../src/tools/pathTools.h"
#ifdef _WIN32
# define S "\\"
@@ -101,61 +102,61 @@ TEST(pathTools, normalizePartsRelative)
TEST(pathTools, isRelativePath)
{
ASSERT_TRUE(isRelativePath("foo"));
ASSERT_TRUE(isRelativePath(P2("foo","bar")));
ASSERT_TRUE(isRelativePath(P3(".","foo","bar")));
ASSERT_TRUE(isRelativePath(P2("..","foo")));
ASSERT_TRUE(isRelativePath(P4("foo","","bar","")));
ASSERT_FALSE(isRelativePath(A1("foo")));
ASSERT_FALSE(isRelativePath(A2("foo", "bar")));
ASSERT_TRUE(kiwix::isRelativePath("foo"));
ASSERT_TRUE(kiwix::isRelativePath(P2("foo","bar")));
ASSERT_TRUE(kiwix::isRelativePath(P3(".","foo","bar")));
ASSERT_TRUE(kiwix::isRelativePath(P2("..","foo")));
ASSERT_TRUE(kiwix::isRelativePath(P4("foo","","bar","")));
ASSERT_FALSE(kiwix::isRelativePath(A1("foo")));
ASSERT_FALSE(kiwix::isRelativePath(A2("foo", "bar")));
#ifdef _WIN32
ASSERT_FALSE(isRelativePath(P2(A_SAMBA, "foo")));
ASSERT_FALSE(isRelativePath(P3(A_SAMBA, "foo", "bar")));
ASSERT_FALSE(kiwix::isRelativePath(P2(A_SAMBA, "foo")));
ASSERT_FALSE(kiwix::isRelativePath(P3(A_SAMBA, "foo", "bar")));
#endif
}
TEST(pathTools, computeAbsolutePath)
{
ASSERT_EQ(computeAbsolutePath(A2("a","b"), "foo"),
ASSERT_EQ(kiwix::computeAbsolutePath(A2("a","b"), "foo"),
A3("a","b","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b",""), "foo"),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b",""), "foo"),
A3("a","b","foo"));
ASSERT_EQ(computeAbsolutePath(A2("a","b"), P2(".","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A2("a","b"), P2(".","foo")),
A3("a","b","foo"));
ASSERT_EQ(computeAbsolutePath(A2("a","b"), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A2("a","b"), P2("..","foo")),
A2("a","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b",""), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b",""), P2("..","foo")),
A2("a","foo"));
ASSERT_EQ(computeAbsolutePath(A5("a","b","c","d","e"), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A5("a","b","c","d","e"), P2("..","foo")),
A5("a","b","c","d","foo"));
ASSERT_EQ(computeAbsolutePath(A5("a","b","c","d","e"), P5("..","..","..","g","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A5("a","b","c","d","e"), P5("..","..","..","g","foo")),
A4("a","b","g","foo"));
#ifdef _WIN32
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b",""), P2("..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b",""), P2("..","foo")),
P3(A_SAMBA,"a","foo"));
ASSERT_EQ(computeAbsolutePath(P6(A_SAMBA,"a","b","c","d","e"), P5("..","..","..","g","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P6(A_SAMBA,"a","b","c","d","e"), P5("..","..","..","g","foo")),
P5(A_SAMBA,"a","b","g","foo"));
#endif
}
TEST(pathTools, computeRelativePath)
{
ASSERT_EQ(computeRelativePath(A2("a","b"), A3("a","b","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A2("a","b"), A3("a","b","foo")),
"foo");
ASSERT_EQ(computeRelativePath(A3("a","b",""), A3("a","b","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A3("a","b",""), A3("a","b","foo")),
"foo");
ASSERT_EQ(computeRelativePath(A2("a","b"), A2("a","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A2("a","b"), A2("a","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(A3("a","b",""), A2("a","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A3("a","b",""), A2("a","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(A5("a","b","c","d","e"), A5("a","b","c","d","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A5("a","b","c","d","e"), A5("a","b","c","d","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(A5("a","b","c","d","e"), A4("a","b","g","foo")),
ASSERT_EQ(kiwix::computeRelativePath(A5("a","b","c","d","e"), A4("a","b","g","foo")),
P5("..","..","..","g","foo"));
#ifdef _WIN32
ASSERT_EQ(computeRelativePath(P3(A_SAMBA,"a","b"), P3(A_SAMBA,"a","foo")),
ASSERT_EQ(kiwix::computeRelativePath(P3(A_SAMBA,"a","b"), P3(A_SAMBA,"a","foo")),
P2("..","foo"));
ASSERT_EQ(computeRelativePath(P6(A_SAMBA,"a","b","c","d","e"), P5(A_SAMBA,"a","b","g","foo")),
ASSERT_EQ(kiwix::computeRelativePath(P6(A_SAMBA,"a","b","c","d","e"), P5(A_SAMBA,"a","b","g","foo")),
P5("..","..","..","g","foo"));
#endif
@@ -163,73 +164,73 @@ TEST(pathTools, computeRelativePath)
TEST(pathTools, removeLastPathElement)
{
ASSERT_EQ(removeLastPathElement(P3("a","b","c")),
ASSERT_EQ(kiwix::removeLastPathElement(P3("a","b","c")),
P2("a","b"));
ASSERT_EQ(removeLastPathElement(A3("a","b","c")),
ASSERT_EQ(kiwix::removeLastPathElement(A3("a","b","c")),
A2("a","b"));
ASSERT_EQ(removeLastPathElement(P4("a","b","c","")),
ASSERT_EQ(kiwix::removeLastPathElement(P4("a","b","c","")),
P2("a","b"));
ASSERT_EQ(removeLastPathElement(A4("a","b","c","")),
ASSERT_EQ(kiwix::removeLastPathElement(A4("a","b","c","")),
A2("a","b"));
}
TEST(pathTools, appendToDirectory)
{
ASSERT_EQ(appendToDirectory(P3("a","b","c"), "foo.xml"),
ASSERT_EQ(kiwix::appendToDirectory(P3("a","b","c"), "foo.xml"),
P4("a","b","c","foo.xml"));
ASSERT_EQ(appendToDirectory(P4("a","b","c",""), "foo.xml"),
ASSERT_EQ(kiwix::appendToDirectory(P4("a","b","c",""), "foo.xml"),
P4("a","b","c","foo.xml"));
ASSERT_EQ(appendToDirectory(P3("a","b","c"), P2("d","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P3("a","b","c"), P2("d","foo.xml")),
P5("a","b","c","d","foo.xml"));
ASSERT_EQ(appendToDirectory(P4("a","b","c",""), P2("d","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P4("a","b","c",""), P2("d","foo.xml")),
P5("a","b","c","d","foo.xml"));
ASSERT_EQ(appendToDirectory(P3("a","b","c"), P2(".","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P3("a","b","c"), P2(".","foo.xml")),
P5("a","b","c",".","foo.xml"));
ASSERT_EQ(appendToDirectory(P4("a","b","c",""), P2(".","foo.xml")),
ASSERT_EQ(kiwix::appendToDirectory(P4("a","b","c",""), P2(".","foo.xml")),
P5("a","b","c",".","foo.xml"));
}
TEST(pathTools, goUp)
{
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), ".."),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), ".."),
A2("a", "b"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P2("..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P2("..","..")),
A1("a"));
#ifdef _WIN32
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
"c:");
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
"c:");
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), ".."),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), ".."),
P3(A_SAMBA,"a", "b"));
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P2("..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P2("..","..")),
P2(A_SAMBA,"a"));
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P3("..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P3("..","..","..")),
A_SAMBA);
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","..")),
A_SAMBA);
#else
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P3("..","..","..")),
"/");
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P4("..","..","..","..")),
"/");
#endif
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P2("..", "foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P2("..", "foo")),
A3("a", "b","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P3("..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P3("..","..","foo")),
A2("a","foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P4("..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P4("..","..","..","foo")),
A1("foo"));
ASSERT_EQ(computeAbsolutePath(A3("a","b","c"), P5("..","..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(A3("a","b","c"), P5("..","..","..","..","foo")),
A1("foo"));
#ifdef _WIN32
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P4("..","..","..","foo")),
P2(A_SAMBA,"foo"));
ASSERT_EQ(computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P5("..","..","..","..","foo")),
ASSERT_EQ(kiwix::computeAbsolutePath(P4(A_SAMBA,"a","b","c"), P5("..","..","..","..","foo")),
P2(A_SAMBA,"foo"));
#endif
}
@@ -241,20 +242,20 @@ TEST(pathTools, dirChange)
std::string p1("c:\\a\\b\\c");
std::string p2("d:\\d\\e\\foo.xml");
{
std::string relative_path = computeRelativePath(p1, p2);
std::string relative_path = kiwix::computeRelativePath(p1, p2);
ASSERT_EQ(relative_path, p2);
std::string abs_path = computeAbsolutePath(p1, relative_path);
std::string abs_path = kiwix::computeAbsolutePath(p1, relative_path);
ASSERT_EQ(abs_path, p2);
ASSERT_EQ(computeAbsolutePath(p1, "..\\..\\..\\..\\..\\d:\\d\\e\\foo.xml"), p2);
ASSERT_EQ(kiwix::computeAbsolutePath(p1, "..\\..\\..\\..\\..\\d:\\d\\e\\foo.xml"), p2);
}
std::string ps("\\\\samba\\d\\e\\foo.xml");
{
std::string relative_path = computeRelativePath(p1, ps);
std::string relative_path = kiwix::computeRelativePath(p1, ps);
ASSERT_EQ(relative_path, ps);
std::string abs_path = computeAbsolutePath(p1, relative_path);
std::string abs_path = kiwix::computeAbsolutePath(p1, relative_path);
ASSERT_EQ(abs_path, ps);
// I'm not sure this test is valid on windows :/
// ASSERT_EQ(computeAbsolutePath(p1, "..\\..\\..\\..\\..\\\\samba\\d\\e\\foo.xml"), ps);
// ASSERT_EQ(kiwix::computeAbsolutePath(p1, "..\\..\\..\\..\\..\\\\samba\\d\\e\\foo.xml"), ps);
}
}

62
test/reader.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "gtest/gtest.h"
#include "../include/reader.h"
#include "zim/archive.h"
namespace kiwix
{
/**
* This test file is written primarily to demonstrate how Reader is simply a
* wrapper over an archive. We will be dropping this wrapper soon.
**/
TEST (Reader, archiveWrapper) {
Reader reader("./test/zimfile.zim");
zim::Archive archive = *reader.getZimArchive();
std::ostringstream s;
s << archive.getUuid();
ASSERT_EQ(reader.getId(), s.str());
ASSERT_EQ(reader.getGlobalCount(), archive.getEntryCount());
ASSERT_EQ(reader.getMainPage().getTitle(), archive.getMainEntry().getTitle());
ASSERT_EQ(reader.hasFulltextIndex(), archive.hasFulltextIndex());
ASSERT_NO_THROW(reader.getRandomPage());
}
TEST (Reader, getFunctions) {
zim::Archive archive("./test/zimfile.zim");
Reader reader("./test/zimfile.zim");
auto archiveEntry = archive.getRandomEntry();
ASSERT_TRUE(reader.pathExists(archiveEntry.getPath()));
auto readerEntry = reader.getEntryFromPath(archiveEntry.getPath());
ASSERT_EQ(readerEntry.getTitle(), archiveEntry.getTitle());
ASSERT_FALSE(reader.pathExists("invalidEntryPath"));
ASSERT_THROW(reader.getEntryFromPath("invalidEntryPath"), NoEntry);
readerEntry = reader.getEntryFromTitle(archiveEntry.getTitle());
ASSERT_EQ(readerEntry.getTitle(), archiveEntry.getTitle());
}
TEST (Reader, suggestions) {
Reader reader("./test/zimfile.zim");
SuggestionsList_t suggestions;
reader.searchSuggestionsSmart("The Genius", 4, suggestions);
std::vector<std::string> suggestionResult, expectedResult;
std::string suggestionTitle;
for (auto it = suggestions.begin(); it != suggestions.end(); it++) {
suggestionResult.push_back(it->getTitle());
}
expectedResult = {
"The Genius After Hours",
"The Genius Hits the Road",
"The Genius Sings the Blues",
"The Genius of Ray Charles"
};
ASSERT_EQ(suggestionResult, expectedResult);
}
}

View File

@@ -21,7 +21,7 @@
#include <string>
#include "../include/tools/regexTools.h"
#include "../src/tools/regexTools.h"
namespace
{

67
test/searcher.cpp Normal file
View File

@@ -0,0 +1,67 @@
#include "gtest/gtest.h"
#include "../include/searcher.h"
#include "../include/reader.h"
namespace kiwix
{
TEST(Searcher, search) {
Reader reader("./test/example.zim");
Searcher searcher;
searcher.add_reader(&reader);
ASSERT_EQ(searcher.get_reader(0)->getTitle(), reader.getTitle());
searcher.search("wiki", 0, 2);
searcher.restart_search();
ASSERT_EQ(searcher.getEstimatedResultCount(), (unsigned int)2);
auto result = searcher.getNextResult();
ASSERT_EQ(result->get_title(), "FreedomBox for Communities/Offline Wikipedia - Wikibooks, open books for an open world");
result = searcher.getNextResult();
ASSERT_EQ(result->get_title(), "Wikibooks");
}
TEST(Searcher, suggestion) {
Reader reader("./test/zimfile.zim");
Searcher searcher;
searcher.add_reader(&reader);
ASSERT_EQ(searcher.get_reader(0)->getTitle(), reader.getTitle());
std::string query = "ray";
searcher.suggestions(query, true);
searcher.restart_search();
auto result = searcher.getNextResult();
ASSERT_EQ(result->get_title(), "Charles, Ray");
ASSERT_EQ(result->get_url(), "A/Charles,_Ray");
ASSERT_EQ(result->get_snippet(), "Charles, <b>Ray</b>");
ASSERT_EQ(result->get_score(), 0);
ASSERT_EQ(result->get_content(), "");
ASSERT_EQ(result->get_size(), 0);
ASSERT_EQ(result->get_wordCount(), 0);
ASSERT_EQ(result->get_zimId(), "");
result = searcher.getNextResult();
ASSERT_EQ(result->get_title(), "Ray (film)");
}
TEST(Searcher, incrementalRange) {
// Attempt to get 50 results in steps of 5
zim::Archive archive("./test/zimfile.zim");
zim::Searcher ftsearcher(archive);
zim::Query query;
query.setQuery("ray");
auto search = ftsearcher.search(query);
int suggCount = 0;
for (int i = 0; i < 10; i++) {
auto srs = search.getResults(i*5, 5); // get 5 results
ASSERT_EQ(srs.size(), 5);
suggCount += srs.size();
}
ASSERT_EQ(suggCount, 50);
}
}

View File

@@ -5,7 +5,7 @@
#include "../include/manager.h"
#include "../include/server.h"
#include "../include/name_mapper.h"
#include "../include/tools.h"
using TestContextImpl = std::vector<std::pair<std::string, std::string> >;
struct TestContext : TestContextImpl {
@@ -54,21 +54,21 @@ public: // types
public: // functions
ZimFileServer(int serverPort, std::string libraryFilePath);
ZimFileServer(int serverPort, const FilePathCollection& zimpaths);
ZimFileServer(int serverPort, const FilePathCollection& zimpaths, std::string indexTemplateString = "");
~ZimFileServer();
Response GET(const char* path, const Headers& headers = Headers())
httplib::Result GET(const char* path, const Headers& headers = Headers())
{
return client->Get(path, headers);
}
Response HEAD(const char* path, const Headers& headers = Headers())
httplib::Result HEAD(const char* path, const Headers& headers = Headers())
{
return client->Head(path, headers);
}
private:
void run(int serverPort);
void run(int serverPort, std::string indexTemplateString = "");
private: // data
kiwix::Library library;
@@ -81,14 +81,14 @@ private: // data
ZimFileServer::ZimFileServer(int serverPort, std::string libraryFilePath)
: manager(&this->library)
{
if ( isRelativePath(libraryFilePath) )
libraryFilePath = computeAbsolutePath(getCurrentDirectory(), libraryFilePath);
if ( kiwix::isRelativePath(libraryFilePath) )
libraryFilePath = kiwix::computeAbsolutePath(kiwix::getCurrentDirectory(), libraryFilePath);
manager.readFile(libraryFilePath, true, true);
run(serverPort);
}
ZimFileServer::ZimFileServer(int serverPort, const FilePathCollection& zimpaths)
ZimFileServer::ZimFileServer(int serverPort, const FilePathCollection& zimpaths, std::string indexTemplateString)
: manager(&this->library)
{
for ( const auto& zimpath : zimpaths ) {
@@ -96,10 +96,10 @@ ZimFileServer::ZimFileServer(int serverPort, const FilePathCollection& zimpaths)
throw std::runtime_error("Unable to add the ZIM file '" + zimpath + "'");
}
run(serverPort);
run(serverPort, indexTemplateString);
}
void ZimFileServer::run(int serverPort)
void ZimFileServer::run(int serverPort, std::string indexTemplateString)
{
const std::string address = "127.0.0.1";
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
@@ -108,6 +108,9 @@ void ZimFileServer::run(int serverPort)
server->setPort(serverPort);
server->setNbThreads(2);
server->setVerbose(false);
if (!indexTemplateString.empty()) {
server->setIndexTemplateString(indexTemplateString);
}
if ( !server->start() )
throw std::runtime_error("ZimFileServer failed to start");
@@ -197,6 +200,7 @@ const ResourceCollection resources200Uncompressible{
{ WITH_ETAG, "/meta?content=zimfile&name=creator" },
{ WITH_ETAG, "/meta?content=zimfile&name=publisher" },
{ WITH_ETAG, "/meta?content=zimfile&name=favicon" },
{ WITH_ETAG, "/meta?content=zimfile&name=Illustration_48x48@1" },
{ WITH_ETAG, "/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
@@ -210,6 +214,35 @@ ResourceCollection all200Resources()
return concat(resources200Compressible, resources200Uncompressible);
}
TEST(indexTemplateStringTest, emptyIndexTemplate) {
const int PORT = 8001;
const ZimFileServer::FilePathCollection ZIMFILES {
"./test/zimfile.zim",
"./test/corner_cases.zim"
};
ZimFileServer zfs(PORT, ZIMFILES, "");
EXPECT_EQ(200, zfs.GET("/")->status);
}
TEST(indexTemplateStringTest, indexTemplateCheck) {
const int PORT = 8001;
const ZimFileServer::FilePathCollection ZIMFILES {
"./test/zimfile.zim",
"./test/corner_cases.zim"
};
ZimFileServer zfs(PORT, ZIMFILES, "<!DOCTYPE html><head>"
"<title>Welcome to kiwix library</title>"
"</head>"
"</html>");
EXPECT_EQ("<!DOCTYPE html><head>"
"<title>Welcome to kiwix library</title>"
"<link type=\"root\" href=\"\">"
"</head>"
"</html>", zfs.GET("/")->body);
}
TEST_F(ServerTest, 200)
{
for ( const Resource& res : all200Resources() )
@@ -240,6 +273,7 @@ const char* urls404[] = {
"/skin/non-existent-skin-resource",
"/catalog",
"/catalog/non-existent-item",
"/catalogBLABLABLA/root.xml",
"/meta",
"/meta?content=zimfile",
"/meta?content=zimfile&name=non-existent-item",
@@ -570,18 +604,21 @@ protected:
}
};
// Returns a copy of 'text' with every line that fully matches 'pattern'
// replaced with the fixed string 'replacement'
// Returns a copy of 'text' where every line that fully matches 'pattern'
// preceded by optional whitespace is replaced with the fixed string
// 'replacement' preserving the leading whitespace
std::string replaceLines(const std::string& text,
const std::string& pattern,
const std::string& replacement)
{
std::regex regex("^" + pattern + "$");
std::regex regex("^ *" + pattern + "$");
std::ostringstream oss;
std::istringstream iss(text);
std::string line;
while ( std::getline(iss, line) ) {
if ( std::regex_match(line, regex) ) {
for ( size_t i = 0; i < line.size() && line[i] == ' '; ++i )
oss << ' ';
oss << replacement << "\n";
} else {
oss << line << "\n";
@@ -592,10 +629,10 @@ std::string replaceLines(const std::string& text,
std::string maskVariableOPDSFeedData(std::string s)
{
s = replaceLines(s, " <updated>.+</updated>",
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>");
s = replaceLines(s, " <id>.+</id>",
" <id>12345678-90ab-cdef-1234-567890abcdef</id>");
s = replaceLines(s, R"(<updated>\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ</updated>)",
"<updated>YYYY-MM-DDThh:mm:ssZ</updated>");
s = replaceLines(s, "<id>[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}</id>",
"<id>12345678-90ab-cdef-1234-567890abcdef</id>");
return s;
}
@@ -607,22 +644,24 @@ std::string maskVariableOPDSFeedData(std::string s)
" <link rel=\"self\" href=\"\" type=\"application/atom+xml\" />\n" \
" <link rel=\"search\"" \
" type=\"application/opensearchdescription+xml\"" \
" href=\"catalog/searchdescription.xml\" />\n"
" href=\"/catalog/searchdescription.xml\" />\n"
#define CHARLES_RAY_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:charlesray</id>\n" \
" <title>Charles, Ray</title>\n" \
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
" <language>eng</language>\n" \
" <updated>2020-03-31T00:00::00Z</updated>\n" \
" <name>wikipedia_en_ray_charles</name>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
" <language>fra</language>\n" \
" <name>wikipedia_fr_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category>jazz</category>\n" \
" <tags>unittest;wikipedia;_category:jazz;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <icon>/meta?name=favicon&amp;content=zimfile</icon>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/meta?name=Illustration_48x48@1&amp;content=zimfile\"\n" \
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
" <link type=\"text/html\" href=\"/zimfile\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
@@ -630,23 +669,25 @@ std::string maskVariableOPDSFeedData(std::string s)
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/kiwix-lib/raw/master/test/data/zimfile.zim\" length=\"569344\" />\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"569344\" />\n" \
" </entry>\n"
#define RAY_CHARLES_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:raycharles</id>\n" \
" <title>Ray Charles</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
" <language>eng</language>\n" \
" <updated>2020-03-31T00:00::00Z</updated>\n" \
" <name>wikipedia_en_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category>wikipedia</category>\n" \
" <tags>unittest;wikipedia;_category:wikipedia;_pictures:no;_videos:no;_details:no;_ftindex:yes</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <icon>/meta?name=favicon&amp;content=zimfile</icon>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/meta?name=Illustration_48x48@1&amp;content=zimfile\"\n" \
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
" <link type=\"text/html\" href=\"/zimfile\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
@@ -654,23 +695,25 @@ std::string maskVariableOPDSFeedData(std::string s)
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/kiwix-lib/raw/master/test/data/zimfile.zim\" length=\"569344\" />\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" length=\"569344\" />\n" \
" </entry>\n"
#define UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY \
" <entry>\n" \
" <id>urn:uuid:raycharles_uncategorized</id>\n" \
" <title>Ray (uncategorized) Charles</title>\n" \
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
" <summary>No category is assigned to this library entry.</summary>\n" \
" <language>eng</language>\n" \
" <updated>2020-03-31T00:00::00Z</updated>\n" \
" <name>wikipedia_en_ray_charles</name>\n" \
" <language>rus</language>\n" \
" <name>wikipedia_ru_ray_charles</name>\n" \
" <flavour></flavour>\n" \
" <category></category>\n" \
" <tags>unittest;wikipedia;_pictures:no;_videos:no;_details:no</tags>\n" \
" <articleCount>284</articleCount>\n" \
" <mediaCount>2</mediaCount>\n" \
" <icon>/meta?name=favicon&amp;content=zimfile</icon>\n" \
" <link rel=\"http://opds-spec.org/image/thumbnail\"\n" \
" href=\"/meta?name=Illustration_48x48@1&amp;content=zimfile\"\n" \
" type=\"image/png;width=48;height=48;scale=1\"/>\n" \
" <link type=\"text/html\" href=\"/zimfile\" />\n" \
" <author>\n" \
" <name>Wikipedia</name>\n" \
@@ -678,7 +721,7 @@ std::string maskVariableOPDSFeedData(std::string s)
" <publisher>\n" \
" <name>Kiwix</name>\n" \
" </publisher>\n" \
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/kiwix-lib/raw/master/test/data/zimfile.zim\" length=\"125952\" />\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"
TEST_F(LibraryServerTest, catalog_root_xml)
@@ -690,6 +733,7 @@ TEST_F(LibraryServerTest, catalog_root_xml)
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>All zims</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n"
CATALOG_LINK_TAGS
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
@@ -711,7 +755,7 @@ TEST_F(LibraryServerTest, catalog_searchdescription_xml)
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
" indexOffset=\"0\"\n"
" template=\"//catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&notag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
" template=\"/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&notag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
"</OpenSearchDescription>\n"
);
}
@@ -723,7 +767,7 @@ TEST_F(LibraryServerTest, catalog_search_by_phrase)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for \"ray charles\"</title>\n"
" <title>Filtered zims (q=&quot;ray charles&quot;)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -742,7 +786,7 @@ TEST_F(LibraryServerTest, catalog_search_by_words)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for ray charles</title>\n"
" <title>Filtered zims (q=ray charles)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -763,7 +807,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for description:ray description:charles</title>\n"
" <title>Filtered zims (q=description:ray description:charles)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -780,7 +824,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for title:\"ray charles\"</title>\n"
" <title>Filtered zims (q=title:&quot;ray charles&quot;)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -799,7 +843,7 @@ TEST_F(LibraryServerTest, catalog_search_with_word_exclusion)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for ray -uncategorized</title>\n"
" <title>Filtered zims (q=ray -uncategorized)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -818,7 +862,7 @@ TEST_F(LibraryServerTest, catalog_search_by_tag)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for &lt;Empty query&gt;</title>\n"
" <title>Filtered zims (tag=_category:jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -836,7 +880,7 @@ TEST_F(LibraryServerTest, catalog_search_by_category)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for &lt;Empty query&gt;</title>\n"
" <title>Filtered zims (category=jazz)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>1</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -855,7 +899,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for &lt;Empty query&gt;</title>\n"
" <title>Filtered zims (count=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
@@ -871,7 +915,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for &lt;Empty query&gt;</title>\n"
" <title>Filtered zims (count=1&amp;start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
@@ -887,7 +931,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
OPDS_FEED_TAG
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
" <title>Search result for &lt;Empty query&gt;</title>\n"
" <title>Filtered zims (count=10&amp;start=100)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>100</startIndex>\n"
@@ -897,3 +941,397 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
);
}
}
TEST_F(LibraryServerTest, catalog_v2_root)
{
const auto r = zfs1_->GET("/catalog/v2/root.xml");
EXPECT_EQ(r->status, 200);
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2">
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<link rel="self"
href="/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="search"
href="/catalog/v2/searchdescription.xml"
type="application/opensearchdescription+xml"/>
<title>OPDS Catalog Root</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>All entries</title>
<link rel="subsection"
href="/catalog/v2/entries"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries from this catalog.</content>
</entry>
<entry>
<title>All entries (partial)</title>
<link rel="subsection"
href="/catalog/v2/partial_entries"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries from this catalog in partial format.</content>
</entry>
<entry>
<title>List of categories</title>
<link rel="subsection"
href="/catalog/v2/categories"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">List of all categories in this catalog.</content>
</entry>
<entry>
<title>List of languages</title>
<link rel="subsection"
href="/catalog/v2/languages"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">List of all languages in this catalog.</content>
</entry>
</feed>
)";
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
}
TEST_F(LibraryServerTest, catalog_v2_searchdescription_xml)
{
const auto r = zfs1_->GET("/catalog/v2/searchdescription.xml");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n"
" <ShortName>Zim catalog search</ShortName>\n"
" <Description>Search zim files in the catalog.</Description>\n"
" <Url type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"\n"
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
" indexOffset=\"0\"\n"
" template=\"/catalog/v2/entries?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
"</OpenSearchDescription>\n"
);
}
TEST_F(LibraryServerTest, catalog_v2_categories)
{
const auto r = zfs1_->GET("/catalog/v2/categories");
EXPECT_EQ(r->status, 200);
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opds="https://specs.opds.io/opds-1.2">
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<link rel="self"
href="/catalog/v2/categories"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of categories</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>jazz</title>
<link rel="subsection"
href="/catalog/v2/entries?category=jazz"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries with category of 'jazz'.</content>
</entry>
<entry>
<title>wikipedia</title>
<link rel="subsection"
href="/catalog/v2/entries?category=wikipedia"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<content type="text">All entries with category of 'wikipedia'.</content>
</entry>
</feed>
)";
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
}
TEST_F(LibraryServerTest, catalog_v2_languages)
{
const auto r = zfs1_->GET("/catalog/v2/languages");
EXPECT_EQ(r->status, 200);
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/terms/"
xmlns:opds="https://specs.opds.io/opds-1.2"
xmlns:thr="http://purl.org/syndication/thread/1.0">
<id>12345678-90ab-cdef-1234-567890abcdef</id>
<link rel="self"
href="/catalog/v2/languages"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<link rel="start"
href="/catalog/v2/root.xml"
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
<title>List of languages</title>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<entry>
<title>English</title>
<dc:language>eng</dc:language>
<thr:count>1</thr:count>
<link rel="subsection"
href="/catalog/v2/entries?lang=eng"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
</entry>
<entry>
<title>français</title>
<dc:language>fra</dc:language>
<thr:count>1</thr:count>
<link rel="subsection"
href="/catalog/v2/entries?lang=fra"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
</entry>
<entry>
<title>русский</title>
<dc:language>rus</dc:language>
<thr:count>1</thr:count>
<link rel="subsection"
href="/catalog/v2/entries?lang=rus"
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
<id>12345678-90ab-cdef-1234-567890abcdef</id>
</entry>
</feed>
)";
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
}
#define CATALOG_V2_ENTRIES_PREAMBLE0(x) \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\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" \
"\n" \
" <link rel=\"self\"\n" \
" href=\"/catalog/v2/" x "\"\n" \
" type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"/>\n" \
" <link rel=\"start\"\n" \
" href=\"/catalog/v2/root.xml\"\n" \
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\n" \
" <link rel=\"up\"\n" \
" href=\"/catalog/v2/root.xml\"\n" \
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\n" \
"\n" \
#define CATALOG_V2_ENTRIES_PREAMBLE(q) \
CATALOG_V2_ENTRIES_PREAMBLE0("entries" q)
#define CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE(q) \
CATALOG_V2_ENTRIES_PREAMBLE0("partial_entries" q)
TEST_F(LibraryServerTest, catalog_v2_entries)
{
const auto r = zfs1_->GET("/catalog/v2/entries");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("")
" <title>All Entries</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n"
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range)
{
{
const auto r = zfs1_->GET("/catalog/v2/entries?start=1");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?start=1")
" <title>Filtered Entries (start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/catalog/v2/entries?count=2");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?count=2")
" <title>Filtered Entries (count=2)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
CHARLES_RAY_CATALOG_ENTRY
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
{
const auto r = zfs1_->GET("/catalog/v2/entries?start=1&count=1");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?count=1&start=1")
" <title>Filtered Entries (count=1&amp;start=1)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>3</totalResults>\n"
" <startIndex>1</startIndex>\n"
" <itemsPerPage>1</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
"</feed>\n"
);
}
}
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
{
const auto r = zfs1_->GET("/catalog/v2/entries?q=\"ray%20charles\"");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_ENTRIES_PREAMBLE("?q=%22ray%20charles%22")
" <title>Filtered Entries (q=&quot;ray charles&quot;)</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <totalResults>2</totalResults>\n"
" <startIndex>0</startIndex>\n"
" <itemsPerPage>2</itemsPerPage>\n"
RAY_CHARLES_CATALOG_ENTRY
CHARLES_RAY_CATALOG_ENTRY
"</feed>\n"
);
}
TEST_F(LibraryServerTest, 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 = "/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 = "/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 = "/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 = "/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 = "/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 = "/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("/catalog/v2/entry/raycharles");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
RAY_CHARLES_CATALOG_ENTRY
);
const auto r1 = zfs1_->GET("/catalog/v2/entry/non-existent-entry");
EXPECT_EQ(r1->status, 404);
}
TEST_F(LibraryServerTest, catalog_v2_partial_entries)
{
const auto r = zfs1_->GET("/catalog/v2/partial_entries");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
CATALOG_V2_PARTIAL_ENTRIES_PREAMBLE("")
" <title>All Entries</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
"\n"
" <entry>\n"
" <id>urn:uuid:charlesray</id>\n"
" <title>Charles, Ray</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <link rel=\"alternate\"\n"
" href=\"/catalog/v2/entry/charlesray\"\n"
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
" </entry>\n"
" <entry>\n"
" <id>urn:uuid:raycharles</id>\n"
" <title>Ray Charles</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <link rel=\"alternate\"\n"
" href=\"/catalog/v2/entry/raycharles\"\n"
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
" </entry>\n"
" <entry>\n"
" <id>urn:uuid:raycharles_uncategorized</id>\n"
" <title>Ray (uncategorized) Charles</title>\n"
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
" <link rel=\"alternate\"\n"
" href=\"/catalog/v2/entry/raycharles_uncategorized\"\n"
" type=\"application/atom+xml;type=entry;profile=opds-catalog\"/>\n"
" </entry>\n"
"</feed>\n"
);
}