mirror of
https://github.com/kiwix/libkiwix.git
synced 2026-01-06 05:18:07 -05:00
Compare commits
161 Commits
12.1.1
...
more_predi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf0fe8b8f | ||
|
|
e625c25ef1 | ||
|
|
b2ae1d66f5 | ||
|
|
2818dd3151 | ||
|
|
09eec822c1 | ||
|
|
34cd553642 | ||
|
|
70dd738801 | ||
|
|
958067d94d | ||
|
|
33a3277400 | ||
|
|
8f5714be07 | ||
|
|
c4fa42f20b | ||
|
|
795fcb9de4 | ||
|
|
c697611064 | ||
|
|
e5dab19844 | ||
|
|
1f44465d09 | ||
|
|
258a6d029f | ||
|
|
fc211d9a2e | ||
|
|
aff801e6cc | ||
|
|
3479589d53 | ||
|
|
d2f20dba66 | ||
|
|
dc3960c5f8 | ||
|
|
1f9026f295 | ||
|
|
30b3f05497 | ||
|
|
13a6863183 | ||
|
|
bb1a730253 | ||
|
|
e1f067c086 | ||
|
|
103a4516db | ||
|
|
bceba4da06 | ||
|
|
e14de69271 | ||
|
|
d2fedf9123 | ||
|
|
b151a2a480 | ||
|
|
8b8a2eede7 | ||
|
|
f3d3ab13cb | ||
|
|
1553d52593 | ||
|
|
f298acd45f | ||
|
|
0b542fe66d | ||
|
|
e72fc2391d | ||
|
|
d39e91f6bc | ||
|
|
0b7cd614c6 | ||
|
|
54191bcfab | ||
|
|
797f4c432c | ||
|
|
c57b8a0c7c | ||
|
|
aee6c23082 | ||
|
|
af228bf45f | ||
|
|
b9323f17bb | ||
|
|
8993f99587 | ||
|
|
96b6f41244 | ||
|
|
3f0ea083e6 | ||
|
|
9c5f5c7be0 | ||
|
|
9375f97b60 | ||
|
|
2ad5e510c6 | ||
|
|
a2e56e2422 | ||
|
|
8cc724b4a4 | ||
|
|
fa212fd6ae | ||
|
|
c0073b3bc7 | ||
|
|
0d2b6b3344 | ||
|
|
5f27b4b651 | ||
|
|
7a85c92025 | ||
|
|
6e2be481fd | ||
|
|
db3b76247f | ||
|
|
6a651e04e5 | ||
|
|
22ea3106c5 | ||
|
|
2d132d701e | ||
|
|
f81a5a1a4b | ||
|
|
3dce025f47 | ||
|
|
e470c97f74 | ||
|
|
a7ea908bcd | ||
|
|
41f25083da | ||
|
|
3188b0afe6 | ||
|
|
f8aae395f3 | ||
|
|
c5088aad7b | ||
|
|
269a659160 | ||
|
|
7161df9e4c | ||
|
|
24faf84163 | ||
|
|
571c09e00a | ||
|
|
a959800173 | ||
|
|
b2196ee7a9 | ||
|
|
aea51c21ff | ||
|
|
95d627afa1 | ||
|
|
183bdcf2c0 | ||
|
|
e1cf16ddea | ||
|
|
a74df86fcf | ||
|
|
605c7f71e0 | ||
|
|
f58d4a93e1 | ||
|
|
00032adce2 | ||
|
|
f5e6502e04 | ||
|
|
37274f7882 | ||
|
|
07ff4eab43 | ||
|
|
e89f4e2ac7 | ||
|
|
bcbdce6a9a | ||
|
|
0effcdb23f | ||
|
|
5c8dd0e8d3 | ||
|
|
d2c031e047 | ||
|
|
733b027c2f | ||
|
|
e8b8c18297 | ||
|
|
29c33a7ad6 | ||
|
|
fd504c1166 | ||
|
|
0c05af658d | ||
|
|
0c0b1f5971 | ||
|
|
a65681d6f4 | ||
|
|
af27141320 | ||
|
|
d2bb3d198c | ||
|
|
a5db4a1fd5 | ||
|
|
59f0070ecc | ||
|
|
bd818d33af | ||
|
|
16fbf15938 | ||
|
|
8383265ac4 | ||
|
|
0eb9a06736 | ||
|
|
01aa190c38 | ||
|
|
da891699ac | ||
|
|
f9be9f98ce | ||
|
|
22b55d36c6 | ||
|
|
2d86927e17 | ||
|
|
86be66a2d8 | ||
|
|
4425cd2122 | ||
|
|
ab0d7b6e80 | ||
|
|
cfc91b0967 | ||
|
|
2650cdd7da | ||
|
|
efdb596561 | ||
|
|
177e1d5da6 | ||
|
|
b861dfc9dd | ||
|
|
3fdbb5a990 | ||
|
|
e49abc1df1 | ||
|
|
9166b67c47 | ||
|
|
1dc9705597 | ||
|
|
5292f06fff | ||
|
|
f8e7c3d476 | ||
|
|
ead1474ead | ||
|
|
1316dec37c | ||
|
|
a5557eeb25 | ||
|
|
efcbf6ef1e | ||
|
|
139b561253 | ||
|
|
c203e07ee9 | ||
|
|
49e99e7c22 | ||
|
|
e13324fbba | ||
|
|
c38ab3e5d7 | ||
|
|
bde737f63b | ||
|
|
cc6aa9b162 | ||
|
|
9063450b5a | ||
|
|
f8c3a1fd2e | ||
|
|
b5b98e7a61 | ||
|
|
e7e8275a31 | ||
|
|
c6456cac42 | ||
|
|
f0c0400485 | ||
|
|
ccbeb154a5 | ||
|
|
0e8a2952d5 | ||
|
|
fe5e6c451d | ||
|
|
3966e8544b | ||
|
|
09476ededb | ||
|
|
d47c4fa72f | ||
|
|
c938101c70 | ||
|
|
9c91fc7369 | ||
|
|
385931f229 | ||
|
|
8726de494c | ||
|
|
94d6bef402 | ||
|
|
a28c2973e9 | ||
|
|
7feb89c30e | ||
|
|
903dcd46d6 | ||
|
|
1be5424711 | ||
|
|
de517330f6 | ||
|
|
5c3a997de4 |
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@@ -8,7 +8,17 @@ on:
|
||||
|
||||
jobs:
|
||||
macOS:
|
||||
runs-on: macos-11
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-13
|
||||
target:
|
||||
- native_dyn
|
||||
- iOS_arm64
|
||||
- iOS_x86_64
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
env:
|
||||
HOME: /Users/runner
|
||||
steps:
|
||||
@@ -22,22 +32,31 @@ jobs:
|
||||
# upgrade from python@3.11.2_1 to python@3.11.3 fails to overwrite those
|
||||
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.11 /usr/local/bin/idle3 /usr/local/bin/idle3.11 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.11 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.11 /usr/local/bin/python3.11-config
|
||||
brew install pkg-config ninja meson
|
||||
env:
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
ARCHIVE_NAME: deps2_macos_native_dyn_libkiwix.tar.xz
|
||||
run: |
|
||||
wget -O- https://tmp.kiwix.org/ci/${{env.ARCHIVE_NAME}} | tar -xJ -C ${{env.HOME}}
|
||||
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
|
||||
with:
|
||||
os_name: macos
|
||||
target_platform: ${{ matrix.target }}
|
||||
|
||||
- name: Compile source code
|
||||
- name: Compile
|
||||
env:
|
||||
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib/pkgconfig
|
||||
PKG_CONFIG_PATH: ${{env.HOME}}/BUILD_${{matrix.target}}/INSTALL/lib/pkgconfig
|
||||
CPPFLAGS: -I${{env.HOME}}/BUILD_native_dyn/INSTALL/include
|
||||
MESON_OPTION: --default-library=shared -Db_coverage=true
|
||||
MESON_CROSSFILE: ${{env.HOME}}/BUILD_${{matrix.target}}/meson_cross_file.txt
|
||||
shell: bash
|
||||
run: |
|
||||
meson . build --default-library=shared -Db_coverage=true
|
||||
if [[ ! "${{matrix.target}}" =~ native_.* ]]; then
|
||||
MESON_OPTION="$MESON_OPTION --cross-file $MESON_CROSSFILE -Dstatic-linkage=true"
|
||||
fi
|
||||
meson . build ${MESON_OPTION}
|
||||
ninja -C build
|
||||
|
||||
- name: Test libkiwix
|
||||
if: startsWith(matrix.target, 'native_')
|
||||
env:
|
||||
SKIP_BIG_MEMORY_TEST: 1
|
||||
LD_LIBRARY_PATH: ${{env.HOME}}/BUILD_native_dyn/INSTALL/lib:${{env.HOME}}/BUILD_native_dyn/INSTALL/lib64
|
||||
@@ -83,15 +102,14 @@ jobs:
|
||||
HOME: /home/runner
|
||||
runs-on: ubuntu-20.04
|
||||
container:
|
||||
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:37"
|
||||
image: "ghcr.io/kiwix/kiwix-build_ci_${{matrix.image_variant}}:38"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install deps
|
||||
shell: bash
|
||||
run: |
|
||||
ARCHIVE_NAME=deps2_${OS_NAME}_${{matrix.target}}_libkiwix.tar.xz
|
||||
wget -O- http://tmp.kiwix.org/ci/${ARCHIVE_NAME} | tar -xJ -C /home/runner
|
||||
- name: Install dependencies
|
||||
uses: kiwix/kiwix-build/actions/dl_deps_archive@main
|
||||
with:
|
||||
target_platform: ${{ matrix.target }}
|
||||
- name: Compile
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
21
.github/workflows/package.yml
vendored
21
.github/workflows/package.yml
vendored
@@ -5,15 +5,17 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-deb:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
distro:
|
||||
- ubuntu-kinetic
|
||||
- debian-unstable
|
||||
- ubuntu-jammy
|
||||
- ubuntu-focal
|
||||
steps:
|
||||
@@ -38,13 +40,12 @@ jobs:
|
||||
email: release+launchpad@kiwix.org
|
||||
distro: ${{ matrix.distro }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-kinetic
|
||||
if: matrix.distro == 'ubuntu-kinetic'
|
||||
name: Build package for ubuntu-kinetic
|
||||
id: build-ubuntu-kinetic
|
||||
- uses: legoktm/gh-action-build-deb@debian-unstable
|
||||
if: matrix.distro == 'debian-unstable'
|
||||
name: Build package for debian-unstable
|
||||
id: build-debian-unstable
|
||||
with:
|
||||
args: --no-sign
|
||||
ppa: ${{ steps.ppa.outputs.ppa }}
|
||||
|
||||
- uses: legoktm/gh-action-build-deb@ubuntu-jammy
|
||||
if: matrix.distro == 'ubuntu-jammy'
|
||||
@@ -69,7 +70,7 @@ jobs:
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
name: Upload dev package
|
||||
# Only upload on pushes to git default branch
|
||||
# Only upload on pushes to main
|
||||
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' && startswith(matrix.distro, 'ubuntu-')
|
||||
with:
|
||||
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
|
||||
@@ -78,10 +79,8 @@ jobs:
|
||||
|
||||
- uses: legoktm/gh-action-dput@master
|
||||
name: Upload release package
|
||||
# Only upload on pushes to master or tag
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && startswith(matrix.distro, 'ubuntu-')
|
||||
if: github.event_name == 'release' && startswith(matrix.distro, 'ubuntu-')
|
||||
with:
|
||||
gpg_key: ${{ secrets.LAUNCHPAD_GPG }}
|
||||
repository: ppa:kiwixteam/release
|
||||
packages: output/*_source.changes
|
||||
|
||||
|
||||
19
ChangeLog
19
ChangeLog
@@ -1,7 +1,22 @@
|
||||
libkiwix 12.1.1
|
||||
libkiwix 13.0.0
|
||||
===============
|
||||
|
||||
* Revert API break introduced in libkiwix 12.1.0
|
||||
* Server:
|
||||
- Improved look & feel of kiwix-serve UI (@veloman-yunkan #917 #1021)
|
||||
- Increase tolerance to malformed (control characters) ZIM entry titles (@veloman-yunkan #1023)
|
||||
- API allowing to filter many categories at once (@juuz0 #974)
|
||||
- Cookie-less user language control (@veloman-yumkan #997)
|
||||
- Hack to fix Mirrorbrain based broken magnet URLs (@rgaudin #1001)
|
||||
* Fix handling of books with 'Name' metadata with dots (@mgautier #1016)
|
||||
* New method beautifyFileSize() to provide nice-looking book sizes (@vuuz0 #971)
|
||||
* Fix a few missing includes (@mgautierfr #978)
|
||||
* New functions to read - kiwix-serve - languages and categories streams (@juuz0 #967)
|
||||
* Add support of Fon language (@kelson42 #1013)
|
||||
* C++17 code base compliancy (@mgautierfr #996)
|
||||
* Use everywhere std::shared_ptr in place of raw pointer (@mgautierfr #991)
|
||||
* Do not use [[nodiscard]] attribute on compiler not supporting it (@mgautierfr #1003)
|
||||
* Add a non minified version of autoComplete.js (@mgautierfr #1008)
|
||||
* Multiple CI/CD improvements (@kelson42 #982)
|
||||
|
||||
libkiwix 12.1.0
|
||||
===============
|
||||
|
||||
@@ -24,9 +24,9 @@ with the Libkiwix compilation itself, we recommend to have a look to
|
||||
Preamble
|
||||
--------
|
||||
|
||||
Although the Libkiwix can be (cross-)compiled on/for many sytems, the
|
||||
Although the Libkiwix can be (cross-)compiled on/for many systems, the
|
||||
following documentation explains how to do it on POSIX ones. It is
|
||||
primarly thought for GNU/Linux systems and has been tested on recent
|
||||
primarily thought for GNU/Linux systems and has been tested on recent
|
||||
releases of Ubuntu and Fedora.
|
||||
|
||||
Dependencies
|
||||
@@ -54,7 +54,7 @@ The following dependency needs to be available at runtime:
|
||||
These dependencies may or may not be packaged by your operating
|
||||
system. They may also be packaged but only in an older version. The
|
||||
compilation script will tell you if one of them is missing or too old.
|
||||
In the worse case, you will have to download and compile bleeding edge
|
||||
In the worst case, you will have to download and compile bleeding edge
|
||||
version by hand.
|
||||
|
||||
If you want to install these dependencies locally, then use the
|
||||
@@ -201,7 +201,7 @@ To use JS provided by kiwix-serve you can use the following template to start wi
|
||||
|
||||
|
||||
If you compile manually Libmicrohttpd, you might need to compile it
|
||||
without GNU TLS, a bug here will empeach further compilation
|
||||
without GNU TLS, a bug here will impeach further compilation
|
||||
otherwise.
|
||||
|
||||
If the compilation still fails, you might need to get a more recent
|
||||
|
||||
@@ -24,8 +24,6 @@ author = 'libkiwix-team'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
@@ -42,9 +40,7 @@ templates_path = ['_templates']
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
|
||||
if not on_rtd:
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
breathe
|
||||
exhale
|
||||
sphinx_rtd_theme
|
||||
|
||||
@@ -184,7 +184,7 @@ class Downloader
|
||||
* @param options: A series of pair <option_name, option_value> to pass to aria.
|
||||
* @return: The newly created Download.
|
||||
*/
|
||||
Download* startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
std::shared_ptr<Download> startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options = {});
|
||||
|
||||
/**
|
||||
* Get a download corrsponding to a download id (did)
|
||||
@@ -194,7 +194,7 @@ class Downloader
|
||||
* @return: The Download corresponding to did.
|
||||
* @throw: Throw std::out_of_range if did is not found.
|
||||
*/
|
||||
Download* getDownload(const std::string& did);
|
||||
std::shared_ptr<Download> getDownload(const std::string& did);
|
||||
|
||||
/**
|
||||
* Get the number of downloads currently managed.
|
||||
@@ -204,11 +204,11 @@ class Downloader
|
||||
/**
|
||||
* Get the ids of the managed downloads.
|
||||
*/
|
||||
std::vector<std::string> getDownloadIds();
|
||||
std::vector<std::string> getDownloadIds() const;
|
||||
|
||||
private:
|
||||
mutable std::mutex m_lock;
|
||||
std::map<std::string, std::unique_ptr<Download>> m_knownDownloads;
|
||||
std::map<std::string, std::shared_ptr<Download>> m_knownDownloads;
|
||||
std::shared_ptr<Aria2> mp_aria;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
|
||||
#define KIWIX_LIBRARY_VERSION "20110515"
|
||||
|
||||
namespace Xapian {
|
||||
class WritableDatabase;
|
||||
};
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
@@ -105,6 +109,12 @@ class Filter {
|
||||
Filter& acceptTags(const Tags& tags);
|
||||
Filter& rejectTags(const Tags& tags);
|
||||
|
||||
/**
|
||||
* Set the filter to only accept books in the specified category.
|
||||
*
|
||||
* Multiple categories can be specified as a comma-separated list (in
|
||||
* which case a book in any of those categories will match).
|
||||
*/
|
||||
Filter& category(std::string category);
|
||||
|
||||
/**
|
||||
@@ -167,31 +177,53 @@ class ZimSearcher : public zim::Searcher
|
||||
std::mutex m_mutex;
|
||||
};
|
||||
|
||||
template<typename, typename>
|
||||
class ConcurrentCache;
|
||||
|
||||
template<typename, typename>
|
||||
class MultiKeyCache;
|
||||
|
||||
using LibraryPtr = std::shared_ptr<Library>;
|
||||
using ConstLibraryPtr = std::shared_ptr<const Library>;
|
||||
|
||||
|
||||
// Some compiler we use don't have [[nodiscard]] attribute.
|
||||
// We don't want to declare `create` with it in this case.
|
||||
#define LIBKIWIX_NODISCARD
|
||||
#if defined __has_cpp_attribute
|
||||
#if __has_cpp_attribute (nodiscard)
|
||||
#undef LIBKIWIX_NODISCARD
|
||||
#define LIBKIWIX_NODISCARD [[nodiscard]]
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A Library store several books.
|
||||
*/
|
||||
class Library
|
||||
class Library: public std::enable_shared_from_this<Library>
|
||||
{
|
||||
// all data fields must be added in LibraryBase
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
public:
|
||||
typedef uint64_t Revision;
|
||||
typedef std::vector<std::string> BookIdCollection;
|
||||
typedef std::map<std::string, int> AttributeCounts;
|
||||
typedef std::set<std::string> BookIdSet;
|
||||
|
||||
public:
|
||||
private:
|
||||
Library();
|
||||
|
||||
public:
|
||||
LIBKIWIX_NODISCARD static LibraryPtr create() {
|
||||
return LibraryPtr(new Library());
|
||||
}
|
||||
~Library();
|
||||
|
||||
/**
|
||||
* Library is not a copiable object. However it can be moved.
|
||||
*/
|
||||
Library(const Library& ) = delete;
|
||||
Library(Library&& );
|
||||
Library(Library&& ) = delete;
|
||||
void operator=(const Library& ) = delete;
|
||||
Library& operator=(Library&& );
|
||||
Library& operator=(Library&& ) = delete;
|
||||
|
||||
/**
|
||||
* Add a book to the library.
|
||||
@@ -362,19 +394,36 @@ class Library
|
||||
|
||||
private: // types
|
||||
typedef const std::string& (Book::*BookStrPropMemFn)() const;
|
||||
struct Impl;
|
||||
struct Entry : Book
|
||||
{
|
||||
Library::Revision lastUpdatedRevision = 0;
|
||||
};
|
||||
|
||||
private: // functions
|
||||
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
|
||||
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
|
||||
BookIdCollection filterViaBookDB(const Filter& filter) const;
|
||||
unsigned int getBookCount_not_protected(const bool localBooks, const bool remoteBooks) const;
|
||||
void updateBookDB(const Book& book);
|
||||
void dropCache(const std::string& bookId);
|
||||
|
||||
private: //data
|
||||
std::unique_ptr<Impl> mp_impl;
|
||||
mutable std::mutex m_mutex;
|
||||
Library::Revision m_revision;
|
||||
std::map<std::string, Entry> m_books;
|
||||
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
|
||||
std::unique_ptr<ArchiveCache> mp_archiveCache;
|
||||
using SearcherCache = MultiKeyCache<std::string, std::shared_ptr<ZimSearcher>>;
|
||||
std::unique_ptr<SearcherCache> mp_searcherCache;
|
||||
std::vector<kiwix::Bookmark> m_bookmarks;
|
||||
std::unique_ptr<Xapian::WritableDatabase> m_bookDB;
|
||||
};
|
||||
|
||||
// We don't need it anymore and we don't want to polute any other potential usage
|
||||
// of `LIBKIWIX_NODISCARD` token.
|
||||
#undef LIBKIWIX_NODISCARD
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -37,10 +37,10 @@ namespace kiwix
|
||||
class LibraryManipulator
|
||||
{
|
||||
public: // functions
|
||||
explicit LibraryManipulator(Library* library);
|
||||
explicit LibraryManipulator(LibraryPtr library);
|
||||
virtual ~LibraryManipulator();
|
||||
|
||||
Library& getLibrary() const { return library; }
|
||||
LibraryPtr getLibrary() const { return library; }
|
||||
|
||||
bool addBookToLibrary(const Book& book);
|
||||
void addBookmarkToLibrary(const Bookmark& bookmark);
|
||||
@@ -52,7 +52,7 @@ class LibraryManipulator
|
||||
virtual void booksWereRemovedFromLibrary();
|
||||
|
||||
private: // data
|
||||
kiwix::Library& library;
|
||||
LibraryPtr library;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -64,8 +64,8 @@ class Manager
|
||||
typedef std::vector<std::string> Paths;
|
||||
|
||||
public: // functions
|
||||
explicit Manager(LibraryManipulator* manipulator);
|
||||
explicit Manager(Library* library);
|
||||
explicit Manager(LibraryManipulator manipulator);
|
||||
explicit Manager(LibraryPtr library);
|
||||
|
||||
/**
|
||||
* Read a `library.xml` and add book in the file to the library.
|
||||
@@ -163,7 +163,7 @@ class Manager
|
||||
uint64_t m_itemsPerPage = 0;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<kiwix::LibraryManipulator> manipulator;
|
||||
kiwix::LibraryManipulator manipulator;
|
||||
|
||||
bool readBookFromPath(const std::string& path, Book* book);
|
||||
bool parseXmlDom(const pugi::xml_document& doc,
|
||||
|
||||
@@ -4,8 +4,6 @@ headers = [
|
||||
'common.h',
|
||||
'library.h',
|
||||
'manager.h',
|
||||
'libxml_dumper.h',
|
||||
'opds_dumper.h',
|
||||
'downloader.h',
|
||||
'search_renderer.h',
|
||||
'server.h',
|
||||
|
||||
@@ -59,7 +59,7 @@ class HumanReadableNameMapper : public NameMapper {
|
||||
class UpdatableNameMapper : public NameMapper {
|
||||
typedef std::shared_ptr<NameMapper> NameMapperHandle;
|
||||
public:
|
||||
UpdatableNameMapper(Library& library, bool withAlias);
|
||||
UpdatableNameMapper(std::shared_ptr<Library> library, bool withAlias);
|
||||
|
||||
virtual std::string getNameForId(const std::string& id) const;
|
||||
virtual std::string getIdForName(const std::string& name) const;
|
||||
@@ -71,7 +71,7 @@ class UpdatableNameMapper : public NameMapper {
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex;
|
||||
Library& library;
|
||||
std::shared_ptr<Library> library;
|
||||
NameMapperHandle nameMapper;
|
||||
const bool withAlias;
|
||||
};
|
||||
|
||||
@@ -37,29 +37,11 @@ class SearchRenderer
|
||||
/**
|
||||
* Construct a SearchRenderer from a SearchResultSet.
|
||||
*
|
||||
* The constructed version of the SearchRenderer will not introduce
|
||||
* the book name for each result. It is better to use the other constructor
|
||||
* with a Library pointer to have a better html page.
|
||||
*
|
||||
* @param srs The `SearchResultSet` to render.
|
||||
* @param mapper The `NameMapper` to use to do the rendering.
|
||||
* @param start The start offset used for the srs.
|
||||
* @param estimatedResultCount The estimatedResultCount of the whole search
|
||||
*/
|
||||
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
|
||||
unsigned int start, unsigned int estimatedResultCount);
|
||||
|
||||
/**
|
||||
* Construct a SearchRenderer from a SearchResultSet.
|
||||
*
|
||||
* @param srs The `SearchResultSet` to render.
|
||||
* @param mapper The `NameMapper` to use to do the rendering.
|
||||
* @param library The `Library` to use to look up book details for search results.
|
||||
* @param start The start offset used for the srs.
|
||||
* @param estimatedResultCount The estimatedResultCount of the whole search
|
||||
*/
|
||||
SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
|
||||
unsigned int start, unsigned int estimatedResultCount);
|
||||
SearchRenderer(zim::SearchResultSet srs, unsigned int start, unsigned int estimatedResultCount);
|
||||
|
||||
~SearchRenderer();
|
||||
|
||||
@@ -90,24 +72,39 @@ class SearchRenderer
|
||||
this->pageLength = pageLength;
|
||||
}
|
||||
|
||||
std::string renderTemplate(const std::string& tmpl_str);
|
||||
/**
|
||||
* set user language
|
||||
*/
|
||||
void setUserLang(const std::string& lang){
|
||||
this->userlang = lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the html page with the resutls of the search.
|
||||
*
|
||||
* @param mapper The `NameMapper` to use to do the rendering.
|
||||
* @param library The `Library` to use to look up book details for search results.
|
||||
May be nullptr. In this case, bookName is not set in the rendered string.
|
||||
* @return The html string
|
||||
*/
|
||||
std::string getHtml();
|
||||
std::string getHtml(const NameMapper& mapper, const Library* library);
|
||||
|
||||
/**
|
||||
/**
|
||||
* Generate the xml page with the resutls of the search.
|
||||
*
|
||||
* @param mapper The `NameMapper` to use to do the rendering.
|
||||
* @param library The `Library` to use to look up book details for search results.
|
||||
May be nullptr. In this case, bookName is not set in the rendered string.
|
||||
* @return The xml string
|
||||
*/
|
||||
std::string getXml();
|
||||
std::string getXml(const NameMapper& mapper, const Library* library);
|
||||
|
||||
protected: // function
|
||||
std::string renderTemplate(const std::string& tmpl_str, const NameMapper& mapper, const Library *library);
|
||||
|
||||
protected:
|
||||
std::string beautifyInteger(const unsigned int number);
|
||||
zim::SearchResultSet m_srs;
|
||||
NameMapper* mp_nameMapper;
|
||||
Library* mp_library;
|
||||
std::string searchBookQuery;
|
||||
std::string searchPattern;
|
||||
std::string protocolPrefix;
|
||||
@@ -115,6 +112,7 @@ class SearchRenderer
|
||||
unsigned int pageLength;
|
||||
unsigned int estimatedResultCount;
|
||||
unsigned int resultStart;
|
||||
std::string userlang = "en";
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace kiwix
|
||||
*
|
||||
* @param library The library to serve.
|
||||
*/
|
||||
Server(Library* library, NameMapper* nameMapper=nullptr);
|
||||
Server(std::shared_ptr<Library> library, std::shared_ptr<NameMapper> nameMapper=nullptr);
|
||||
|
||||
virtual ~Server();
|
||||
|
||||
@@ -66,8 +66,8 @@ namespace kiwix
|
||||
std::string getAddress();
|
||||
|
||||
protected:
|
||||
Library* mp_library;
|
||||
NameMapper* mp_nameMapper;
|
||||
std::shared_ptr<Library> mp_library;
|
||||
std::shared_ptr<NameMapper> mp_nameMapper;
|
||||
std::string m_root = "";
|
||||
std::string m_addr = "";
|
||||
std::string m_indexTemplateString = "";
|
||||
|
||||
@@ -23,8 +23,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
|
||||
namespace kiwix {
|
||||
typedef std::pair<std::string, std::string> LangNameCodePair;
|
||||
typedef std::vector<LangNameCodePair> FeedLanguages;
|
||||
typedef std::vector<std::string> FeedCategories;
|
||||
|
||||
/**
|
||||
* Return the current directory.
|
||||
@@ -216,5 +220,37 @@ std::map<std::string, std::string> getNetworkInterfaces();
|
||||
*/
|
||||
std::string getBestPublicIp();
|
||||
|
||||
/** Converts file size to human readable format.
|
||||
*
|
||||
* This function will convert a number to its equivalent size using units.
|
||||
*
|
||||
* @param number file size in bytes.
|
||||
* @return a human-readable string representation of the size, e.g., "2.3 KB", "1.8 MB", "5.2 GB".
|
||||
*/
|
||||
std::string beautifyFileSize(uint64_t number);
|
||||
|
||||
/**
|
||||
* Load languages stored in an OPDS stream.
|
||||
*
|
||||
* @param content the OPDS stream.
|
||||
* @return vector containing pairs of language code and their corresponding full language name.
|
||||
*/
|
||||
FeedLanguages readLanguagesFromFeed(const std::string& content);
|
||||
|
||||
/**
|
||||
* Load categories stored in an OPDS stream .
|
||||
*
|
||||
* @param content the OPDS stream.
|
||||
* @return vector containing category strings.
|
||||
*/
|
||||
FeedCategories readCategoriesFromFeed(const std::string& content);
|
||||
|
||||
/**
|
||||
* Retrieve the full language name associated with a given ISO 639-3 language code.
|
||||
*
|
||||
* @param lang ISO 639-3 language code.
|
||||
* @return full language name.
|
||||
*/
|
||||
std::string getLanguageSelfName(const std::string& lang);
|
||||
}
|
||||
#endif // KIWIX_TOOLS_H
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
project('libkiwix', 'cpp',
|
||||
version : '12.1.1',
|
||||
version : '13.0.0',
|
||||
license : 'GPLv3+',
|
||||
default_options : ['c_std=c11', 'cpp_std=c++11', 'werror=true'])
|
||||
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])
|
||||
|
||||
compiler = meson.get_compiler('cpp')
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ void Downloader::close()
|
||||
mp_aria->close();
|
||||
}
|
||||
|
||||
std::vector<std::string> Downloader::getDownloadIds() {
|
||||
std::vector<std::string> Downloader::getDownloadIds() const {
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
std::vector<std::string> ret;
|
||||
for(auto& p:m_knownDownloads) {
|
||||
@@ -166,37 +166,31 @@ std::vector<std::string> Downloader::getDownloadIds() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
Download* Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
std::shared_ptr<Download> Downloader::startDownload(const std::string& uri, const std::vector<std::pair<std::string, std::string>>& options)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
for (auto& p: m_knownDownloads) {
|
||||
auto& d = p.second;
|
||||
auto& uris = d->getUris();
|
||||
if (std::find(uris.begin(), uris.end(), uri) != uris.end())
|
||||
return d.get();
|
||||
}
|
||||
std::vector<std::string> uris = {uri};
|
||||
auto gid = mp_aria->addUri(uris, options);
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
return m_knownDownloads[gid].get();
|
||||
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
|
||||
return m_knownDownloads[gid];
|
||||
}
|
||||
|
||||
Download* Downloader::getDownload(const std::string& did)
|
||||
std::shared_ptr<Download> Downloader::getDownload(const std::string& did)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_lock);
|
||||
try {
|
||||
return m_knownDownloads.at(did).get();
|
||||
return m_knownDownloads.at(did);
|
||||
} catch(std::exception& e) {
|
||||
for (auto gid : mp_aria->tellWaiting()) {
|
||||
if (gid == did) {
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
return m_knownDownloads[gid].get();
|
||||
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
|
||||
return m_knownDownloads[gid];
|
||||
}
|
||||
}
|
||||
for (auto gid : mp_aria->tellActive()) {
|
||||
if (gid == did) {
|
||||
m_knownDownloads[gid] = std::unique_ptr<Download>(new Download(mp_aria, gid));
|
||||
return m_knownDownloads[gid].get();
|
||||
m_knownDownloads[gid] = std::make_shared<Download>(mp_aria, gid);
|
||||
return m_knownDownloads[gid];
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
|
||||
163
src/library.cpp
163
src/library.cpp
@@ -39,6 +39,8 @@
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
@@ -58,6 +60,8 @@ bool booksReferToTheSameArchive(const Book& book1, const Book& book2)
|
||||
&& book1.getPath() == book2.getPath();
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
template<typename Key, typename Value>
|
||||
class MultiKeyCache: public ConcurrentCache<std::set<Key>, Value>
|
||||
{
|
||||
@@ -79,49 +83,8 @@ class MultiKeyCache: public ConcurrentCache<std::set<Key>, Value>
|
||||
}
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
struct Library::Impl
|
||||
{
|
||||
struct Entry : Book
|
||||
{
|
||||
Library::Revision lastUpdatedRevision = 0;
|
||||
};
|
||||
|
||||
Library::Revision m_revision;
|
||||
std::map<std::string, Entry> m_books;
|
||||
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
|
||||
std::unique_ptr<ArchiveCache> mp_archiveCache;
|
||||
using SearcherCache = MultiKeyCache<std::string, std::shared_ptr<ZimSearcher>>;
|
||||
std::unique_ptr<SearcherCache> mp_searcherCache;
|
||||
std::vector<kiwix::Bookmark> m_bookmarks;
|
||||
Xapian::WritableDatabase m_bookDB;
|
||||
|
||||
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;
|
||||
|
||||
Impl();
|
||||
~Impl();
|
||||
|
||||
Impl(Impl&& );
|
||||
Impl& operator=(Impl&& );
|
||||
};
|
||||
|
||||
Library::Impl::Impl()
|
||||
: mp_archiveCache(new ArchiveCache(std::max(getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", 1), 1))),
|
||||
mp_searcherCache(new SearcherCache(std::max(getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", 1), 1))),
|
||||
m_bookDB("", Xapian::DB_BACKEND_INMEMORY)
|
||||
{
|
||||
}
|
||||
|
||||
Library::Impl::~Impl()
|
||||
{
|
||||
}
|
||||
|
||||
Library::Impl::Impl(Library::Impl&& ) = default;
|
||||
Library::Impl& Library::Impl::operator=(Library::Impl&& ) = default;
|
||||
|
||||
unsigned int
|
||||
Library::Impl::getBookCount(const bool localBooks, const bool remoteBooks) const
|
||||
Library::getBookCount_not_protected(const bool localBooks, const bool remoteBooks) const
|
||||
{
|
||||
unsigned int result = 0;
|
||||
for (auto& pair: m_books) {
|
||||
@@ -136,50 +99,41 @@ Library::Impl::getBookCount(const bool localBooks, const bool remoteBooks) const
|
||||
|
||||
/* Constructor */
|
||||
Library::Library()
|
||||
: mp_impl(new Library::Impl)
|
||||
: mp_archiveCache(new ArchiveCache(std::max(getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", 1), 1))),
|
||||
mp_searcherCache(new SearcherCache(std::max(getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", 1), 1))),
|
||||
m_bookDB(new Xapian::WritableDatabase("", Xapian::DB_BACKEND_INMEMORY))
|
||||
{
|
||||
}
|
||||
|
||||
Library::Library(Library&& other)
|
||||
: mp_impl(std::move(other.mp_impl))
|
||||
{
|
||||
}
|
||||
|
||||
Library& Library::operator=(Library&& other)
|
||||
{
|
||||
mp_impl = std::move(other.mp_impl);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/* Destructor */
|
||||
Library::~Library() = default;
|
||||
|
||||
bool Library::addBook(const Book& book)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
++mp_impl->m_revision;
|
||||
++m_revision;
|
||||
/* Try to find it */
|
||||
updateBookDB(book);
|
||||
try {
|
||||
auto& oldbook = mp_impl->m_books.at(book.getId());
|
||||
auto& oldbook = m_books.at(book.getId());
|
||||
if ( ! booksReferToTheSameArchive(oldbook, book) ) {
|
||||
dropCache(book.getId());
|
||||
}
|
||||
oldbook.update(book); // XXX: This may have no effect if oldbook is readonly
|
||||
// XXX: Then m_bookDB will become out-of-sync with
|
||||
// XXX: the real contents of the library.
|
||||
oldbook.lastUpdatedRevision = mp_impl->m_revision;
|
||||
oldbook.lastUpdatedRevision = m_revision;
|
||||
return false;
|
||||
} catch (std::out_of_range&) {
|
||||
auto& newEntry = mp_impl->m_books[book.getId()];
|
||||
auto& newEntry = m_books[book.getId()];
|
||||
static_cast<Book&>(newEntry) = book;
|
||||
newEntry.lastUpdatedRevision = mp_impl->m_revision;
|
||||
size_t new_cache_size = static_cast<size_t>(std::ceil(mp_impl->getBookCount(true, true)*0.1));
|
||||
newEntry.lastUpdatedRevision = m_revision;
|
||||
size_t new_cache_size = static_cast<size_t>(std::ceil(getBookCount_not_protected(true, true)*0.1));
|
||||
if (getEnvVar<int>("KIWIX_ARCHIVE_CACHE_SIZE", -1) <= 0) {
|
||||
mp_impl->mp_archiveCache->setMaxSize(new_cache_size);
|
||||
mp_archiveCache->setMaxSize(new_cache_size);
|
||||
}
|
||||
if (getEnvVar<int>("KIWIX_SEARCHER_CACHE_SIZE", -1) <= 0) {
|
||||
mp_impl->mp_searcherCache->setMaxSize(new_cache_size);
|
||||
mp_searcherCache->setMaxSize(new_cache_size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -188,15 +142,15 @@ bool Library::addBook(const Book& book)
|
||||
void Library::addBookmark(const Bookmark& bookmark)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
mp_impl->m_bookmarks.push_back(bookmark);
|
||||
m_bookmarks.push_back(bookmark);
|
||||
}
|
||||
|
||||
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for(auto it=mp_impl->m_bookmarks.begin(); it!=mp_impl->m_bookmarks.end(); it++) {
|
||||
for(auto it=m_bookmarks.begin(); it!=m_bookmarks.end(); it++) {
|
||||
if (it->getBookId() == zimId && it->getUrl() == url) {
|
||||
mp_impl->m_bookmarks.erase(it);
|
||||
m_bookmarks.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -206,14 +160,14 @@ bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||
|
||||
void Library::dropCache(const std::string& id)
|
||||
{
|
||||
mp_impl->mp_archiveCache->drop(id);
|
||||
mp_impl->mp_searcherCache->drop(id);
|
||||
mp_archiveCache->drop(id);
|
||||
mp_searcherCache->drop(id);
|
||||
}
|
||||
|
||||
bool Library::removeBookById(const std::string& id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
mp_impl->m_bookDB.delete_document("Q" + id);
|
||||
m_bookDB->delete_document("Q" + id);
|
||||
dropCache(id);
|
||||
// We do not change the cache size here
|
||||
// Most of the time, the book is remove in case of library refresh, it is
|
||||
@@ -221,9 +175,9 @@ bool Library::removeBookById(const std::string& id)
|
||||
// Having a too big cache is not a problem here (or it would have been before)
|
||||
// (And setMaxSize doesn't actually reduce the cache size, extra cached items
|
||||
// will be removed in put or getOrPut).
|
||||
const bool bookWasRemoved = mp_impl->m_books.erase(id) == 1;
|
||||
const bool bookWasRemoved = m_books.erase(id) == 1;
|
||||
if ( bookWasRemoved ) {
|
||||
++mp_impl->m_revision;
|
||||
++m_revision;
|
||||
}
|
||||
return bookWasRemoved;
|
||||
}
|
||||
@@ -231,7 +185,7 @@ bool Library::removeBookById(const std::string& id)
|
||||
Library::Revision Library::getRevision() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return mp_impl->m_revision;
|
||||
return m_revision;
|
||||
}
|
||||
|
||||
uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
|
||||
@@ -239,7 +193,7 @@ uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
|
||||
BookIdCollection booksToRemove;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for ( const auto& entry : mp_impl->m_books) {
|
||||
for ( const auto& entry : m_books) {
|
||||
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
|
||||
booksToRemove.push_back(entry.first);
|
||||
}
|
||||
@@ -258,7 +212,7 @@ const Book& Library::getBookById(const std::string& id) const
|
||||
{
|
||||
// XXX: Doesn't make sense to lock this operation since it cannot
|
||||
// XXX: guarantee thread-safety because of its return type
|
||||
return mp_impl->m_books.at(id);
|
||||
return m_books.at(id);
|
||||
}
|
||||
|
||||
Book Library::getBookByIdThreadSafe(const std::string& id) const
|
||||
@@ -271,7 +225,7 @@ const Book& Library::getBookByPath(const std::string& path) const
|
||||
{
|
||||
// XXX: Doesn't make sense to lock this operation since it cannot
|
||||
// XXX: guarantee thread-safety because of its return type
|
||||
for(auto& it: mp_impl->m_books) {
|
||||
for(auto& it: m_books) {
|
||||
auto& book = it.second;
|
||||
if (book.getPath() == path)
|
||||
return book;
|
||||
@@ -284,7 +238,7 @@ const Book& Library::getBookByPath(const std::string& path) const
|
||||
std::shared_ptr<zim::Archive> Library::getArchiveById(const std::string& id)
|
||||
{
|
||||
try {
|
||||
return mp_impl->mp_archiveCache->getOrPut(id,
|
||||
return mp_archiveCache->getOrPut(id,
|
||||
[&](){
|
||||
auto book = getBookById(id);
|
||||
if (!book.isPathValid()) {
|
||||
@@ -301,7 +255,7 @@ std::shared_ptr<ZimSearcher> Library::getSearcherByIds(const BookIdSet& ids)
|
||||
{
|
||||
assert(!ids.empty());
|
||||
try {
|
||||
return mp_impl->mp_searcherCache->getOrPut(ids,
|
||||
return mp_searcherCache->getOrPut(ids,
|
||||
[&](){
|
||||
std::vector<zim::Archive> archives;
|
||||
for(auto& id:ids) {
|
||||
@@ -322,7 +276,7 @@ unsigned int Library::getBookCount(const bool localBooks,
|
||||
const bool remoteBooks) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return mp_impl->getBookCount(localBooks, remoteBooks);
|
||||
return getBookCount_not_protected(localBooks, remoteBooks);
|
||||
}
|
||||
|
||||
bool Library::writeToFile(const std::string& path) const
|
||||
@@ -353,7 +307,7 @@ Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) con
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
AttributeCounts propValueCounts;
|
||||
|
||||
for (const auto& pair: mp_impl->m_books) {
|
||||
for (const auto& pair: m_books) {
|
||||
const auto& book = pair.second;
|
||||
if (book.getOrigId().empty()) {
|
||||
propValueCounts[(book.*p)()] += 1;
|
||||
@@ -385,7 +339,7 @@ Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
AttributeCounts langsWithCounts;
|
||||
|
||||
for (const auto& pair: mp_impl->m_books) {
|
||||
for (const auto& pair: m_books) {
|
||||
const auto& book = pair.second;
|
||||
if (book.getOrigId().empty()) {
|
||||
for ( const auto& lang : book.getLanguages() ) {
|
||||
@@ -401,7 +355,7 @@ std::vector<std::string> Library::getBooksCategories() const
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::set<std::string> categories;
|
||||
|
||||
for (const auto& pair: mp_impl->m_books) {
|
||||
for (const auto& pair: m_books) {
|
||||
const auto& book = pair.second;
|
||||
const auto& c = book.getCategory();
|
||||
if ( !c.empty() ) {
|
||||
@@ -425,12 +379,12 @@ std::vector<std::string> Library::getBooksPublishers() const
|
||||
const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks) const
|
||||
{
|
||||
if (!onlyValidBookmarks) {
|
||||
return mp_impl->m_bookmarks;
|
||||
return m_bookmarks;
|
||||
}
|
||||
std::vector<kiwix::Bookmark> validBookmarks;
|
||||
auto booksId = getBooksIds();
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for(auto& bookmark:mp_impl->m_bookmarks) {
|
||||
for(auto& bookmark:m_bookmarks) {
|
||||
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
|
||||
validBookmarks.push_back(bookmark);
|
||||
}
|
||||
@@ -443,7 +397,7 @@ Library::BookIdCollection Library::getBooksIds() const
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
BookIdCollection bookIds;
|
||||
|
||||
for (auto& pair: mp_impl->m_books) {
|
||||
for (auto& pair: m_books) {
|
||||
bookIds.push_back(pair.first);
|
||||
}
|
||||
|
||||
@@ -482,7 +436,7 @@ void Library::updateBookDB(const Book& book)
|
||||
}
|
||||
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
|
||||
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
|
||||
indexer.index_text(normalizeText(book.getName()), 1, "XN");
|
||||
doc.add_term("XN"+normalizeText(book.getName()));
|
||||
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
|
||||
|
||||
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
|
||||
@@ -498,7 +452,7 @@ void Library::updateBookDB(const Book& book)
|
||||
|
||||
doc.set_data(book.getId());
|
||||
|
||||
mp_impl->m_bookDB.replace_document(idterm, doc);
|
||||
m_bookDB->replace_document(idterm, doc);
|
||||
}
|
||||
|
||||
namespace
|
||||
@@ -549,25 +503,30 @@ Xapian::Query nameQuery(const std::string& name)
|
||||
return Xapian::Query("XN" + normalizeText(name));
|
||||
}
|
||||
|
||||
Xapian::Query categoryQuery(const std::string& category)
|
||||
Xapian::Query multipleParamQuery(const std::string& commaSeparatedList, const std::string& prefix)
|
||||
{
|
||||
return Xapian::Query("XC" + normalizeText(category));
|
||||
Xapian::Query q;
|
||||
bool firstIteration = true;
|
||||
for ( const auto& elem : kiwix::split(commaSeparatedList, ",") ) {
|
||||
const Xapian::Query singleQuery(prefix + normalizeText(elem));
|
||||
if ( firstIteration ) {
|
||||
q = singleQuery;
|
||||
firstIteration = false;
|
||||
} else {
|
||||
q = Xapian::Query(Xapian::Query::OP_OR, q, singleQuery);
|
||||
}
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
Xapian::Query categoryQuery(const std::string& commaSeparatedCategoryList)
|
||||
{
|
||||
return multipleParamQuery(commaSeparatedCategoryList, "XC");
|
||||
}
|
||||
|
||||
Xapian::Query langQuery(const std::string& commaSeparatedLanguageList)
|
||||
{
|
||||
Xapian::Query q;
|
||||
bool firstIteration = true;
|
||||
for ( const auto& lang : kiwix::split(commaSeparatedLanguageList, ",") ) {
|
||||
const Xapian::Query singleLangQuery("L" + normalizeText(lang));
|
||||
if ( firstIteration ) {
|
||||
q = singleLangQuery;
|
||||
firstIteration = false;
|
||||
} else {
|
||||
q = Xapian::Query(Xapian::Query::OP_OR, q, singleLangQuery);
|
||||
}
|
||||
}
|
||||
return q;
|
||||
return multipleParamQuery(commaSeparatedLanguageList, "L");
|
||||
}
|
||||
|
||||
Xapian::Query publisherQuery(const std::string& publisher)
|
||||
@@ -642,9 +601,9 @@ Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
|
||||
BookIdCollection bookIds;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
Xapian::Enquire enquire(mp_impl->m_bookDB);
|
||||
Xapian::Enquire enquire(*m_bookDB);
|
||||
enquire.set_query(query);
|
||||
const auto results = enquire.get_mset(0, mp_impl->m_books.size());
|
||||
const auto results = enquire.get_mset(0, m_books.size());
|
||||
for ( auto it = results.begin(); it != results.end(); ++it ) {
|
||||
bookIds.push_back(it.get_document().get_data());
|
||||
}
|
||||
@@ -658,7 +617,7 @@ Library::BookIdCollection Library::filter(const Filter& filter) const
|
||||
const auto preliminaryResult = filterViaBookDB(filter);
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for(auto id : preliminaryResult) {
|
||||
if(filter.accept(mp_impl->m_books.at(id))) {
|
||||
if(filter.accept(m_books.at(id))) {
|
||||
result.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,65 +23,6 @@ void LibraryDumper::setOpenSearchInfo(int totalResults, int startIndex, int coun
|
||||
m_count = count;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::map<std::string, std::string> iso639_3 = {
|
||||
{"atj", "atikamekw"},
|
||||
{"azb", "آذربایجان دیلی"},
|
||||
{"bcl", "central bikol"},
|
||||
{"bgs", "tagabawa"},
|
||||
{"bxr", "буряад хэлэн"},
|
||||
{"cbk", "chavacano"},
|
||||
{"cdo", "閩東語"},
|
||||
{"dag", "Dagbani"},
|
||||
{"diq", "dimli"},
|
||||
{"dty", "डोटेली"},
|
||||
{"eml", "emiliân-rumagnōl"},
|
||||
{"fbs", "српскохрватски"},
|
||||
{"guw", "Gungbe"},
|
||||
{"hbs", "srpskohrvatski"},
|
||||
{"ido", "ido"},
|
||||
{"kbp", "kabɩyɛ"},
|
||||
{"kld", "Gamilaraay"},
|
||||
{"lbe", "лакку маз"},
|
||||
{"lbj", "ལ་དྭགས་སྐད་"},
|
||||
{"map", "Austronesian"},
|
||||
{"mhr", "марий йылме"},
|
||||
{"mnw", "ဘာသာမန်"},
|
||||
{"myn", "mayan"},
|
||||
{"nah", "nahuatl"},
|
||||
{"nai", "north American Indian"},
|
||||
{"nds", "plattdütsch"},
|
||||
{"nrm", "bhasa narom"},
|
||||
{"olo", "livvi"},
|
||||
{"pih", "Pitcairn-Norfolk"},
|
||||
{"pnb", "Western Panjabi"},
|
||||
{"rmr", "Caló"},
|
||||
{"rmy", "romani shib"},
|
||||
{"roa", "romance languages"},
|
||||
{"twi", "twi"},
|
||||
};
|
||||
|
||||
std::once_flag fillLanguagesFlag;
|
||||
|
||||
void fillLanguagesMap()
|
||||
{
|
||||
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
|
||||
const ICULanguageInfo lang(*icuLangPtr);
|
||||
iso639_3.insert({lang.iso3Code(), lang.selfName()});
|
||||
}
|
||||
}
|
||||
|
||||
std::string getLanguageSelfName(const std::string& lang) {
|
||||
const auto itr = iso639_3.find(lang);
|
||||
if (itr != iso639_3.end()) {
|
||||
return itr->second;
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
kainjow::mustache::list LibraryDumper::getCategoryData() const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
@@ -102,7 +43,6 @@ kainjow::mustache::list LibraryDumper::getLanguageData() const
|
||||
{
|
||||
const auto now = gen_date_str();
|
||||
kainjow::mustache::list languageData;
|
||||
std::call_once(fillLanguagesFlag, fillLanguagesMap);
|
||||
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
|
||||
const std::string languageCode = langAndBookCount.first;
|
||||
const int bookCount = langAndBookCount.second;
|
||||
|
||||
@@ -27,22 +27,12 @@
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct NoDelete
|
||||
{
|
||||
template<class T> void operator()(T*) {}
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// LibraryManipulator
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
LibraryManipulator::LibraryManipulator(Library* library)
|
||||
: library(*library)
|
||||
LibraryManipulator::LibraryManipulator(LibraryPtr library)
|
||||
: library(library)
|
||||
{}
|
||||
|
||||
LibraryManipulator::~LibraryManipulator()
|
||||
@@ -50,7 +40,7 @@ LibraryManipulator::~LibraryManipulator()
|
||||
|
||||
bool LibraryManipulator::addBookToLibrary(const Book& book)
|
||||
{
|
||||
const auto ret = library.addBook(book);
|
||||
const auto ret = library->addBook(book);
|
||||
if ( ret ) {
|
||||
bookWasAddedToLibrary(book);
|
||||
}
|
||||
@@ -59,13 +49,13 @@ bool LibraryManipulator::addBookToLibrary(const Book& book)
|
||||
|
||||
void LibraryManipulator::addBookmarkToLibrary(const Bookmark& bookmark)
|
||||
{
|
||||
library.addBookmark(bookmark);
|
||||
library->addBookmark(bookmark);
|
||||
bookmarkWasAddedToLibrary(bookmark);
|
||||
}
|
||||
|
||||
uint32_t LibraryManipulator::removeBooksNotUpdatedSince(Library::Revision rev)
|
||||
{
|
||||
const auto n = library.removeBooksNotUpdatedSince(rev);
|
||||
const auto n = library->removeBooksNotUpdatedSince(rev);
|
||||
if ( n != 0 ) {
|
||||
booksWereRemovedFromLibrary();
|
||||
}
|
||||
@@ -89,15 +79,15 @@ void LibraryManipulator::booksWereRemovedFromLibrary()
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* Constructor */
|
||||
Manager::Manager(LibraryManipulator* manipulator):
|
||||
Manager::Manager(LibraryManipulator manipulator):
|
||||
writableLibraryPath(""),
|
||||
manipulator(manipulator, NoDelete())
|
||||
manipulator(manipulator)
|
||||
{
|
||||
}
|
||||
|
||||
Manager::Manager(Library* library) :
|
||||
Manager::Manager(LibraryPtr library) :
|
||||
writableLibraryPath(""),
|
||||
manipulator(new LibraryManipulator(library))
|
||||
manipulator(LibraryManipulator(library))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -121,7 +111,7 @@ bool Manager::parseXmlDom(const pugi::xml_document& doc,
|
||||
if (!trustLibrary && !book.getPath().empty()) {
|
||||
this->readBookFromPath(book.getPath(), &book);
|
||||
}
|
||||
manipulator->addBookToLibrary(book);
|
||||
manipulator.addBookToLibrary(book);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -166,7 +156,7 @@ bool Manager::parseOpdsDom(const pugi::xml_document& doc, const std::string& url
|
||||
book.updateFromOpds(entryNode, urlHost);
|
||||
|
||||
/* Update the book properties with the new importer */
|
||||
manipulator->addBookToLibrary(book);
|
||||
manipulator.addBookToLibrary(book);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -241,7 +231,7 @@ std::string Manager::addBookFromPathAndGetId(const std::string& pathToOpen,
|
||||
|| (!book.getTitle().empty() && !book.getLanguages().empty()
|
||||
&& !book.getDate().empty())) {
|
||||
book.setUrl(url);
|
||||
manipulator->addBookToLibrary(book);
|
||||
manipulator.addBookToLibrary(book);
|
||||
return book.getId();
|
||||
}
|
||||
}
|
||||
@@ -296,7 +286,7 @@ bool Manager::readBookmarkFile(const std::string& path)
|
||||
|
||||
bookmark.updateFromXml(node);
|
||||
|
||||
manipulator->addBookmarkToLibrary(bookmark);
|
||||
manipulator.addBookmarkToLibrary(bookmark);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -304,7 +294,7 @@ bool Manager::readBookmarkFile(const std::string& path)
|
||||
|
||||
void Manager::reload(const Paths& paths)
|
||||
{
|
||||
const auto libRevision = manipulator->getLibrary().getRevision();
|
||||
const auto libRevision = manipulator.getLibrary()->getRevision();
|
||||
for (std::string path : paths) {
|
||||
if (!path.empty()) {
|
||||
if ( kiwix::isRelativePath(path) )
|
||||
@@ -316,7 +306,7 @@ void Manager::reload(const Paths& paths)
|
||||
}
|
||||
}
|
||||
|
||||
manipulator->removeBooksNotUpdatedSince(libRevision);
|
||||
manipulator.removeBooksNotUpdatedSince(libRevision);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ kiwix_sources = [
|
||||
'tools/regexTools.cpp',
|
||||
'tools/stringTools.cpp',
|
||||
'tools/networkTools.cpp',
|
||||
'tools/opdsParsingTools.cpp',
|
||||
'tools/languageTools.cpp',
|
||||
'tools/otherTools.cpp',
|
||||
'tools/archiveTools.cpp',
|
||||
'kiwixserve.cpp',
|
||||
|
||||
@@ -63,7 +63,7 @@ std::string HumanReadableNameMapper::getIdForName(const std::string& name) const
|
||||
// UpdatableNameMapper
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
UpdatableNameMapper::UpdatableNameMapper(Library& lib, bool withAlias)
|
||||
UpdatableNameMapper::UpdatableNameMapper(LibraryPtr lib, bool withAlias)
|
||||
: library(lib)
|
||||
, withAlias(withAlias)
|
||||
{
|
||||
@@ -72,7 +72,7 @@ UpdatableNameMapper::UpdatableNameMapper(Library& lib, bool withAlias)
|
||||
|
||||
void UpdatableNameMapper::update()
|
||||
{
|
||||
const auto newNameMapper = new HumanReadableNameMapper(library, withAlias);
|
||||
const auto newNameMapper = new HumanReadableNameMapper(*library, withAlias);
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
nameMapper.reset(newNameMapper);
|
||||
}
|
||||
|
||||
@@ -32,20 +32,46 @@
|
||||
#include "libkiwix-resources.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include "server/i18n.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
/* Constructor */
|
||||
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper,
|
||||
unsigned int start, unsigned int estimatedResultCount)
|
||||
: SearchRenderer(srs, mapper, nullptr, start, estimatedResultCount)
|
||||
{}
|
||||
namespace
|
||||
{
|
||||
|
||||
SearchRenderer::SearchRenderer(zim::SearchResultSet srs, NameMapper* mapper, Library* library,
|
||||
ParameterizedMessage searchResultsPageTitleMsg(const std::string& searchPattern)
|
||||
{
|
||||
return ParameterizedMessage("search-results-page-title",
|
||||
{{"SEARCH_PATTERN", searchPattern}}
|
||||
);
|
||||
}
|
||||
|
||||
ParameterizedMessage searchResultsPageHeaderMsg(const std::string& searchPattern,
|
||||
const kainjow::mustache::data& r)
|
||||
{
|
||||
if ( r.get("count")->string_value() == "0" ) {
|
||||
return ParameterizedMessage("empty-search-results-page-header",
|
||||
{{"SEARCH_PATTERN", searchPattern}}
|
||||
);
|
||||
} else {
|
||||
return ParameterizedMessage("search-results-page-header",
|
||||
{
|
||||
{"SEARCH_PATTERN", searchPattern},
|
||||
{"START", r.get("start")->string_value()},
|
||||
{"END", r.get("end") ->string_value()},
|
||||
{"COUNT", r.get("count")->string_value()},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
/* Constructor */
|
||||
SearchRenderer::SearchRenderer(zim::SearchResultSet srs,
|
||||
unsigned int start, unsigned int estimatedResultCount)
|
||||
: m_srs(srs),
|
||||
mp_nameMapper(mapper),
|
||||
mp_library(library),
|
||||
protocolPrefix("zim://"),
|
||||
searchProtocolPrefix("search://"),
|
||||
estimatedResultCount(estimatedResultCount),
|
||||
@@ -164,7 +190,7 @@ kainjow::mustache::data buildPagination(
|
||||
return pagination;
|
||||
}
|
||||
|
||||
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const NameMapper& nameMapper, const Library* library)
|
||||
{
|
||||
const std::string absPathPrefix = protocolPrefix;
|
||||
// Build the results list
|
||||
@@ -172,15 +198,25 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
for (auto it = m_srs.begin(); it != m_srs.end(); it++) {
|
||||
kainjow::mustache::data result;
|
||||
const std::string zim_id(it.getZimId());
|
||||
const auto path = mp_nameMapper->getNameForId(zim_id) + "/" + it.getPath();
|
||||
const auto path = nameMapper.getNameForId(zim_id) + "/" + it.getPath();
|
||||
result.set("title", it.getTitle());
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(path));
|
||||
result.set("snippet", it.getSnippet());
|
||||
if (mp_library) {
|
||||
result.set("bookTitle", mp_library->getBookById(zim_id).getTitle());
|
||||
if (library) {
|
||||
const std::string bookTitle = library->getBookById(zim_id).getTitle();
|
||||
const ParameterizedMessage bookInfoMsg("search-result-book-info",
|
||||
{{"BOOK_TITLE", bookTitle}}
|
||||
);
|
||||
result.set("bookInfo", bookInfoMsg.getText(userlang)); // for HTML
|
||||
result.set("bookTitle", bookTitle); // for XML
|
||||
}
|
||||
if (it.getWordCount() >= 0) {
|
||||
result.set("wordCount", kiwix::beautifyInteger(it.getWordCount()));
|
||||
const auto wordCountStr = kiwix::beautifyInteger(it.getWordCount());
|
||||
const ParameterizedMessage wordCountMsg("word-count",
|
||||
{{"COUNT", wordCountStr}}
|
||||
);
|
||||
result.set("wordCountInfo", wordCountMsg.getText(userlang)); // for HTML
|
||||
result.set("wordCount", wordCountStr); // for XML
|
||||
}
|
||||
|
||||
items.push_back(result);
|
||||
@@ -188,7 +224,6 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
kainjow::mustache::data results;
|
||||
results.set("items", items);
|
||||
results.set("count", kiwix::beautifyInteger(estimatedResultCount));
|
||||
results.set("hasResults", estimatedResultCount != 0);
|
||||
results.set("start", kiwix::beautifyInteger(resultStart));
|
||||
results.set("end", kiwix::beautifyInteger(std::min(resultStart+pageLength-1, estimatedResultCount)));
|
||||
|
||||
@@ -205,12 +240,15 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
searchBookQuery
|
||||
);
|
||||
|
||||
|
||||
kainjow::mustache::data allData;
|
||||
allData.set("searchProtocolPrefix", searchProtocolPrefix);
|
||||
allData.set("results", results);
|
||||
allData.set("pagination", pagination);
|
||||
allData.set("query", query);
|
||||
const auto pageHeaderMsg = searchResultsPageHeaderMsg(searchPattern, results);
|
||||
const kainjow::mustache::object allData{
|
||||
{"PAGE_TITLE", searchResultsPageTitleMsg(searchPattern).getText(userlang)},
|
||||
{"PAGE_HEADER", pageHeaderMsg.getText(userlang)},
|
||||
{"searchProtocolPrefix", searchProtocolPrefix},
|
||||
{"results", results},
|
||||
{"pagination", pagination},
|
||||
{"query", query},
|
||||
};
|
||||
|
||||
kainjow::mustache::mustache tmpl(tmpl_str);
|
||||
|
||||
@@ -222,14 +260,14 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str)
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string SearchRenderer::getHtml()
|
||||
std::string SearchRenderer::getHtml(const NameMapper& mapper, const Library* library)
|
||||
{
|
||||
return renderTemplate(RESOURCE::templates::search_result_html);
|
||||
return renderTemplate(RESOURCE::templates::search_result_html, mapper, library);
|
||||
}
|
||||
|
||||
std::string SearchRenderer::getXml()
|
||||
std::string SearchRenderer::getXml(const NameMapper& mapper, const Library* library)
|
||||
{
|
||||
return renderTemplate(RESOURCE::templates::search_result_xml);
|
||||
return renderTemplate(RESOURCE::templates::search_result_xml, mapper, library);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
Server::Server(Library* library, NameMapper* nameMapper) :
|
||||
Server::Server(LibraryPtr library, std::shared_ptr<NameMapper> nameMapper) :
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper),
|
||||
mp_server(nullptr)
|
||||
|
||||
@@ -112,8 +112,12 @@ std::string expandParameterizedString(const std::string& lang,
|
||||
const std::string& key,
|
||||
const Parameters& params)
|
||||
{
|
||||
kainjow::mustache::object mustacheParams;
|
||||
for( const auto& kv : params ) {
|
||||
mustacheParams[kv.first] = kv.second;
|
||||
}
|
||||
const std::string tmpl = getTranslatedString(lang, key);
|
||||
return render_template(tmpl, params);
|
||||
return render_template(tmpl, mustacheParams);
|
||||
}
|
||||
|
||||
} // namespace i18n
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#ifndef KIWIX_SERVER_I18N
|
||||
#define KIWIX_SERVER_I18N
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <mustache.hpp>
|
||||
|
||||
@@ -44,7 +45,7 @@ std::string getTranslatedString(const std::string& lang, const std::string& key)
|
||||
namespace i18n
|
||||
{
|
||||
|
||||
typedef kainjow::mustache::object Parameters;
|
||||
typedef std::map<std::string, std::string> Parameters;
|
||||
|
||||
std::string expandParameterizedString(const std::string& lang,
|
||||
const std::string& key,
|
||||
@@ -93,10 +94,10 @@ private:
|
||||
|
||||
} // namespace i18n
|
||||
|
||||
struct ParameterizedMessage
|
||||
class ParameterizedMessage
|
||||
{
|
||||
public: // types
|
||||
typedef kainjow::mustache::object Parameters;
|
||||
typedef i18n::Parameters Parameters;
|
||||
|
||||
public: // functions
|
||||
ParameterizedMessage(const std::string& msgId, const Parameters& params)
|
||||
@@ -106,11 +107,20 @@ public: // functions
|
||||
|
||||
std::string getText(const std::string& lang) const;
|
||||
|
||||
const std::string& getMsgId() const { return msgId; }
|
||||
const Parameters& getParams() const { return params; }
|
||||
|
||||
private: // data
|
||||
const std::string msgId;
|
||||
const Parameters params;
|
||||
};
|
||||
|
||||
inline ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
|
||||
{
|
||||
const ParameterizedMessage::Parameters noParams;
|
||||
return ParameterizedMessage(msgId, noParams);
|
||||
}
|
||||
|
||||
struct LangPreference
|
||||
{
|
||||
const std::string lang;
|
||||
|
||||
@@ -190,12 +190,6 @@ ParameterizedMessage tooManyBooksMsg(size_t nbBooks, size_t limit)
|
||||
);
|
||||
}
|
||||
|
||||
ParameterizedMessage nonParameterizedMessage(const std::string& msgId)
|
||||
{
|
||||
const ParameterizedMessage::Parameters noParams;
|
||||
return ParameterizedMessage(msgId, noParams);
|
||||
}
|
||||
|
||||
struct Error : public std::runtime_error {
|
||||
explicit Error(const ParameterizedMessage& message)
|
||||
: std::runtime_error("Error while handling request"),
|
||||
@@ -254,6 +248,11 @@ get_matching_if_none_match_etag(const RequestContext& r, const std::string& etag
|
||||
}
|
||||
}
|
||||
|
||||
struct NoDelete
|
||||
{
|
||||
template<class T> void operator()(T*) {}
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::pair<std::string, Library::BookIdSet> InternalServer::selectBooks(const RequestContext& request) const
|
||||
@@ -406,8 +405,8 @@ public:
|
||||
};
|
||||
|
||||
|
||||
InternalServer::InternalServer(Library* library,
|
||||
NameMapper* nameMapper,
|
||||
InternalServer::InternalServer(LibraryPtr library,
|
||||
std::shared_ptr<NameMapper> nameMapper,
|
||||
std::string addr,
|
||||
int port,
|
||||
std::string root,
|
||||
@@ -433,7 +432,7 @@ InternalServer::InternalServer(Library* library,
|
||||
m_ipConnectionLimit(ipConnectionLimit),
|
||||
mp_daemon(nullptr),
|
||||
mp_library(library),
|
||||
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper),
|
||||
mp_nameMapper(nameMapper ? nameMapper : std::shared_ptr<NameMapper>(&defaultNameMapper, NoDelete())),
|
||||
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
|
||||
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
|
||||
m_customizedResources(new CustomizedResources)
|
||||
@@ -514,6 +513,19 @@ static MHD_Result staticHandlerCallback(void* cls,
|
||||
cont_cls);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
MHD_Result add_name_value_pair(void *nvp, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
{
|
||||
auto& nameValuePairs = *reinterpret_cast<RequestContext::NameValuePairs*>(nvp);
|
||||
nameValuePairs.push_back({key, value});
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
const char* fullUrl,
|
||||
const char* method,
|
||||
@@ -530,7 +542,10 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
}
|
||||
|
||||
const auto url = fullURL2LocalURL(fullUrl, m_rootPrefixOfDecodedURL);
|
||||
RequestContext request(connection, m_root, url, method, version);
|
||||
RequestContext::NameValuePairs headers, queryArgs;
|
||||
MHD_get_connection_values(connection, MHD_HEADER_KIND, add_name_value_pair, &headers);
|
||||
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, add_name_value_pair, &queryArgs);
|
||||
RequestContext request(m_root, url, method, version, headers, queryArgs);
|
||||
|
||||
if (m_verbose.load() ) {
|
||||
request.print_debug_info();
|
||||
@@ -559,7 +574,7 @@ MHD_Result InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||
response->set_etag_body(getLibraryId());
|
||||
}
|
||||
|
||||
auto ret = response->send(request, connection);
|
||||
auto ret = response->send(request, m_verbose.load(), connection);
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
|
||||
if (m_verbose.load()) {
|
||||
@@ -588,20 +603,19 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
{
|
||||
try {
|
||||
if (! request.is_valid_url()) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if ( request.get_url() == "" ) {
|
||||
// Redirect /ROOT_LOCATION to /ROOT_LOCATION/ (note the added slash)
|
||||
// so that relative URLs are resolved correctly
|
||||
const std::string query = getSearchComponent(request);
|
||||
return Response::build_redirect(*this, m_root + "/" + query);
|
||||
return Response::build_redirect(m_root + "/" + query);
|
||||
}
|
||||
|
||||
const ETag etag = get_matching_if_none_match_etag(request, getLibraryId());
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
return Response::build_304(etag);
|
||||
|
||||
const auto url = request.get_url();
|
||||
if ( isLocallyCustomizedResource(url) )
|
||||
@@ -642,15 +656,15 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||
|
||||
const std::string contentUrl = m_root + "/content" + urlEncode(url);
|
||||
const std::string query = getSearchComponent(request);
|
||||
return Response::build_redirect(*this, contentUrl + query);
|
||||
return Response::build_redirect(contentUrl + query);
|
||||
} catch (std::exception& e) {
|
||||
fprintf(stderr, "===== Unhandled error : %s\n", e.what());
|
||||
return HTTP500Response(*this, request)
|
||||
+ e.what();
|
||||
return HTTP500Response(request)
|
||||
+ ParameterizedMessage("non-translated-text", {{"MSG", e.what()}});
|
||||
} catch (...) {
|
||||
fprintf(stderr, "===== Unhandled unknown error\n");
|
||||
return HTTP500Response(*this, request)
|
||||
+ "Unknown error";
|
||||
return HTTP500Response(request)
|
||||
+ nonParameterizedMessage("unknown-error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,7 +677,7 @@ MustacheData InternalServer::get_default_data() const
|
||||
|
||||
std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
|
||||
{
|
||||
return ContentResponse::build(*this, m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
|
||||
return ContentResponse::build(m_indexTemplateString, get_default_data(), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -692,8 +706,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
}
|
||||
|
||||
if ( startsWith(request.get_url(), "/suggest/") ) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
std::string bookName, bookId;
|
||||
@@ -707,7 +720,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
}
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
return HTTP404Response(request)
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
@@ -742,7 +755,7 @@ std::unique_ptr<Response> InternalServer::handle_suggest(const RequestContext& r
|
||||
results.addFTSearchSuggestion(request.get_user_language(), queryString);
|
||||
}
|
||||
|
||||
return ContentResponse::build(*this, results.getJSON(), "application/json; charset=utf-8");
|
||||
return ContentResponse::build(results.getJSON(), "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestContext& request)
|
||||
@@ -756,7 +769,7 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
|
||||
{"enable_link_blocking", m_blockExternalLinks ? "true" : "false" },
|
||||
{"enable_library_button", m_withLibraryButton ? "true" : "false" }
|
||||
};
|
||||
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
|
||||
return ContentResponse::build(RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
|
||||
}
|
||||
|
||||
std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const
|
||||
@@ -786,7 +799,7 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
|
||||
{
|
||||
const auto url = request.get_url();
|
||||
const auto urlParts = kiwix::split(url, "/", true, false);
|
||||
HTMLDumper htmlDumper(mp_library, mp_nameMapper);
|
||||
HTMLDumper htmlDumper(mp_library.get(), mp_nameMapper.get());
|
||||
htmlDumper.setRootLocation(m_root);
|
||||
htmlDumper.setLibraryId(getLibraryId());
|
||||
auto userLang = request.get_user_language();
|
||||
@@ -811,19 +824,13 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
|
||||
const auto bookId = mp_nameMapper->getIdForName(urlParts[2]);
|
||||
content = getNoJSDownloadPageHTML(bookId, userLang);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
} else {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
content,
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
return ContentResponse::build(content, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
namespace
|
||||
@@ -861,14 +868,12 @@ std::unique_ptr<Response> InternalServer::handle_skin(const RequestContext& requ
|
||||
try {
|
||||
const auto accessType = staticResourceAccessType(request, resourceCacheId);
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
getResource(resourceName),
|
||||
getMimeTypeForFile(resourceName));
|
||||
response->set_kind(accessType);
|
||||
return std::move(response);
|
||||
} catch (const ResourceNotFound& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,20 +886,17 @@ std::unique_ptr<Response> InternalServer::handle_search(const RequestContext& re
|
||||
if ( startsWith(request.get_url(), "/search/") ) {
|
||||
if (request.get_url() == "/search/searchdescription.xml") {
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
RESOURCE::ft_opensearchdescription_xml,
|
||||
get_default_data(),
|
||||
"application/opensearchdescription+xml");
|
||||
}
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
try {
|
||||
return handle_search_request(request);
|
||||
} catch (const Error& e) {
|
||||
return HTTP400Response(*this, request)
|
||||
+ invalidUrlMsg
|
||||
return HTTP400Response(request)
|
||||
+ e.message();
|
||||
}
|
||||
}
|
||||
@@ -936,10 +938,11 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
|
||||
// Searcher->search will throw a runtime error if there is no valid xapian database to do the search.
|
||||
// (in case of zim file not containing a index)
|
||||
const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css);
|
||||
HTTPErrorResponse response(*this, request, MHD_HTTP_NOT_FOUND,
|
||||
HTTPErrorResponse response(request, MHD_HTTP_NOT_FOUND,
|
||||
"fulltext-search-unavailable",
|
||||
"404-page-heading",
|
||||
cssUrl);
|
||||
cssUrl,
|
||||
/*includeKiwixResponseData=*/true);
|
||||
response += nonParameterizedMessage("no-search-results");
|
||||
// XXX: Now this has to be handled by the iframe-based viewer which
|
||||
// XXX: has to resolve if the book selection resulted in a single book.
|
||||
@@ -957,17 +960,24 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
|
||||
const auto pageLength = getSearchPageSize(request);
|
||||
|
||||
/* Get the results */
|
||||
SearchRenderer renderer(search->getResults(start-1, pageLength), mp_nameMapper, mp_library, start,
|
||||
SearchRenderer renderer(search->getResults(start-1, pageLength), start,
|
||||
search->getEstimatedMatches());
|
||||
renderer.setSearchPattern(searchInfo.pattern);
|
||||
renderer.setSearchBookQuery(searchInfo.bookFilterQuery);
|
||||
renderer.setProtocolPrefix(m_root + "/content/");
|
||||
renderer.setSearchProtocolPrefix(m_root + "/search");
|
||||
renderer.setPageLength(pageLength);
|
||||
renderer.setUserLang(request.get_user_language());
|
||||
if (request.get_requested_format() == "xml") {
|
||||
return ContentResponse::build(*this, renderer.getXml(), "application/rss+xml; charset=utf-8");
|
||||
return ContentResponse::build(
|
||||
renderer.getXml(*mp_nameMapper, mp_library.get()),
|
||||
"application/rss+xml; charset=utf-8"
|
||||
);
|
||||
}
|
||||
auto response = ContentResponse::build(*this, renderer.getHtml(), "text/html; charset=utf-8");
|
||||
auto response = ContentResponse::build(
|
||||
renderer.getHtml(*mp_nameMapper, mp_library.get()),
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
// XXX: Now this has to be handled by the iframe-based viewer which
|
||||
// XXX: has to resolve if the book selection resulted in a single book.
|
||||
/*
|
||||
@@ -987,8 +997,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
}
|
||||
|
||||
if ( startsWith(request.get_url(), "/random/") ) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
std::string bookName;
|
||||
@@ -1002,7 +1011,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
}
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
return HTTP404Response(request)
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
@@ -1010,7 +1019,7 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
|
||||
auto entry = archive->getRandomEntry();
|
||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||
} catch(zim::EntryNotFound& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
return HTTP404Response(request)
|
||||
+ nonParameterizedMessage("random-article-failure");
|
||||
}
|
||||
}
|
||||
@@ -1023,13 +1032,12 @@ std::unique_ptr<Response> InternalServer::handle_captured_external(const Request
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (source.empty()) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
auto data = get_default_data();
|
||||
data.set("source", source);
|
||||
return ContentResponse::build(*this, RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
|
||||
return ContentResponse::build(RESOURCE::templates::captured_external_html, data, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& request)
|
||||
@@ -1042,8 +1050,7 @@ std::unique_ptr<Response> InternalServer::handle_catch(const RequestContext& req
|
||||
return handle_captured_external(request);
|
||||
}
|
||||
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
@@ -1103,7 +1110,7 @@ InternalServer::build_redirect(const std::string& bookName, const zim::Item& ite
|
||||
{
|
||||
const auto contentPath = "/content/" + bookName + "/" + item.getPath();
|
||||
const auto url = m_root + kiwix::urlEncode(contentPath);
|
||||
return Response::build_redirect(*this, url);
|
||||
return Response::build_redirect(url);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& request)
|
||||
@@ -1127,15 +1134,14 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
|
||||
if (archive == nullptr) {
|
||||
const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern);
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
}
|
||||
|
||||
const std::string archiveUuid(archive->getUuid());
|
||||
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
return Response::build_304(etag);
|
||||
|
||||
auto urlStr = url.substr(prefixLength + bookName.size());
|
||||
if (urlStr[0] == '/') {
|
||||
@@ -1154,7 +1160,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
// '-' namespaces, in which case that resource is returned instead.
|
||||
return build_redirect(bookName, getFinalItem(*archive, entry));
|
||||
}
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
auto response = ItemResponse::build(request, entry.getItem());
|
||||
response->set_etag_body(archiveUuid);
|
||||
|
||||
if ( !startsWith(entry.getItem().getMimetype(), "application/pdf") ) {
|
||||
@@ -1175,8 +1181,7 @@ std::unique_ptr<Response> InternalServer::handle_content(const RequestContext& r
|
||||
printf("Failed to find %s\n", urlStr.c_str());
|
||||
|
||||
std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern);
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ suggestSearchMsg(searchURL, kiwix::urlDecode(pattern));
|
||||
}
|
||||
}
|
||||
@@ -1194,13 +1199,11 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
bookName = request.get_url_part(1);
|
||||
kind = request.get_url_part(2);
|
||||
} catch (const std::out_of_range& e) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (kind != "meta" && kind!= "content") {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ invalidRawAccessMsg(kind);
|
||||
}
|
||||
|
||||
@@ -1211,15 +1214,14 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
} catch (const std::out_of_range& e) {}
|
||||
|
||||
if (archive == nullptr) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ noSuchBookErrorMsg(bookName);
|
||||
}
|
||||
|
||||
const std::string archiveUuid(archive->getUuid());
|
||||
const ETag etag = get_matching_if_none_match_etag(request, archiveUuid);
|
||||
if ( etag )
|
||||
return Response::build_304(*this, etag);
|
||||
return Response::build_304(etag);
|
||||
|
||||
// Remove the beggining of the path:
|
||||
// /raw/<bookName>/<kind>/foo
|
||||
@@ -1230,7 +1232,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
try {
|
||||
if (kind == "meta") {
|
||||
auto item = archive->getMetadataItem(itemPath);
|
||||
auto response = ItemResponse::build(*this, request, item);
|
||||
auto response = ItemResponse::build(request, item);
|
||||
response->set_etag_body(archiveUuid);
|
||||
return response;
|
||||
} else {
|
||||
@@ -1238,7 +1240,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
if (entry.isRedirect()) {
|
||||
return build_redirect(bookName, entry.getItem(true));
|
||||
}
|
||||
auto response = ItemResponse::build(*this, request, entry.getItem());
|
||||
auto response = ItemResponse::build(request, entry.getItem());
|
||||
response->set_etag_body(archiveUuid);
|
||||
return response;
|
||||
}
|
||||
@@ -1246,8 +1248,7 @@ std::unique_ptr<Response> InternalServer::handle_raw(const RequestContext& reque
|
||||
if (m_verbose.load()) {
|
||||
printf("Failed to find %s\n", itemPath.c_str());
|
||||
}
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg
|
||||
return UrlNotFoundResponse(request)
|
||||
+ rawEntryNotFoundMsg(kind, itemPath);
|
||||
}
|
||||
}
|
||||
@@ -1272,12 +1273,10 @@ std::unique_ptr<Response> InternalServer::handle_locally_customized_resource(con
|
||||
|
||||
auto byteRange = request.get_range().resolve(resourceData.size());
|
||||
if (byteRange.kind() != ByteRange::RESOLVED_FULL_CONTENT) {
|
||||
return Response::build_416(*this, resourceData.size());
|
||||
return Response::build_416(resourceData.size());
|
||||
}
|
||||
|
||||
return ContentResponse::build(*this,
|
||||
resourceData,
|
||||
crd.mimeType);
|
||||
return ContentResponse::build(resourceData, crd.mimeType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ class OPDSDumper;
|
||||
|
||||
class InternalServer {
|
||||
public:
|
||||
InternalServer(Library* library,
|
||||
NameMapper* nameMapper,
|
||||
InternalServer(LibraryPtr library,
|
||||
std::shared_ptr<NameMapper> nameMapper,
|
||||
std::string addr,
|
||||
int port,
|
||||
std::string root,
|
||||
@@ -178,8 +178,8 @@ class InternalServer {
|
||||
int m_ipConnectionLimit;
|
||||
struct MHD_Daemon* mp_daemon;
|
||||
|
||||
Library* mp_library;
|
||||
NameMapper* mp_nameMapper;
|
||||
LibraryPtr mp_library;
|
||||
std::shared_ptr<NameMapper> mp_nameMapper;
|
||||
|
||||
SearchCache searchCache;
|
||||
SuggestionSearcherCache suggestionSearcherCache;
|
||||
@@ -188,10 +188,6 @@ class InternalServer {
|
||||
|
||||
class CustomizedResources;
|
||||
std::unique_ptr<CustomizedResources> m_customizedResources;
|
||||
|
||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype);
|
||||
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
host = request.get_header("Host");
|
||||
url = request.get_url_part(1);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (url == "v2") {
|
||||
@@ -72,17 +71,16 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
}
|
||||
|
||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (url == "searchdescription.xml") {
|
||||
auto response = ContentResponse::build(*this, RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
|
||||
auto response = ContentResponse::build(RESOURCE::opensearchdescription_xml, get_default_data(), "application/opensearchdescription+xml");
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
@@ -95,7 +93,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
}
|
||||
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
|
||||
opdsMimeType[OPDS_ACQUISITION_FEED]);
|
||||
return std::move(response);
|
||||
@@ -111,15 +108,14 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
|
||||
try {
|
||||
url = request.get_url_part(2);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
if (url == "root.xml") {
|
||||
return handle_catalog_v2_root(request);
|
||||
} else if (url == "searchdescription.xml") {
|
||||
const std::string endpoint_root = m_root + "/catalog/v2";
|
||||
return ContentResponse::build(*this,
|
||||
return ContentResponse::build(
|
||||
RESOURCE::catalog_v2_searchdescription_xml,
|
||||
kainjow::mustache::object({{"endpoint_root", endpoint_root}}),
|
||||
"application/opensearchdescription+xml"
|
||||
@@ -138,8 +134,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext
|
||||
} else if (url == "illustration") {
|
||||
return handle_catalog_v2_illustration(request);
|
||||
} else {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +142,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
|
||||
{
|
||||
const std::string libraryId = getLibraryId();
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
RESOURCE::templates::catalog_v2_root_xml,
|
||||
kainjow::mustache::object{
|
||||
{"date", gen_date_str()},
|
||||
@@ -164,13 +158,12 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
const auto bookIds = search_catalog(request, opdsDumper);
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
opdsMimeType[OPDS_ACQUISITION_FEED]
|
||||
);
|
||||
@@ -181,16 +174,14 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
try {
|
||||
mp_library->getBookById(entryId);
|
||||
} catch (const std::out_of_range&) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
opdsMimeType[OPDS_ENTRY]
|
||||
);
|
||||
@@ -198,11 +189,10 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.categoriesOPDSFeed(),
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
@@ -210,11 +200,10 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library, mp_nameMapper);
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.languagesOPDSFeed(),
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
@@ -228,13 +217,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_illustration(const R
|
||||
auto size = request.get_argument<unsigned int>("size");
|
||||
auto illustration = book.getIllustration(size);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
illustration->getData(),
|
||||
illustration->mimeType
|
||||
);
|
||||
} catch(...) {
|
||||
return HTTP404Response(*this, request)
|
||||
+ urlNotFoundMsg;
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,12 @@ RequestMethod str2RequestMethod(const std::string& method) {
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
const std::string& _rootLocation, // URI-encoded
|
||||
RequestContext::RequestContext(const std::string& _rootLocation, // URI-encoded
|
||||
const std::string& unrootedUrl, // URI-decoded
|
||||
const std::string& _method,
|
||||
const std::string& version) :
|
||||
const std::string& version,
|
||||
const NameValuePairs& headers,
|
||||
const NameValuePairs& queryArgs) :
|
||||
rootLocation(_rootLocation),
|
||||
url(unrootedUrl),
|
||||
method(str2RequestMethod(_method)),
|
||||
@@ -64,9 +65,13 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
acceptEncodingGzip(false),
|
||||
byteRange_()
|
||||
{
|
||||
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
|
||||
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
|
||||
MHD_get_connection_values(connection, MHD_COOKIE_KIND, &RequestContext::fill_cookie, this);
|
||||
for ( const auto& kv : headers ) {
|
||||
add_header(kv.first, kv.second);
|
||||
}
|
||||
|
||||
for ( const auto& kv : queryArgs ) {
|
||||
add_argument(kv.first, kv.second);
|
||||
}
|
||||
|
||||
try {
|
||||
acceptEncodingGzip =
|
||||
@@ -83,18 +88,14 @@ RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||
RequestContext::~RequestContext()
|
||||
{}
|
||||
|
||||
MHD_Result RequestContext::fill_header(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char *value)
|
||||
void RequestContext::add_header(const char *key, const char *value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->headers[lcAll(key)] = value;
|
||||
return MHD_YES;
|
||||
this->headers[lcAll(key)] = value;
|
||||
}
|
||||
|
||||
MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
void RequestContext::add_argument(const char *key, const char* value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
RequestContext *_this = this;
|
||||
_this->arguments[key].push_back(value == nullptr ? "" : value);
|
||||
if ( ! _this->queryString.empty() ) {
|
||||
_this->queryString += "&";
|
||||
@@ -104,15 +105,6 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||
_this->queryString += "=";
|
||||
_this->queryString += urlEncode(value);
|
||||
}
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
MHD_Result RequestContext::fill_cookie(void *__this, enum MHD_ValueKind kind,
|
||||
const char *key, const char* value)
|
||||
{
|
||||
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||
_this->cookies[key] = value == nullptr ? "" : value;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
void RequestContext::print_debug_info() const {
|
||||
@@ -202,21 +194,12 @@ std::string RequestContext::get_user_language() const
|
||||
return userlang.lang;
|
||||
}
|
||||
|
||||
bool RequestContext::user_language_comes_from_cookie() const
|
||||
{
|
||||
return userlang.selectedBy == UserLanguage::SelectorKind::COOKIE;
|
||||
}
|
||||
|
||||
RequestContext::UserLanguage RequestContext::determine_user_language() const
|
||||
{
|
||||
try {
|
||||
return {UserLanguage::SelectorKind::QUERY_PARAM, get_argument("userlang")};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
try {
|
||||
return {UserLanguage::SelectorKind::COOKIE, cookies.at("userlang")};
|
||||
} catch(const std::out_of_range&) {}
|
||||
|
||||
try {
|
||||
const std::string acceptLanguage = get_header("Accept-Language");
|
||||
const auto userLangPrefs = parseUserLanguagePreferences(acceptLanguage);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include <stdexcept>
|
||||
|
||||
#include "byte_range.h"
|
||||
#include "tools/stringTools.h"
|
||||
#include "../tools/stringTools.h"
|
||||
|
||||
extern "C" {
|
||||
#include "microhttpd_wrapper.h"
|
||||
@@ -55,12 +55,17 @@ class IndexError: public std::runtime_error {};
|
||||
|
||||
|
||||
class RequestContext {
|
||||
public: // types
|
||||
typedef std::vector<std::pair<const char*, const char*>> NameValuePairs;
|
||||
|
||||
public: // functions
|
||||
RequestContext(struct MHD_Connection* connection,
|
||||
const std::string& rootLocation, // URI-encoded
|
||||
RequestContext(const std::string& rootLocation, // URI-encoded
|
||||
const std::string& unrootedUrl, // URI-decoded
|
||||
const std::string& method,
|
||||
const std::string& version);
|
||||
const std::string& version,
|
||||
const NameValuePairs& headers,
|
||||
const NameValuePairs& queryArgs);
|
||||
|
||||
~RequestContext();
|
||||
|
||||
void print_debug_info() const;
|
||||
@@ -119,15 +124,12 @@ class RequestContext {
|
||||
std::string get_user_language() const;
|
||||
std::string get_requested_format() const;
|
||||
|
||||
bool user_language_comes_from_cookie() const;
|
||||
|
||||
private: // types
|
||||
struct UserLanguage
|
||||
{
|
||||
enum SelectorKind
|
||||
{
|
||||
QUERY_PARAM,
|
||||
COOKIE,
|
||||
ACCEPT_LANGUAGE_HEADER,
|
||||
DEFAULT
|
||||
};
|
||||
@@ -148,16 +150,14 @@ class RequestContext {
|
||||
ByteRange byteRange_;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::map<std::string, std::vector<std::string>> arguments;
|
||||
std::map<std::string, std::string> cookies;
|
||||
std::string queryString;
|
||||
UserLanguage userlang;
|
||||
|
||||
private: // functions
|
||||
UserLanguage determine_user_language() const;
|
||||
|
||||
static MHD_Result fill_header(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_cookie(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
static MHD_Result fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
|
||||
void add_header(const char* name, const char* value);
|
||||
void add_argument(const char* name, const char* value);
|
||||
};
|
||||
|
||||
template<> std::string RequestContext::get_argument(const std::string& name) const;
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
#include <zlib.h>
|
||||
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
|
||||
// This is somehow a magic value.
|
||||
// If this value is too small, we will compress (and lost cpu time) too much
|
||||
@@ -47,6 +50,8 @@ namespace kiwix {
|
||||
|
||||
namespace
|
||||
{
|
||||
typedef kainjow::mustache::data MustacheData;
|
||||
|
||||
// some utilities
|
||||
|
||||
std::string get_mime_type(const zim::Item& item)
|
||||
@@ -119,9 +124,8 @@ const char* getCacheControlHeader(Response::Kind k)
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
Response::Response(bool verbose)
|
||||
: m_verbose(verbose),
|
||||
m_returnCode(MHD_HTTP_OK)
|
||||
Response::Response()
|
||||
: m_returnCode(MHD_HTTP_OK)
|
||||
{
|
||||
add_header(MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
}
|
||||
@@ -133,14 +137,14 @@ void Response::set_kind(Kind k)
|
||||
m_etag.set_option(ETag::ZIM_CONTENT);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build(const InternalServer& server)
|
||||
std::unique_ptr<Response> Response::build()
|
||||
{
|
||||
return std::unique_ptr<Response>(new Response(server.m_verbose.load()));
|
||||
return std::make_unique<Response>();
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_304(const InternalServer& server, const ETag& etag)
|
||||
std::unique_ptr<Response> Response::build_304(const ETag& etag)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
auto response = Response::build();
|
||||
response->set_code(MHD_HTTP_NOT_MODIFIED);
|
||||
response->m_etag = etag;
|
||||
if ( etag.get_option(ETag::ZIM_CONTENT) ) {
|
||||
@@ -152,67 +156,260 @@ std::unique_ptr<Response> Response::build_304(const InternalServer& server, cons
|
||||
return response;
|
||||
}
|
||||
|
||||
const UrlNotFoundMsg urlNotFoundMsg;
|
||||
const InvalidUrlMsg invalidUrlMsg;
|
||||
|
||||
std::string ContentResponseBlueprint::getMessage(const std::string& msgId) const
|
||||
namespace
|
||||
{
|
||||
return getTranslatedString(m_request.get_user_language(), msgId);
|
||||
|
||||
// This class was introduced in order to work around the missing support
|
||||
// for std::variant (and std::optional) under some of the current build
|
||||
// platforms.
|
||||
template<class T>
|
||||
class Optional
|
||||
{
|
||||
public: // functions
|
||||
Optional() {}
|
||||
Optional(const T& t) : ptr(new T(t)) {}
|
||||
Optional(const Optional& o) : ptr(o.has_value() ? new T(*o) : nullptr) {}
|
||||
Optional(Optional&& o) : ptr(std::move(o.ptr)) {}
|
||||
|
||||
Optional& operator=(const Optional& o)
|
||||
{
|
||||
*this = Optional(o);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Optional& operator=(Optional&& o)
|
||||
{
|
||||
ptr = std::move(o.ptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool has_value() const { return ptr.get() != nullptr; }
|
||||
const T& operator*() const { return *ptr; }
|
||||
T& operator*() { return *ptr; }
|
||||
|
||||
private: // data
|
||||
std::unique_ptr<T> ptr;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
class ContentResponseBlueprint::Data
|
||||
{
|
||||
public:
|
||||
typedef std::list<Data> List;
|
||||
typedef std::map<std::string, Data> Object;
|
||||
|
||||
private:
|
||||
// std::variant<std::string, bool, List, Object> data;
|
||||
// XXX: libkiwix is compiled on platforms where std::variant
|
||||
// XXX: is not yet supported. Hence this hack. Only one
|
||||
// XXX: of the below data members is expected to contain a value.
|
||||
Optional<std::string> m_stringValue;
|
||||
Optional<bool> m_boolValue;
|
||||
Optional<List> m_listValue;
|
||||
Optional<Object> m_objectValue;
|
||||
|
||||
public:
|
||||
Data() {}
|
||||
Data(const std::string& s) : m_stringValue(s) {}
|
||||
Data(bool b) : m_boolValue(b) {}
|
||||
Data(const List& l) : m_listValue(l) {}
|
||||
Data(const Object& o) : m_objectValue(o) {}
|
||||
|
||||
MustacheData toMustache(const std::string& lang) const;
|
||||
|
||||
Data& operator[](const std::string& key)
|
||||
{
|
||||
return (*m_objectValue)[key];
|
||||
}
|
||||
|
||||
void push_back(const Data& d) { (*m_listValue).push_back(d); }
|
||||
|
||||
static Data onlyAsNonEmptyValue(const std::string& s)
|
||||
{
|
||||
return s.empty() ? Data(false) : Data(s);
|
||||
}
|
||||
|
||||
static Data from(const ParameterizedMessage& pmsg)
|
||||
{
|
||||
Object obj;
|
||||
for(const auto& kv : pmsg.getParams()) {
|
||||
obj[kv.first] = kv.second;
|
||||
}
|
||||
return Object{
|
||||
{ "msgid", pmsg.getMsgId() },
|
||||
{ "params", Data(obj) }
|
||||
};
|
||||
}
|
||||
|
||||
std::string asJSON() const;
|
||||
void dumpJSON(std::ostream& os) const;
|
||||
|
||||
private:
|
||||
bool isString() const { return m_stringValue.has_value(); }
|
||||
bool isList() const { return m_listValue.has_value(); }
|
||||
bool isObject() const { return m_objectValue.has_value(); }
|
||||
|
||||
const std::string& stringValue() const { return *m_stringValue; }
|
||||
bool boolValue() const { return *m_boolValue; }
|
||||
const List& listValue() const { return *m_listValue; }
|
||||
const Object& objectValue() const { return *m_objectValue; }
|
||||
|
||||
const Data* get(const std::string& key) const
|
||||
{
|
||||
if ( !isObject() )
|
||||
return nullptr;
|
||||
|
||||
const auto& obj = objectValue();
|
||||
const auto it = obj.find(key);
|
||||
return it != obj.end() ? &it->second : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
MustacheData ContentResponseBlueprint::Data::toMustache(const std::string& lang) const
|
||||
{
|
||||
if ( this->isList() ) {
|
||||
kainjow::mustache::list l;
|
||||
for ( const auto& x : this->listValue() ) {
|
||||
l.push_back(x.toMustache(lang));
|
||||
}
|
||||
return l;
|
||||
} else if ( this->isObject() ) {
|
||||
const Data* msgId = this->get("msgid");
|
||||
const Data* msgParams = this->get("params");
|
||||
if ( msgId && msgId->isString() && msgParams && msgParams->isObject() ) {
|
||||
std::map<std::string, std::string> params;
|
||||
for(const auto& kv : msgParams->objectValue()) {
|
||||
params[kv.first] = kv.second.stringValue();
|
||||
}
|
||||
const ParameterizedMessage msg(msgId->stringValue(), ParameterizedMessage::Parameters(params));
|
||||
return msg.getText(lang);
|
||||
} else {
|
||||
kainjow::mustache::object o;
|
||||
for ( const auto& kv : this->objectValue() ) {
|
||||
o[kv.first] = kv.second.toMustache(lang);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
} else if ( this->isString() ) {
|
||||
return this->stringValue();
|
||||
} else {
|
||||
return this->boolValue();
|
||||
}
|
||||
}
|
||||
|
||||
void ContentResponseBlueprint::Data::dumpJSON(std::ostream& os) const
|
||||
{
|
||||
if ( this->isString() ) {
|
||||
os << '"' << escapeForJSON(this->stringValue()) << '"';
|
||||
} else if ( this->isList() ) {
|
||||
const char * sep = " ";
|
||||
os << "[";
|
||||
|
||||
for ( const auto& x : this->listValue() ) {
|
||||
os << sep;
|
||||
x.dumpJSON(os);
|
||||
sep = ", ";
|
||||
}
|
||||
os << " ]";
|
||||
} else if ( this->isObject() ) {
|
||||
const char * sep = " ";
|
||||
os << "{";
|
||||
for ( const auto& kv : this->objectValue() ) {
|
||||
os << sep << '"' << kv.first << "\" : ";
|
||||
kv.second.dumpJSON(os);
|
||||
sep = ", ";
|
||||
}
|
||||
os << " }";
|
||||
} else {
|
||||
os << (this->boolValue() ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
std::string ContentResponseBlueprint::Data::asJSON() const
|
||||
{
|
||||
std::ostringstream oss;
|
||||
this->dumpJSON(oss);
|
||||
|
||||
// This JSON is going to be used in HTML inside a <script></script> tag.
|
||||
// If it contains "</script>" (or "</script >") as a substring, then the HTML
|
||||
// parser will be confused. Since for a valid JSON that may happen only inside
|
||||
// a JSON string, we can safely take advantage of the answers to
|
||||
// https://stackoverflow.com/questions/28259389/how-to-put-script-in-a-javascript-string
|
||||
// and work around the issue by inserting an otherwise harmless backslash.
|
||||
return std::regex_replace(oss.str(), std::regex("</script"), "</scr\\ipt");
|
||||
}
|
||||
|
||||
ContentResponseBlueprint::ContentResponseBlueprint(const RequestContext* request,
|
||||
int httpStatusCode,
|
||||
const std::string& mimeType,
|
||||
const std::string& templateStr,
|
||||
bool includeKiwixResponseData)
|
||||
: m_request(*request)
|
||||
, m_httpStatusCode(httpStatusCode)
|
||||
, m_mimeType(mimeType)
|
||||
, m_template(templateStr)
|
||||
, m_includeKiwixResponseData(includeKiwixResponseData)
|
||||
, m_data(new Data)
|
||||
{}
|
||||
|
||||
ContentResponseBlueprint::~ContentResponseBlueprint() = default;
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponseBlueprint::generateResponseObject() const
|
||||
{
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, m_mimeType);
|
||||
kainjow::mustache::data d = m_data->toMustache(m_request.get_user_language());
|
||||
if ( m_includeKiwixResponseData ) {
|
||||
d.set("KIWIX_RESPONSE_TEMPLATE", escapeForJSON(m_template, false));
|
||||
d.set("KIWIX_RESPONSE_DATA", m_data->asJSON());
|
||||
}
|
||||
auto r = ContentResponse::build(m_template, d, m_mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
return r;
|
||||
}
|
||||
|
||||
HTTPErrorResponse::HTTPErrorResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
HTTPErrorResponse::HTTPErrorResponse(const RequestContext& request,
|
||||
int httpStatusCode,
|
||||
const std::string& pageTitleMsgId,
|
||||
const std::string& headingMsgId,
|
||||
const std::string& cssUrl)
|
||||
: ContentResponseBlueprint(&server,
|
||||
&request,
|
||||
const std::string& cssUrl,
|
||||
bool includeKiwixResponseData)
|
||||
: ContentResponseBlueprint(&request,
|
||||
httpStatusCode,
|
||||
request.get_requested_format() == "html" ? "text/html; charset=utf-8" : "application/xml; charset=utf-8",
|
||||
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml)
|
||||
request.get_requested_format() == "html" ? RESOURCE::templates::error_html : RESOURCE::templates::error_xml,
|
||||
includeKiwixResponseData)
|
||||
{
|
||||
kainjow::mustache::list emptyList;
|
||||
this->m_data = kainjow::mustache::object{
|
||||
{"CSS_URL", onlyAsNonEmptyMustacheValue(cssUrl) },
|
||||
{"PAGE_TITLE", getMessage(pageTitleMsgId)},
|
||||
{"PAGE_HEADING", getMessage(headingMsgId)},
|
||||
Data::List emptyList;
|
||||
*this->m_data = Data(Data::Object{
|
||||
{"CSS_URL", Data::onlyAsNonEmptyValue(cssUrl) },
|
||||
{"PAGE_TITLE", Data::from(nonParameterizedMessage(pageTitleMsgId))},
|
||||
{"PAGE_HEADING", Data::from(nonParameterizedMessage(headingMsgId))},
|
||||
{"details", emptyList}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
HTTP404Response::HTTP404Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
HTTP404Response::HTTP404Response(const RequestContext& request)
|
||||
: HTTPErrorResponse(request,
|
||||
MHD_HTTP_NOT_FOUND,
|
||||
"404-page-title",
|
||||
"404-page-heading")
|
||||
"404-page-heading",
|
||||
std::string(),
|
||||
/*includeKiwixResponseData=*/true)
|
||||
{
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTP404Response::operator+(UrlNotFoundMsg /*unused*/)
|
||||
UrlNotFoundResponse::UrlNotFoundResponse(const RequestContext& request)
|
||||
: HTTP404Response(request)
|
||||
{
|
||||
const std::string requestUrl = urlDecode(m_request.get_full_url(), false);
|
||||
return *this + ParameterizedMessage("url-not-found", {{"url", requestUrl}});
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTPErrorResponse::operator+(const std::string& msg)
|
||||
{
|
||||
m_data["details"].push_back({"p", msg});
|
||||
return *this;
|
||||
*this += ParameterizedMessage("url-not-found", {{"url", requestUrl}});
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTPErrorResponse::operator+(const ParameterizedMessage& details)
|
||||
{
|
||||
return *this + details.getText(m_request.get_user_language());
|
||||
(*m_data)["details"].push_back(Data::Object{{"p", Data::from(details)}});
|
||||
return *this;
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& details)
|
||||
@@ -222,50 +419,36 @@ HTTPErrorResponse& HTTPErrorResponse::operator+=(const ParameterizedMessage& det
|
||||
}
|
||||
|
||||
|
||||
HTTP400Response::HTTP400Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
HTTP400Response::HTTP400Response(const RequestContext& request)
|
||||
: HTTPErrorResponse(request,
|
||||
MHD_HTTP_BAD_REQUEST,
|
||||
"400-page-title",
|
||||
"400-page-heading")
|
||||
{
|
||||
}
|
||||
|
||||
HTTPErrorResponse& HTTP400Response::operator+(InvalidUrlMsg /*unused*/)
|
||||
"400-page-heading",
|
||||
std::string(),
|
||||
/*includeKiwixResponseData=*/true)
|
||||
{
|
||||
std::string requestUrl = urlDecode(m_request.get_full_url(), false);
|
||||
const auto query = m_request.get_query();
|
||||
if (!query.empty()) {
|
||||
requestUrl += "?" + encodeDiples(query);
|
||||
}
|
||||
kainjow::mustache::mustache msgTmpl(R"(The requested URL "{{{url}}}" is not a valid request.)");
|
||||
return *this + msgTmpl.render({"url", requestUrl});
|
||||
*this += ParameterizedMessage("invalid-request", {{"url", requestUrl}});
|
||||
}
|
||||
|
||||
HTTP500Response::HTTP500Response(const InternalServer& server,
|
||||
const RequestContext& request)
|
||||
: HTTPErrorResponse(server,
|
||||
request,
|
||||
HTTP500Response::HTTP500Response(const RequestContext& request)
|
||||
: HTTPErrorResponse(request,
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"500-page-title",
|
||||
"500-page-heading")
|
||||
"500-page-heading",
|
||||
std::string(),
|
||||
/*includeKiwixResponseData=*/true)
|
||||
{
|
||||
// operator+() is a state-modifying operator (akin to operator+=)
|
||||
*this + "An internal server error occured. We are sorry about that :/";
|
||||
*this += nonParameterizedMessage("500-page-text");
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> HTTP500Response::generateResponseObject() const
|
||||
std::unique_ptr<Response> Response::build_416(size_t resourceLength)
|
||||
{
|
||||
const std::string mimeType = "text/html;charset=utf-8";
|
||||
auto r = ContentResponse::build(m_server, m_template, m_data, mimeType);
|
||||
r->set_code(m_httpStatusCode);
|
||||
return r;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> Response::build_416(const InternalServer& server, size_t resourceLength)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
auto response = Response::build();
|
||||
// [FIXME] (compile with recent enough version of libmicrohttpd)
|
||||
// response->set_code(MHD_HTTP_RANGE_NOT_SATISFIABLE);
|
||||
response->set_code(416);
|
||||
@@ -277,9 +460,9 @@ std::unique_ptr<Response> Response::build_416(const InternalServer& server, size
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Response> Response::build_redirect(const InternalServer& server, const std::string& redirectUrl)
|
||||
std::unique_ptr<Response> Response::build_redirect(const std::string& redirectUrl)
|
||||
{
|
||||
auto response = Response::build(server);
|
||||
auto response = Response::build();
|
||||
response->m_returnCode = MHD_HTTP_FOUND;
|
||||
response->add_header(MHD_HTTP_HEADER_LOCATION, redirectUrl);
|
||||
return response;
|
||||
@@ -374,7 +557,7 @@ ContentResponse::create_mhd_response(const RequestContext& request)
|
||||
return response;
|
||||
}
|
||||
|
||||
MHD_Result Response::send(const RequestContext& request, MHD_Connection* connection)
|
||||
MHD_Result Response::send(const RequestContext& request, bool verbose, MHD_Connection* connection)
|
||||
{
|
||||
MHD_Response* response = create_mhd_response(request);
|
||||
|
||||
@@ -387,17 +570,10 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
MHD_add_response_header(response, p.first.c_str(), p.second.c_str());
|
||||
}
|
||||
|
||||
if ( ! request.user_language_comes_from_cookie() ) {
|
||||
const std::string cookie = "userlang=" + request.get_user_language()
|
||||
+ ";Path=" + request.get_root_path()
|
||||
+ ";Max-Age=31536000";
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_SET_COOKIE, cookie.c_str());
|
||||
}
|
||||
|
||||
if (m_returnCode == MHD_HTTP_OK && m_byteRange.kind() == ByteRange::RESOLVED_PARTIAL_CONTENT)
|
||||
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
|
||||
|
||||
if (m_verbose)
|
||||
if (verbose)
|
||||
print_response_info(m_returnCode, response);
|
||||
|
||||
auto ret = MHD_queue_response(connection, m_returnCode, response);
|
||||
@@ -405,9 +581,8 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
return ret;
|
||||
}
|
||||
|
||||
ContentResponse::ContentResponse(const std::string& root, bool verbose, const std::string& content, const std::string& mimetype) :
|
||||
Response(verbose),
|
||||
m_root(root),
|
||||
ContentResponse::ContentResponse(const std::string& content, const std::string& mimetype) :
|
||||
Response(),
|
||||
m_content(content),
|
||||
m_mimeType(mimetype)
|
||||
{
|
||||
@@ -415,29 +590,23 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, const st
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||
const InternalServer& server,
|
||||
const std::string& content,
|
||||
const std::string& mimetype)
|
||||
{
|
||||
return std::unique_ptr<ContentResponse>(new ContentResponse(
|
||||
server.m_root,
|
||||
server.m_verbose.load(),
|
||||
content,
|
||||
mimetype));
|
||||
return std::make_unique<ContentResponse>(content, mimetype);
|
||||
}
|
||||
|
||||
std::unique_ptr<ContentResponse> ContentResponse::build(
|
||||
const InternalServer& server,
|
||||
const std::string& template_str,
|
||||
kainjow::mustache::data data,
|
||||
const std::string& mimetype)
|
||||
{
|
||||
auto content = render_template(template_str, data);
|
||||
return ContentResponse::build(server, content, mimetype);
|
||||
return ContentResponse::build(content, mimetype);
|
||||
}
|
||||
|
||||
ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
||||
Response(verbose),
|
||||
ItemResponse::ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
|
||||
Response(),
|
||||
m_item(item),
|
||||
m_mimeType(mimetype)
|
||||
{
|
||||
@@ -446,30 +615,26 @@ ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::strin
|
||||
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item)
|
||||
std::unique_ptr<Response> ItemResponse::build(const RequestContext& request, const zim::Item& item)
|
||||
{
|
||||
const std::string mimetype = get_mime_type(item);
|
||||
auto byteRange = request.get_range().resolve(item.getSize());
|
||||
const bool noRange = byteRange.kind() == ByteRange::RESOLVED_FULL_CONTENT;
|
||||
if (noRange && is_compressible_mime_type(mimetype)) {
|
||||
// Return a contentResponse
|
||||
auto response = ContentResponse::build(server, item.getData(), mimetype);
|
||||
auto response = ContentResponse::build(item.getData(), mimetype);
|
||||
response->set_kind(Response::ZIM_CONTENT);
|
||||
response->m_byteRange = byteRange;
|
||||
return std::move(response);
|
||||
}
|
||||
|
||||
if (byteRange.kind() == ByteRange::RESOLVED_UNSATISFIABLE) {
|
||||
auto response = Response::build_416(server, item.getSize());
|
||||
auto response = Response::build_416(item.getSize());
|
||||
response->set_kind(Response::ZIM_CONTENT);
|
||||
return response;
|
||||
}
|
||||
|
||||
return std::unique_ptr<Response>(new ItemResponse(
|
||||
server.m_verbose.load(),
|
||||
item,
|
||||
mimetype,
|
||||
byteRange));
|
||||
return std::make_unique<ItemResponse>(item, mimetype, byteRange);
|
||||
}
|
||||
|
||||
MHD_Response*
|
||||
|
||||
@@ -41,7 +41,6 @@ class Archive;
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
class InternalServer;
|
||||
class RequestContext;
|
||||
|
||||
class Response {
|
||||
@@ -54,15 +53,15 @@ class Response {
|
||||
};
|
||||
|
||||
public:
|
||||
Response(bool verbose);
|
||||
Response();
|
||||
virtual ~Response() = default;
|
||||
|
||||
static std::unique_ptr<Response> build(const InternalServer& server);
|
||||
static std::unique_ptr<Response> build_304(const InternalServer& server, const ETag& etag);
|
||||
static std::unique_ptr<Response> build_416(const InternalServer& server, size_t resourceLength);
|
||||
static std::unique_ptr<Response> build_redirect(const InternalServer& server, const std::string& redirectUrl);
|
||||
static std::unique_ptr<Response> build();
|
||||
static std::unique_ptr<Response> build_304(const ETag& etag);
|
||||
static std::unique_ptr<Response> build_416(size_t resourceLength);
|
||||
static std::unique_ptr<Response> build_redirect(const std::string& redirectUrl);
|
||||
|
||||
MHD_Result send(const RequestContext& request, MHD_Connection* connection);
|
||||
MHD_Result send(const RequestContext& request, bool verbose, MHD_Connection* connection);
|
||||
|
||||
void set_code(int code) { m_returnCode = code; }
|
||||
void set_kind(Kind k);
|
||||
@@ -78,7 +77,6 @@ class Response {
|
||||
|
||||
protected: // data
|
||||
Kind m_kind = DYNAMIC_CONTENT;
|
||||
bool m_verbose;
|
||||
int m_returnCode;
|
||||
ByteRange m_byteRange;
|
||||
ETag m_etag;
|
||||
@@ -91,22 +89,21 @@ class Response {
|
||||
class ContentResponse : public Response {
|
||||
public:
|
||||
ContentResponse(
|
||||
const std::string& root,
|
||||
bool verbose,
|
||||
const std::string& content,
|
||||
const std::string& mimetype);
|
||||
|
||||
static std::unique_ptr<ContentResponse> build(
|
||||
const InternalServer& server,
|
||||
const std::string& content,
|
||||
const std::string& mimetype);
|
||||
|
||||
static std::unique_ptr<ContentResponse> build(
|
||||
const InternalServer& server,
|
||||
const std::string& template_str,
|
||||
kainjow::mustache::data data,
|
||||
const std::string& mimetype);
|
||||
|
||||
const std::string& getContent() const { return m_content; }
|
||||
const std::string& getMimeType() const { return m_mimeType; }
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
|
||||
@@ -114,7 +111,6 @@ class ContentResponse : public Response {
|
||||
|
||||
|
||||
private:
|
||||
std::string m_root;
|
||||
std::string m_content;
|
||||
std::string m_mimeType;
|
||||
};
|
||||
@@ -122,99 +118,70 @@ class ContentResponse : public Response {
|
||||
class ContentResponseBlueprint
|
||||
{
|
||||
public: // functions
|
||||
ContentResponseBlueprint(const InternalServer* server,
|
||||
const RequestContext* request,
|
||||
ContentResponseBlueprint(const RequestContext* request,
|
||||
int httpStatusCode,
|
||||
const std::string& mimeType,
|
||||
const std::string& templateStr)
|
||||
: m_server(*server)
|
||||
, m_request(*request)
|
||||
, m_httpStatusCode(httpStatusCode)
|
||||
, m_mimeType(mimeType)
|
||||
, m_template(templateStr)
|
||||
{}
|
||||
const std::string& templateStr,
|
||||
bool includeKiwixResponseData = false);
|
||||
|
||||
virtual ~ContentResponseBlueprint() = default;
|
||||
~ContentResponseBlueprint();
|
||||
|
||||
operator std::unique_ptr<ContentResponse>() const
|
||||
operator std::unique_ptr<Response>() const
|
||||
{
|
||||
return generateResponseObject();
|
||||
}
|
||||
|
||||
operator std::unique_ptr<Response>() const
|
||||
{
|
||||
return operator std::unique_ptr<ContentResponse>();
|
||||
}
|
||||
std::unique_ptr<ContentResponse> generateResponseObject() const;
|
||||
|
||||
protected: // types
|
||||
class Data;
|
||||
|
||||
protected: // functions
|
||||
std::string getMessage(const std::string& msgId) const;
|
||||
virtual std::unique_ptr<ContentResponse> generateResponseObject() const;
|
||||
|
||||
public: //data
|
||||
const InternalServer& m_server;
|
||||
protected: //data
|
||||
const RequestContext& m_request;
|
||||
const int m_httpStatusCode;
|
||||
const std::string m_mimeType;
|
||||
const std::string m_template;
|
||||
kainjow::mustache::data m_data;
|
||||
const bool m_includeKiwixResponseData;
|
||||
std::unique_ptr<Data> m_data;
|
||||
};
|
||||
|
||||
struct HTTPErrorResponse : ContentResponseBlueprint
|
||||
{
|
||||
HTTPErrorResponse(const InternalServer& server,
|
||||
const RequestContext& request,
|
||||
HTTPErrorResponse(const RequestContext& request,
|
||||
int httpStatusCode,
|
||||
const std::string& pageTitleMsgId,
|
||||
const std::string& headingMsgId,
|
||||
const std::string& cssUrl = "");
|
||||
const std::string& cssUrl = "",
|
||||
bool includeKiwixResponseData = false);
|
||||
|
||||
HTTPErrorResponse& operator+(const std::string& msg);
|
||||
HTTPErrorResponse& operator+(const ParameterizedMessage& errorDetails);
|
||||
HTTPErrorResponse& operator+=(const ParameterizedMessage& errorDetails);
|
||||
};
|
||||
|
||||
class UrlNotFoundMsg {};
|
||||
|
||||
extern const UrlNotFoundMsg urlNotFoundMsg;
|
||||
|
||||
struct HTTP404Response : HTTPErrorResponse
|
||||
{
|
||||
HTTP404Response(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
using HTTPErrorResponse::operator+;
|
||||
HTTPErrorResponse& operator+(UrlNotFoundMsg /*unused*/);
|
||||
explicit HTTP404Response(const RequestContext& request);
|
||||
};
|
||||
|
||||
class InvalidUrlMsg {};
|
||||
|
||||
extern const InvalidUrlMsg invalidUrlMsg;
|
||||
struct UrlNotFoundResponse : HTTP404Response
|
||||
{
|
||||
explicit UrlNotFoundResponse(const RequestContext& request);
|
||||
};
|
||||
|
||||
struct HTTP400Response : HTTPErrorResponse
|
||||
{
|
||||
HTTP400Response(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
using HTTPErrorResponse::operator+;
|
||||
HTTPErrorResponse& operator+(InvalidUrlMsg /*unused*/);
|
||||
explicit HTTP400Response(const RequestContext& request);
|
||||
};
|
||||
|
||||
struct HTTP500Response : HTTPErrorResponse
|
||||
{
|
||||
HTTP500Response(const InternalServer& server,
|
||||
const RequestContext& request);
|
||||
|
||||
private: // overrides
|
||||
// generateResponseObject() is overriden in order to produce a minimal
|
||||
// response without any need for additional resources from the server
|
||||
std::unique_ptr<ContentResponse> generateResponseObject() const override;
|
||||
explicit HTTP500Response(const RequestContext& request);
|
||||
};
|
||||
|
||||
class ItemResponse : public Response {
|
||||
public:
|
||||
ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||
static std::unique_ptr<Response> build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
|
||||
ItemResponse(const zim::Item& item, const std::string& mimetype, const ByteRange& byterange);
|
||||
static std::unique_ptr<Response> build(const RequestContext& request, const zim::Item& item);
|
||||
|
||||
private:
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
|
||||
75
src/tools/languageTools.cpp
Normal file
75
src/tools/languageTools.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "tools.h"
|
||||
#include "stringTools.h"
|
||||
#include <mutex>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// These mappings are not provided by the ICU library, any such mappings can be manually added here
|
||||
std::map<std::string, std::string> iso639_3 = {
|
||||
{"atj", "atikamekw"},
|
||||
{"azb", "آذربایجان دیلی"},
|
||||
{"bcl", "central bikol"},
|
||||
{"bgs", "tagabawa"},
|
||||
{"bxr", "буряад хэлэн"},
|
||||
{"cbk", "chavacano"},
|
||||
{"cdo", "閩東語"},
|
||||
{"dag", "Dagbani"},
|
||||
{"diq", "dimli"},
|
||||
{"dty", "डोटेली"},
|
||||
{"eml", "emiliân-rumagnōl"},
|
||||
{"fbs", "српскохрватски"},
|
||||
{"fon", "fɔ̀ngbè"},
|
||||
{"guw", "Gungbe"},
|
||||
{"hbs", "srpskohrvatski"},
|
||||
{"ido", "ido"},
|
||||
{"kbp", "kabɩyɛ"},
|
||||
{"kld", "Gamilaraay"},
|
||||
{"lbe", "лакку маз"},
|
||||
{"lbj", "ལ་དྭགས་སྐད་"},
|
||||
{"map", "Austronesian"},
|
||||
{"mhr", "марий йылме"},
|
||||
{"mnw", "ဘာသာမန်"},
|
||||
{"myn", "mayan"},
|
||||
{"nah", "nahuatl"},
|
||||
{"nai", "north American Indian"},
|
||||
{"nds", "plattdütsch"},
|
||||
{"nrm", "bhasa narom"},
|
||||
{"olo", "livvi"},
|
||||
{"pih", "Pitcairn-Norfolk"},
|
||||
{"pnb", "Western Panjabi"},
|
||||
{"rmr", "Caló"},
|
||||
{"rmy", "romani shib"},
|
||||
{"roa", "romance languages"},
|
||||
{"twi", "twi"},
|
||||
// ICU for Ubuntu versions <= focal (20.04) returns "" for the language code ""
|
||||
// unlike the later versions - which returns "und". We map this value to "Undetermined" for a common ground.
|
||||
{"", "Undetermined"},
|
||||
};
|
||||
|
||||
std::once_flag fillLanguagesFlag;
|
||||
|
||||
void fillLanguagesMap()
|
||||
{
|
||||
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
|
||||
const kiwix::ICULanguageInfo lang(*icuLangPtr);
|
||||
iso639_3.insert({lang.iso3Code(), lang.selfName()});
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::string getLanguageSelfName(const std::string& lang)
|
||||
{
|
||||
std::call_once(fillLanguagesFlag, fillLanguagesMap);
|
||||
const auto itr = iso639_3.find(lang);
|
||||
if (itr != iso639_3.end()) {
|
||||
return itr->second;
|
||||
}
|
||||
return lang;
|
||||
};
|
||||
|
||||
} // namespace kiwix
|
||||
@@ -43,6 +43,10 @@
|
||||
#include <netdb.h>
|
||||
#endif
|
||||
|
||||
#ifdef __HAIKU__
|
||||
#include <sys/sockio.h>
|
||||
#endif
|
||||
|
||||
size_t write_callback_to_iss(char* ptr, size_t size, size_t nmemb, void* userdata)
|
||||
{
|
||||
auto str = static_cast<std::stringstream*>(userdata);
|
||||
|
||||
70
src/tools/opdsParsingTools.cpp
Normal file
70
src/tools/opdsParsingTools.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "tools.h"
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
#define VALUE(name) entryNode.child(name).child_value()
|
||||
FeedLanguages parseLanguages(const pugi::xml_document& doc)
|
||||
{
|
||||
pugi::xml_node feedNode = doc.child("feed");
|
||||
FeedLanguages langs;
|
||||
|
||||
for (pugi::xml_node entryNode = feedNode.child("entry"); entryNode;
|
||||
entryNode = entryNode.next_sibling("entry")) {
|
||||
auto title = VALUE("title");
|
||||
auto code = VALUE("dc:language");
|
||||
langs.push_back({code, title});
|
||||
}
|
||||
|
||||
return langs;
|
||||
}
|
||||
|
||||
FeedCategories parseCategories(const pugi::xml_document& doc)
|
||||
{
|
||||
pugi::xml_node feedNode = doc.child("feed");
|
||||
FeedCategories categories;
|
||||
|
||||
for (pugi::xml_node entryNode = feedNode.child("entry"); entryNode;
|
||||
entryNode = entryNode.next_sibling("entry")) {
|
||||
auto title = VALUE("title");
|
||||
categories.push_back(title);
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
FeedLanguages readLanguagesFromFeed(const std::string& content)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result
|
||||
= doc.load_buffer((void*)content.data(), content.size());
|
||||
|
||||
if (result) {
|
||||
auto langs = parseLanguages(doc);
|
||||
return langs;
|
||||
}
|
||||
|
||||
return FeedLanguages();
|
||||
}
|
||||
|
||||
FeedCategories readCategoriesFromFeed(const std::string& content)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result
|
||||
= doc.load_buffer((void*)content.data(), content.size());
|
||||
|
||||
FeedCategories categories;
|
||||
if (result) {
|
||||
categories = parseCategories(doc);
|
||||
return categories;
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
||||
@@ -327,22 +327,37 @@ std::string kiwix::render_template(const std::string& template_str, kainjow::mus
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// The escapeQuote parameter of escapeForJSON() defaults to true.
|
||||
// This constant makes the calls to escapeForJSON() where the quote symbol
|
||||
// should not be escaped (as it is later replaced with the HTML character entity
|
||||
// ") more readable.
|
||||
static const bool DONT_ESCAPE_QUOTE = false;
|
||||
|
||||
std::string escapeBackslashes(const std::string& s)
|
||||
std::string kiwix::escapeForJSON(const std::string& s, bool escapeQuote)
|
||||
{
|
||||
std::string es;
|
||||
es.reserve(s.size());
|
||||
std::ostringstream oss;
|
||||
for (char c : s) {
|
||||
if ( c == '\\' ) {
|
||||
es.push_back('\\');
|
||||
oss << "\\\\";
|
||||
} else if ( unsigned(c) < 0x20U ) {
|
||||
switch ( c ) {
|
||||
case '\n': oss << "\\n"; break;
|
||||
case '\r': oss << "\\r"; break;
|
||||
case '\t': oss << "\\t"; break;
|
||||
default: oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c);
|
||||
}
|
||||
} else if ( c == '"' && escapeQuote ) {
|
||||
oss << "\\\"";
|
||||
} else {
|
||||
oss << c;
|
||||
}
|
||||
es.push_back(c);
|
||||
}
|
||||
return es;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string makeFulltextSearchSuggestion(const std::string& lang,
|
||||
const std::string& queryString)
|
||||
{
|
||||
@@ -368,10 +383,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
|
||||
? suggestion.getSnippet()
|
||||
: suggestion.getTitle();
|
||||
|
||||
result.set("label", escapeBackslashes(label));
|
||||
result.set("value", escapeBackslashes(suggestion.getTitle()));
|
||||
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
|
||||
result.set("value", escapeForJSON(suggestion.getTitle(), DONT_ESCAPE_QUOTE));
|
||||
result.set("kind", "path");
|
||||
result.set("path", escapeBackslashes(suggestion.getPath()));
|
||||
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
|
||||
result.set("first", m_data.is_empty_list());
|
||||
m_data.push_back(result);
|
||||
}
|
||||
@@ -381,8 +396,8 @@ void kiwix::Suggestions::addFTSearchSuggestion(const std::string& uiLang,
|
||||
{
|
||||
kainjow::mustache::data result;
|
||||
const std::string label = makeFulltextSearchSuggestion(uiLang, queryString);
|
||||
result.set("label", escapeBackslashes(label));
|
||||
result.set("value", escapeBackslashes(queryString + " "));
|
||||
result.set("label", escapeForJSON(label, DONT_ESCAPE_QUOTE));
|
||||
result.set("value", escapeForJSON(queryString + " ", DONT_ESCAPE_QUOTE));
|
||||
result.set("kind", "pattern");
|
||||
result.set("first", m_data.is_empty_list());
|
||||
m_data.push_back(result);
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
namespace kiwix
|
||||
{
|
||||
std::string beautifyInteger(uint64_t number);
|
||||
std::string beautifyFileSize(uint64_t number);
|
||||
void printStringInHexadecimal(const char* s);
|
||||
void printStringInHexadecimal(icu::UnicodeString s);
|
||||
void stringReplacement(std::string& str,
|
||||
@@ -54,6 +53,7 @@ private:
|
||||
const icu::Locale locale;
|
||||
};
|
||||
|
||||
std::string escapeForJSON(const std::string& s, bool escapeQuote = true);
|
||||
|
||||
/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only
|
||||
* difference that the slash (/) symbol is NOT encoded. */
|
||||
|
||||
@@ -30,7 +30,10 @@ def get_translation_info(filepath):
|
||||
with open(filepath, 'r', encoding="utf-8") as f:
|
||||
content = json.load(f)
|
||||
lang_name = content.get("name")
|
||||
return lang_code, lang_name
|
||||
translation_count = len(content)
|
||||
return dict(iso_code=lang_code,
|
||||
self_name=lang_name,
|
||||
translation_count=translation_count)
|
||||
|
||||
language_list = []
|
||||
json_files = translation_dir.glob("*.json")
|
||||
@@ -40,14 +43,14 @@ with open(resource_file, 'w', encoding="utf-8") as f:
|
||||
continue
|
||||
print("Processing", i18n_file.name)
|
||||
if i18n_file.name != "test.json":
|
||||
lang_code, lang_name = get_translation_info(i18n_file)
|
||||
translation_info = get_translation_info(i18n_file)
|
||||
lang_name = translation_info["self_name"]
|
||||
if lang_name:
|
||||
language_list.append((lang_code, lang_name))
|
||||
language_list.append(translation_info)
|
||||
else:
|
||||
print(f"Warning: missing 'name' in {i18n_file.name}")
|
||||
f.write(str(i18n_file.relative_to(script_path.parent)) + '\n')
|
||||
|
||||
language_list = [{name: code} for code, name in sorted(language_list)]
|
||||
language_list_jsobj_str = json.dumps(language_list,
|
||||
indent=2,
|
||||
ensure_ascii=False)
|
||||
|
||||
@@ -5,9 +5,11 @@ skin/i18n/de.json
|
||||
skin/i18n/dga.json
|
||||
skin/i18n/el.json
|
||||
skin/i18n/en.json
|
||||
skin/i18n/es.json
|
||||
skin/i18n/fi.json
|
||||
skin/i18n/fr.json
|
||||
skin/i18n/he.json
|
||||
skin/i18n/hi.json
|
||||
skin/i18n/hy.json
|
||||
skin/i18n/ia.json
|
||||
skin/i18n/it.json
|
||||
@@ -16,14 +18,19 @@ skin/i18n/ko.json
|
||||
skin/i18n/ku-latn.json
|
||||
skin/i18n/lb.json
|
||||
skin/i18n/mk.json
|
||||
skin/i18n/ms.json
|
||||
skin/i18n/nl.json
|
||||
skin/i18n/nqo.json
|
||||
skin/i18n/or.json
|
||||
skin/i18n/pl.json
|
||||
skin/i18n/ru.json
|
||||
skin/i18n/sc.json
|
||||
skin/i18n/sk.json
|
||||
skin/i18n/skr-arab.json
|
||||
skin/i18n/sl.json
|
||||
skin/i18n/sq.json
|
||||
skin/i18n/sv.json
|
||||
skin/i18n/te.json
|
||||
skin/i18n/test.json
|
||||
skin/i18n/tr.json
|
||||
skin/i18n/zh-hans.json
|
||||
|
||||
@@ -9,7 +9,8 @@ skin/search-icon.svg
|
||||
skin/iso6391To3.js
|
||||
skin/isotope.pkgd.min.js
|
||||
skin/index.js
|
||||
skin/autoComplete.min.js
|
||||
skin/autoComplete/autoComplete.min.js
|
||||
skin/kiwix.css
|
||||
skin/taskbar.css
|
||||
skin/index.css
|
||||
skin/fonts/Poppins.ttf
|
||||
@@ -42,7 +43,7 @@ templates/no_js_download.html
|
||||
opensearchdescription.xml
|
||||
ft_opensearchdescription.xml
|
||||
catalog_v2_searchdescription.xml
|
||||
skin/css/autoComplete.css
|
||||
skin/autoComplete/css/autoComplete.css
|
||||
skin/favicon/android-chrome-192x192.png
|
||||
skin/favicon/android-chrome-512x512.png
|
||||
skin/favicon/apple-touch-icon.png
|
||||
|
||||
201
static/skin/autoComplete/LICENSE
Normal file
201
static/skin/autoComplete/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
654
static/skin/autoComplete/autoComplete.js
Normal file
654
static/skin/autoComplete/autoComplete.js
Normal file
@@ -0,0 +1,654 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.autoComplete = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
|
||||
if (enumerableOnly) {
|
||||
symbols = symbols.filter(function (sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
});
|
||||
}
|
||||
|
||||
keys.push.apply(keys, symbols);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function _objectSpread2(target) {
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var source = arguments[i] != null ? arguments[i] : {};
|
||||
|
||||
if (i % 2) {
|
||||
ownKeys(Object(source), true).forEach(function (key) {
|
||||
_defineProperty(target, key, source[key]);
|
||||
});
|
||||
} else if (Object.getOwnPropertyDescriptors) {
|
||||
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
|
||||
} else {
|
||||
ownKeys(Object(source)).forEach(function (key) {
|
||||
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
function _typeof(obj) {
|
||||
"@babel/helpers - typeof";
|
||||
|
||||
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
|
||||
_typeof = function (obj) {
|
||||
return typeof obj;
|
||||
};
|
||||
} else {
|
||||
_typeof = function (obj) {
|
||||
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
||||
};
|
||||
}
|
||||
|
||||
return _typeof(obj);
|
||||
}
|
||||
|
||||
function _defineProperty(obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function _toConsumableArray(arr) {
|
||||
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
|
||||
}
|
||||
|
||||
function _arrayWithoutHoles(arr) {
|
||||
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
|
||||
}
|
||||
|
||||
function _iterableToArray(iter) {
|
||||
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
|
||||
}
|
||||
|
||||
function _unsupportedIterableToArray(o, minLen) {
|
||||
if (!o) return;
|
||||
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
|
||||
var n = Object.prototype.toString.call(o).slice(8, -1);
|
||||
if (n === "Object" && o.constructor) n = o.constructor.name;
|
||||
if (n === "Map" || n === "Set") return Array.from(o);
|
||||
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
|
||||
}
|
||||
|
||||
function _arrayLikeToArray(arr, len) {
|
||||
if (len == null || len > arr.length) len = arr.length;
|
||||
|
||||
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
|
||||
|
||||
return arr2;
|
||||
}
|
||||
|
||||
function _nonIterableSpread() {
|
||||
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
||||
}
|
||||
|
||||
function _createForOfIteratorHelper(o, allowArrayLike) {
|
||||
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
|
||||
|
||||
if (!it) {
|
||||
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
|
||||
if (it) o = it;
|
||||
var i = 0;
|
||||
|
||||
var F = function () {};
|
||||
|
||||
return {
|
||||
s: F,
|
||||
n: function () {
|
||||
if (i >= o.length) return {
|
||||
done: true
|
||||
};
|
||||
return {
|
||||
done: false,
|
||||
value: o[i++]
|
||||
};
|
||||
},
|
||||
e: function (e) {
|
||||
throw e;
|
||||
},
|
||||
f: F
|
||||
};
|
||||
}
|
||||
|
||||
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
||||
}
|
||||
|
||||
var normalCompletion = true,
|
||||
didErr = false,
|
||||
err;
|
||||
return {
|
||||
s: function () {
|
||||
it = it.call(o);
|
||||
},
|
||||
n: function () {
|
||||
var step = it.next();
|
||||
normalCompletion = step.done;
|
||||
return step;
|
||||
},
|
||||
e: function (e) {
|
||||
didErr = true;
|
||||
err = e;
|
||||
},
|
||||
f: function () {
|
||||
try {
|
||||
if (!normalCompletion && it.return != null) it.return();
|
||||
} finally {
|
||||
if (didErr) throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var select$1 = function select(element) {
|
||||
return typeof element === "string" ? document.querySelector(element) : element();
|
||||
};
|
||||
var create = function create(tag, options) {
|
||||
var el = typeof tag === "string" ? document.createElement(tag) : tag;
|
||||
for (var key in options) {
|
||||
var val = options[key];
|
||||
if (key === "inside") {
|
||||
val.append(el);
|
||||
} else if (key === "dest") {
|
||||
select$1(val[0]).insertAdjacentElement(val[1], el);
|
||||
} else if (key === "around") {
|
||||
var ref = val;
|
||||
ref.parentNode.insertBefore(el, ref);
|
||||
el.append(ref);
|
||||
if (ref.getAttribute("autofocus") != null) ref.focus();
|
||||
} else if (key in el) {
|
||||
el[key] = val;
|
||||
} else {
|
||||
el.setAttribute(key, val);
|
||||
}
|
||||
}
|
||||
return el;
|
||||
};
|
||||
var getQuery = function getQuery(field) {
|
||||
return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement ? field.value : field.innerHTML;
|
||||
};
|
||||
var format = function format(value, diacritics) {
|
||||
value = value.toString().toLowerCase();
|
||||
return diacritics ? value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC") : value;
|
||||
};
|
||||
var debounce = function debounce(callback, duration) {
|
||||
var timer;
|
||||
return function () {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
return callback();
|
||||
}, duration);
|
||||
};
|
||||
};
|
||||
var checkTrigger = function checkTrigger(query, condition, threshold) {
|
||||
return condition ? condition(query) : query.length >= threshold;
|
||||
};
|
||||
var mark = function mark(value, cls) {
|
||||
return create("mark", _objectSpread2({
|
||||
innerHTML: value
|
||||
}, typeof cls === "string" && {
|
||||
"class": cls
|
||||
})).outerHTML;
|
||||
};
|
||||
|
||||
var configure = (function (ctx) {
|
||||
var name = ctx.name,
|
||||
options = ctx.options,
|
||||
resultsList = ctx.resultsList,
|
||||
resultItem = ctx.resultItem;
|
||||
for (var option in options) {
|
||||
if (_typeof(options[option]) === "object") {
|
||||
if (!ctx[option]) ctx[option] = {};
|
||||
for (var subOption in options[option]) {
|
||||
ctx[option][subOption] = options[option][subOption];
|
||||
}
|
||||
} else {
|
||||
ctx[option] = options[option];
|
||||
}
|
||||
}
|
||||
ctx.selector = ctx.selector || "#" + name;
|
||||
resultsList.destination = resultsList.destination || ctx.selector;
|
||||
resultsList.id = resultsList.id || name + "_list_" + ctx.id;
|
||||
resultItem.id = resultItem.id || name + "_result";
|
||||
ctx.input = select$1(ctx.selector);
|
||||
});
|
||||
|
||||
var eventEmitter = (function (name, ctx) {
|
||||
ctx.input.dispatchEvent(new CustomEvent(name, {
|
||||
bubbles: true,
|
||||
detail: ctx.feedback,
|
||||
cancelable: true
|
||||
}));
|
||||
});
|
||||
|
||||
var search = (function (query, record, options) {
|
||||
var _ref = options || {},
|
||||
mode = _ref.mode,
|
||||
diacritics = _ref.diacritics,
|
||||
highlight = _ref.highlight;
|
||||
var nRecord = format(record, diacritics);
|
||||
record = record.toString();
|
||||
query = format(query, diacritics);
|
||||
if (mode === "loose") {
|
||||
query = query.replace(/ /g, "");
|
||||
var qLength = query.length;
|
||||
var cursor = 0;
|
||||
var match = Array.from(record).map(function (character, index) {
|
||||
if (cursor < qLength && nRecord[index] === query[cursor]) {
|
||||
character = highlight ? mark(character, highlight) : character;
|
||||
cursor++;
|
||||
}
|
||||
return character;
|
||||
}).join("");
|
||||
if (cursor === qLength) return match;
|
||||
} else {
|
||||
var _match = nRecord.indexOf(query);
|
||||
if (~_match) {
|
||||
query = record.substring(_match, _match + query.length);
|
||||
_match = highlight ? record.replace(query, mark(query, highlight)) : record;
|
||||
return _match;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var getData = function getData(ctx, query) {
|
||||
return new Promise(function ($return, $error) {
|
||||
var data;
|
||||
data = ctx.data;
|
||||
if (data.cache && data.store) return $return();
|
||||
return new Promise(function ($return, $error) {
|
||||
if (typeof data.src === "function") {
|
||||
return data.src(query).then($return, $error);
|
||||
}
|
||||
return $return(data.src);
|
||||
}).then(function ($await_4) {
|
||||
try {
|
||||
ctx.feedback = data.store = $await_4;
|
||||
eventEmitter("response", ctx);
|
||||
return $return();
|
||||
} catch ($boundEx) {
|
||||
return $error($boundEx);
|
||||
}
|
||||
}, $error);
|
||||
});
|
||||
};
|
||||
var findMatches = function findMatches(query, ctx) {
|
||||
var data = ctx.data,
|
||||
searchEngine = ctx.searchEngine;
|
||||
var matches = [];
|
||||
data.store.forEach(function (value, index) {
|
||||
var find = function find(key) {
|
||||
var record = key ? value[key] : value;
|
||||
var match = typeof searchEngine === "function" ? searchEngine(query, record) : search(query, record, {
|
||||
mode: searchEngine,
|
||||
diacritics: ctx.diacritics,
|
||||
highlight: ctx.resultItem.highlight
|
||||
});
|
||||
if (!match) return;
|
||||
var result = {
|
||||
match: match,
|
||||
value: value
|
||||
};
|
||||
if (key) result.key = key;
|
||||
matches.push(result);
|
||||
};
|
||||
if (data.keys) {
|
||||
var _iterator = _createForOfIteratorHelper(data.keys),
|
||||
_step;
|
||||
try {
|
||||
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
||||
var key = _step.value;
|
||||
find(key);
|
||||
}
|
||||
} catch (err) {
|
||||
_iterator.e(err);
|
||||
} finally {
|
||||
_iterator.f();
|
||||
}
|
||||
} else {
|
||||
find();
|
||||
}
|
||||
});
|
||||
if (data.filter) matches = data.filter(matches);
|
||||
var results = matches.slice(0, ctx.resultsList.maxResults);
|
||||
ctx.feedback = {
|
||||
query: query,
|
||||
matches: matches,
|
||||
results: results
|
||||
};
|
||||
eventEmitter("results", ctx);
|
||||
};
|
||||
|
||||
var Expand = "aria-expanded";
|
||||
var Active = "aria-activedescendant";
|
||||
var Selected = "aria-selected";
|
||||
var feedback = function feedback(ctx, index) {
|
||||
ctx.feedback.selection = _objectSpread2({
|
||||
index: index
|
||||
}, ctx.feedback.results[index]);
|
||||
};
|
||||
var render = function render(ctx) {
|
||||
var resultsList = ctx.resultsList,
|
||||
list = ctx.list,
|
||||
resultItem = ctx.resultItem,
|
||||
feedback = ctx.feedback;
|
||||
var matches = feedback.matches,
|
||||
results = feedback.results;
|
||||
ctx.cursor = -1;
|
||||
list.innerHTML = "";
|
||||
if (matches.length || resultsList.noResults) {
|
||||
var fragment = new DocumentFragment();
|
||||
results.forEach(function (result, index) {
|
||||
var element = create(resultItem.tag, _objectSpread2({
|
||||
id: "".concat(resultItem.id, "_").concat(index),
|
||||
role: "option",
|
||||
innerHTML: result.match,
|
||||
inside: fragment
|
||||
}, resultItem["class"] && {
|
||||
"class": resultItem["class"]
|
||||
}));
|
||||
if (resultItem.element) resultItem.element(element, result);
|
||||
});
|
||||
list.append(fragment);
|
||||
if (resultsList.element) resultsList.element(list, feedback);
|
||||
open(ctx);
|
||||
} else {
|
||||
close(ctx);
|
||||
}
|
||||
};
|
||||
var open = function open(ctx) {
|
||||
if (ctx.isOpen) return;
|
||||
(ctx.wrapper || ctx.input).setAttribute(Expand, true);
|
||||
ctx.list.removeAttribute("hidden");
|
||||
ctx.isOpen = true;
|
||||
eventEmitter("open", ctx);
|
||||
};
|
||||
var close = function close(ctx) {
|
||||
if (!ctx.isOpen) return;
|
||||
(ctx.wrapper || ctx.input).setAttribute(Expand, false);
|
||||
ctx.input.setAttribute(Active, "");
|
||||
ctx.list.setAttribute("hidden", "");
|
||||
ctx.isOpen = false;
|
||||
eventEmitter("close", ctx);
|
||||
};
|
||||
var goTo = function goTo(index, ctx) {
|
||||
var resultItem = ctx.resultItem;
|
||||
var results = ctx.list.getElementsByTagName(resultItem.tag);
|
||||
var cls = resultItem.selected ? resultItem.selected.split(" ") : false;
|
||||
if (ctx.isOpen && results.length) {
|
||||
var _results$index$classL;
|
||||
var state = ctx.cursor;
|
||||
if (index >= results.length) index = 0;
|
||||
if (index < 0) index = results.length - 1;
|
||||
ctx.cursor = index;
|
||||
if (state > -1) {
|
||||
var _results$state$classL;
|
||||
results[state].removeAttribute(Selected);
|
||||
if (cls) (_results$state$classL = results[state].classList).remove.apply(_results$state$classL, _toConsumableArray(cls));
|
||||
}
|
||||
results[index].setAttribute(Selected, true);
|
||||
if (cls) (_results$index$classL = results[index].classList).add.apply(_results$index$classL, _toConsumableArray(cls));
|
||||
ctx.input.setAttribute(Active, results[ctx.cursor].id);
|
||||
ctx.list.scrollTop = results[index].offsetTop - ctx.list.clientHeight + results[index].clientHeight + 5;
|
||||
ctx.feedback.cursor = ctx.cursor;
|
||||
feedback(ctx, index);
|
||||
eventEmitter("navigate", ctx);
|
||||
}
|
||||
};
|
||||
var next = function next(ctx) {
|
||||
goTo(ctx.cursor + 1, ctx);
|
||||
};
|
||||
var previous = function previous(ctx) {
|
||||
goTo(ctx.cursor - 1, ctx);
|
||||
};
|
||||
var select = function select(ctx, event, index) {
|
||||
index = index >= 0 ? index : ctx.cursor;
|
||||
if (index < 0) return;
|
||||
ctx.feedback.event = event;
|
||||
feedback(ctx, index);
|
||||
eventEmitter("selection", ctx);
|
||||
close(ctx);
|
||||
};
|
||||
var click = function click(event, ctx) {
|
||||
var itemTag = ctx.resultItem.tag.toUpperCase();
|
||||
var items = Array.from(ctx.list.querySelectorAll(itemTag));
|
||||
var item = event.target.closest(itemTag);
|
||||
if (item && item.nodeName === itemTag) {
|
||||
select(ctx, event, items.indexOf(item));
|
||||
}
|
||||
};
|
||||
var navigate = function navigate(event, ctx) {
|
||||
switch (event.keyCode) {
|
||||
case 40:
|
||||
case 38:
|
||||
event.preventDefault();
|
||||
event.keyCode === 40 ? next(ctx) : previous(ctx);
|
||||
break;
|
||||
case 13:
|
||||
if (!ctx.submit) event.preventDefault();
|
||||
if (ctx.cursor >= 0) select(ctx, event);
|
||||
break;
|
||||
case 9:
|
||||
if (ctx.resultsList.tabSelect && ctx.cursor >= 0) select(ctx, event);
|
||||
break;
|
||||
case 27:
|
||||
ctx.input.value = "";
|
||||
close(ctx);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function start (ctx, q) {
|
||||
var _this = this;
|
||||
return new Promise(function ($return, $error) {
|
||||
var queryVal, condition;
|
||||
queryVal = q || getQuery(ctx.input);
|
||||
queryVal = ctx.query ? ctx.query(queryVal) : queryVal;
|
||||
condition = checkTrigger(queryVal, ctx.trigger, ctx.threshold);
|
||||
if (condition) {
|
||||
return getData(ctx, queryVal).then(function ($await_2) {
|
||||
try {
|
||||
if (ctx.feedback instanceof Error) return $return();
|
||||
findMatches(queryVal, ctx);
|
||||
if (ctx.resultsList) render(ctx);
|
||||
return $If_1.call(_this);
|
||||
} catch ($boundEx) {
|
||||
return $error($boundEx);
|
||||
}
|
||||
}, $error);
|
||||
} else {
|
||||
close(ctx);
|
||||
return $If_1.call(_this);
|
||||
}
|
||||
function $If_1() {
|
||||
return $return();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var eventsManager = function eventsManager(events, callback) {
|
||||
for (var element in events) {
|
||||
for (var event in events[element]) {
|
||||
callback(element, event);
|
||||
}
|
||||
}
|
||||
};
|
||||
var addEvents = function addEvents(ctx) {
|
||||
var events = ctx.events;
|
||||
var run = debounce(function () {
|
||||
return start(ctx);
|
||||
}, ctx.debounce);
|
||||
var publicEvents = ctx.events = _objectSpread2({
|
||||
input: _objectSpread2({}, events && events.input)
|
||||
}, ctx.resultsList && {
|
||||
list: events ? _objectSpread2({}, events.list) : {}
|
||||
});
|
||||
var privateEvents = {
|
||||
input: {
|
||||
input: function input() {
|
||||
run();
|
||||
},
|
||||
keydown: function keydown(event) {
|
||||
navigate(event, ctx);
|
||||
},
|
||||
blur: function blur() {
|
||||
close(ctx);
|
||||
}
|
||||
},
|
||||
list: {
|
||||
mousedown: function mousedown(event) {
|
||||
event.preventDefault();
|
||||
},
|
||||
click: function click$1(event) {
|
||||
click(event, ctx);
|
||||
}
|
||||
}
|
||||
};
|
||||
eventsManager(privateEvents, function (element, event) {
|
||||
if (!ctx.resultsList && event !== "input") return;
|
||||
if (publicEvents[element][event]) return;
|
||||
publicEvents[element][event] = privateEvents[element][event];
|
||||
});
|
||||
eventsManager(publicEvents, function (element, event) {
|
||||
ctx[element].addEventListener(event, publicEvents[element][event]);
|
||||
});
|
||||
};
|
||||
var removeEvents = function removeEvents(ctx) {
|
||||
eventsManager(ctx.events, function (element, event) {
|
||||
ctx[element].removeEventListener(event, ctx.events[element][event]);
|
||||
});
|
||||
};
|
||||
|
||||
function init (ctx) {
|
||||
var _this = this;
|
||||
return new Promise(function ($return, $error) {
|
||||
var placeHolder, resultsList, parentAttrs;
|
||||
placeHolder = ctx.placeHolder;
|
||||
resultsList = ctx.resultsList;
|
||||
parentAttrs = {
|
||||
role: "combobox",
|
||||
"aria-owns": resultsList.id,
|
||||
"aria-haspopup": true,
|
||||
"aria-expanded": false
|
||||
};
|
||||
create(ctx.input, _objectSpread2(_objectSpread2({
|
||||
"aria-controls": resultsList.id,
|
||||
"aria-autocomplete": "both"
|
||||
}, placeHolder && {
|
||||
placeholder: placeHolder
|
||||
}), !ctx.wrapper && _objectSpread2({}, parentAttrs)));
|
||||
if (ctx.wrapper) ctx.wrapper = create("div", _objectSpread2({
|
||||
around: ctx.input,
|
||||
"class": ctx.name + "_wrapper"
|
||||
}, parentAttrs));
|
||||
if (resultsList) ctx.list = create(resultsList.tag, _objectSpread2({
|
||||
dest: [resultsList.destination, resultsList.position],
|
||||
id: resultsList.id,
|
||||
role: "listbox",
|
||||
hidden: "hidden"
|
||||
}, resultsList["class"] && {
|
||||
"class": resultsList["class"]
|
||||
}));
|
||||
addEvents(ctx);
|
||||
if (ctx.data.cache) {
|
||||
return getData(ctx).then(function ($await_2) {
|
||||
try {
|
||||
return $If_1.call(_this);
|
||||
} catch ($boundEx) {
|
||||
return $error($boundEx);
|
||||
}
|
||||
}, $error);
|
||||
}
|
||||
function $If_1() {
|
||||
eventEmitter("init", ctx);
|
||||
return $return();
|
||||
}
|
||||
return $If_1.call(_this);
|
||||
});
|
||||
}
|
||||
|
||||
function extend (autoComplete) {
|
||||
var prototype = autoComplete.prototype;
|
||||
prototype.init = function () {
|
||||
init(this);
|
||||
};
|
||||
prototype.start = function (query) {
|
||||
start(this, query);
|
||||
};
|
||||
prototype.unInit = function () {
|
||||
if (this.wrapper) {
|
||||
var parentNode = this.wrapper.parentNode;
|
||||
parentNode.insertBefore(this.input, this.wrapper);
|
||||
parentNode.removeChild(this.wrapper);
|
||||
}
|
||||
removeEvents(this);
|
||||
};
|
||||
prototype.open = function () {
|
||||
open(this);
|
||||
};
|
||||
prototype.close = function () {
|
||||
close(this);
|
||||
};
|
||||
prototype.goTo = function (index) {
|
||||
goTo(index, this);
|
||||
};
|
||||
prototype.next = function () {
|
||||
next(this);
|
||||
};
|
||||
prototype.previous = function () {
|
||||
previous(this);
|
||||
};
|
||||
prototype.select = function (index) {
|
||||
select(this, null, index);
|
||||
};
|
||||
prototype.search = function (query, record, options) {
|
||||
return search(query, record, options);
|
||||
};
|
||||
}
|
||||
|
||||
function autoComplete(config) {
|
||||
this.options = config;
|
||||
this.id = autoComplete.instances = (autoComplete.instances || 0) + 1;
|
||||
this.name = "autoComplete";
|
||||
this.wrapper = 1;
|
||||
this.threshold = 1;
|
||||
this.debounce = 0;
|
||||
this.resultsList = {
|
||||
position: "afterend",
|
||||
tag: "ul",
|
||||
maxResults: 5
|
||||
};
|
||||
this.resultItem = {
|
||||
tag: "li"
|
||||
};
|
||||
configure(this);
|
||||
extend.call(this, autoComplete);
|
||||
init(this);
|
||||
}
|
||||
|
||||
return autoComplete;
|
||||
|
||||
})));
|
||||
@@ -1,3 +1,4 @@
|
||||
/* Modified from https://github.com/TarekRaafat/autoComplete.js (version 10.2.6)*/
|
||||
.autoComplete_wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -69,30 +69,70 @@ function $t(msgId, params={}) {
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(cookieName) {
|
||||
const name = cookieName + "=";
|
||||
let result;
|
||||
decodeURIComponent(document.cookie).split('; ').forEach(val => {
|
||||
if (val.indexOf(name) === 0) {
|
||||
result = val.substring(name.length);
|
||||
const I18n = {
|
||||
instantiateParameterizedMessages: function(data) {
|
||||
if ( data.__proto__ == Array.prototype ) {
|
||||
const result = [];
|
||||
for ( const x of data ) {
|
||||
result.push(this.instantiateParameterizedMessages(x));
|
||||
}
|
||||
return result;
|
||||
} else if ( data.__proto__ == Object.prototype ) {
|
||||
const msgId = data.msgid;
|
||||
const msgParams = data.params;
|
||||
if ( msgId && msgId.__proto__ == String.prototype && msgParams && msgParams.__proto__ == Object.prototype ) {
|
||||
return $t(msgId, msgParams);
|
||||
} else {
|
||||
const result = {};
|
||||
for ( const p in data ) {
|
||||
result[p] = this.instantiateParameterizedMessages(data[p]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
render: function (template, params) {
|
||||
params = this.instantiateParameterizedMessages(params);
|
||||
return mustache.render(template, params);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_UI_LANGUAGE = 'en';
|
||||
|
||||
Translations.load(DEFAULT_UI_LANGUAGE, /*asDefault=*/true);
|
||||
|
||||
// Below function selects the most suitable UI language from the list
|
||||
// of preferred languages in browser preferences and available translations.
|
||||
// Since, unlike Accept-Language header, navigator.languages doesn't contain
|
||||
// qvalues, they are computed using the same algorithm as in Firefox 121
|
||||
function getDefaultUserLanguage() {
|
||||
const mostSuitableLang = { code: DEFAULT_UI_LANGUAGE, score: 0 }
|
||||
const n = navigator.languages.length;
|
||||
for (const lang of uiLanguages ) {
|
||||
const rank = navigator.languages.indexOf(lang.iso_code);
|
||||
if ( rank >= 0 ) {
|
||||
const qvalue = Math.round(10*(1 - rank/n))/10;
|
||||
const score = qvalue * lang.translation_count;
|
||||
if ( score > mostSuitableLang.score ) {
|
||||
mostSuitableLang.code = lang.iso_code;
|
||||
mostSuitableLang.score = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mostSuitableLang.code;
|
||||
}
|
||||
|
||||
function getUserLanguage() {
|
||||
return new URLSearchParams(window.location.search).get('userlang')
|
||||
|| getCookie('userlang')
|
||||
|| DEFAULT_UI_LANGUAGE;
|
||||
|| window.localStorage.getItem('userlang')
|
||||
|| getDefaultUserLanguage();
|
||||
}
|
||||
|
||||
function setUserLanguage(lang, callback) {
|
||||
setPermanentGlobalCookie('userlang', lang);
|
||||
window.localStorage.setItem('userlang', lang);
|
||||
Translations.load(lang);
|
||||
Translations.whenReady(callback);
|
||||
}
|
||||
@@ -144,10 +184,8 @@ function initUILanguageSelector(activeLanguage, languageChangeCallback) {
|
||||
}
|
||||
const languageSelector = document.getElementById("ui_language");
|
||||
for (const lang of uiLanguages ) {
|
||||
const lang_name = Object.getOwnPropertyNames(lang)[0];
|
||||
const lang_code = lang[lang_name];
|
||||
const is_selected = lang_code == activeLanguage;
|
||||
languageSelector.appendChild(new Option(lang_name, lang_code, is_selected, is_selected));
|
||||
const is_selected = lang.iso_code == activeLanguage;
|
||||
languageSelector.appendChild(new Option(lang.self_name, lang.iso_code, is_selected, is_selected));
|
||||
}
|
||||
languageSelector.onchange = languageChangeCallback;
|
||||
}
|
||||
@@ -156,3 +194,4 @@ window.$t = $t;
|
||||
window.getUserLanguage = getUserLanguage;
|
||||
window.setUserLanguage = setUserLanguage;
|
||||
window.initUILanguageSelector = initUILanguageSelector;
|
||||
window.I18n = I18n;
|
||||
|
||||
@@ -1,20 +1,57 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"IMayBeABitShy",
|
||||
"Lucas Werkmeister",
|
||||
"ThisCarthing"
|
||||
]
|
||||
},
|
||||
"name": "Deutsch",
|
||||
"suggest-full-text-search": "enthält '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Buch nicht gefunden: {{BOOK_NAME}}",
|
||||
"too-many-books": "Zu viele Bücher angefragt ({{NB_BOOKS}}), die Beschränkung liegt bei {{LIMIT}}",
|
||||
"no-book-found": "Keine Bücher entsprechen den Auswahlkriterien",
|
||||
"url-not-found": "Die angeforderte URL \"{{url}}\" konnte auf diesem Server nicht gefunden werden.",
|
||||
"suggest-search": "Führe eine Volltextsuche nach <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> durch",
|
||||
"random-article-failure": "Hoppla! Konnte keinen zufälligen Artikel auswählen :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ist keine gültige Anfrage für unverarbeiteten Inhalt",
|
||||
"no-value-for-arg": "Kein Wert für den Parameter {{ARGUMENT}} gegeben",
|
||||
"no-query": "Keine Suchanfrage gegeben.",
|
||||
"raw-entry-not-found": "Eintrag {{ENTRY}} des Typs {{DATATYPE}} konnte nicht gefunden werden.",
|
||||
"400-page-title": "Ungültige Anfrage",
|
||||
"400-page-heading": "Ungültige Anfrage",
|
||||
"404-page-title": "Inhalt nicht gefunden",
|
||||
"404-page-heading": "Nicht gefunden",
|
||||
"500-page-title": "Interner Server-Fehler",
|
||||
"500-page-heading": "Interner Server-Fehler",
|
||||
"fulltext-search-unavailable": "Die Volltestsuche steht nicht zur Verfügung.",
|
||||
"no-search-results": "Die Volltextsuche ist für diesen Inhalt nicht verfügbar.",
|
||||
"library-button-text": "Zur Willkommensseite gehen",
|
||||
"home-button-text": "Zur Hauptseite von '{{BOOK_TITLE}}' gehen",
|
||||
"random-page-button-text": "Zu einer zufällig ausgewählten Seite gehen",
|
||||
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen"
|
||||
"searchbox-tooltip": "Nach '{{BOOK_TITLE}}' suchen",
|
||||
"confusion-of-tongues": "Zwei oder mehr Bücher unterschiedlicher Sprachen werden durchsucht, was zu unübersichtlichen Ergebnissen führen kann.",
|
||||
"welcome-page-overzealous-filter": "Keine Ergebnisse gefunden. Möchten Sie den <a href=\"{{URL}}\">Filter zurücksetzen</a>?",
|
||||
"powered-by-kiwix-html": "Angetrieben durch <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Suchen",
|
||||
"book-filtering-all-categories": "Alle Kategorien",
|
||||
"book-filtering-all-languages": "Alle Sprachen",
|
||||
"count-of-matching-books": "{{COUNT}} Bücher",
|
||||
"download": "Herunterladen",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"direct-download-alt-text": "direkt herunterladen",
|
||||
"hash-download-link-text": "Sha256 Hash",
|
||||
"hash-download-alt-text": "Hash herunterladen",
|
||||
"magnet-link-text": "Magnet Link",
|
||||
"magnet-alt-text": "Magnet Link herunterladen",
|
||||
"torrent-download-link-text": "Torrent-Datei",
|
||||
"torrent-download-alt-text": "Torrent herunterladen",
|
||||
"library-opds-feed-all-entries": "ODPS Feed der Bibliothek - Alle Einträge",
|
||||
"filter-by-tag": "Nach Tag \"{{TAG}}\" filtern",
|
||||
"stop-filtering-by-tag": "Filterung nach Tag \"{{TAG}}\" aufheben",
|
||||
"library-opds-feed-parameterised": "ODPS Feed der Bibliothek - Einträge mit {{#LANG}\nSprache {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}}{{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Wilkommen beim Kiwix Server",
|
||||
"download-links-heading": "Download Links für <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Buch herunterladen",
|
||||
"preview-book": "Vorschau"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
, "suggest-search" : "Make a full text search for <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>"
|
||||
, "random-article-failure" : "Oops! Failed to pick a random article :("
|
||||
, "invalid-raw-data-type" : "{{DATATYPE}} is not a valid request for raw content."
|
||||
, "invalid-request" : "The requested URL \"{{{url}}}\" is not a valid request."
|
||||
, "no-value-for-arg": "No value provided for argument {{ARGUMENT}}"
|
||||
, "no-query" : "No query provided."
|
||||
, "raw-entry-not-found" : "Cannot find {{DATATYPE}} entry {{ENTRY}}"
|
||||
@@ -21,8 +22,14 @@
|
||||
, "404-page-heading" : "Not Found"
|
||||
, "500-page-title" : "Internal Server Error"
|
||||
, "500-page-heading" : "Internal Server Error"
|
||||
, "500-page-text": "An internal server error occured. We are sorry about that :/"
|
||||
, "fulltext-search-unavailable" : "Fulltext search unavailable"
|
||||
, "no-search-results": "The fulltext search engine is not available for this content."
|
||||
, "search-results-page-title": "Search: {{SEARCH_PATTERN}}"
|
||||
, "search-results-page-header": "Results <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
|
||||
, "empty-search-results-page-header": "No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
|
||||
, "search-result-book-info": "from {{BOOK_TITLE}}"
|
||||
, "word-count": "{{COUNT}} words"
|
||||
, "library-button-text": "Go to welcome page"
|
||||
, "home-button-text": "Go to the main page of '{{BOOK_TITLE}}'"
|
||||
, "random-page-button-text": "Go to a randomly selected page"
|
||||
@@ -51,4 +58,6 @@
|
||||
, "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>"
|
||||
, "download-links-title": "Download book"
|
||||
, "preview-book": "Preview"
|
||||
, "non-translated-text": "{{MSG}}"
|
||||
, "unknown-error": "Unknown error"
|
||||
}
|
||||
|
||||
57
static/skin/i18n/es.json
Normal file
57
static/skin/i18n/es.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Fitoschido",
|
||||
"Ovruni",
|
||||
"SpikeShroom",
|
||||
"Vis4valentine"
|
||||
]
|
||||
},
|
||||
"name": "español",
|
||||
"suggest-full-text-search": "que contenga \"{{{SEARCH_TERMS}}}\"...",
|
||||
"no-such-book": "No existe el libro: {{BOOK_NAME}}",
|
||||
"too-many-books": "Demasiadas solicitudes de libros ({{NB_BOOKS}}) donde el límite es {{LIMIT}}",
|
||||
"no-book-found": "Ningún libro coincide con los criterios de selección.",
|
||||
"url-not-found": "La URL solicitada \"{{url}}\" no se encontró en este servidor.",
|
||||
"suggest-search": "Haga una búsqueda de texto completo para <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "¡Ups! Error al elegir un artículo aleatorio :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} no es una solicitud válida de contenido en crudo.",
|
||||
"no-value-for-arg": "No se ha proporcionado ningún valor para el argumento {{ARGUMENT}}",
|
||||
"no-query": "No se ha proporcionado ninguna consulta.",
|
||||
"raw-entry-not-found": "No se puede encontrar la entrada {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "Solicitud inválida",
|
||||
"400-page-heading": "Solicitud inválida",
|
||||
"404-page-title": "Contenido no encontrado",
|
||||
"404-page-heading": "No encontrado",
|
||||
"500-page-title": "Error interno del servidor",
|
||||
"500-page-heading": "Error interno del servidor",
|
||||
"fulltext-search-unavailable": "Búsqueda de texto completo no disponible",
|
||||
"no-search-results": "El motor de búsqueda de texto completo no está disponible para este contenido.",
|
||||
"library-button-text": "Ir a la página de bienvenida",
|
||||
"home-button-text": "Ir a la página principal de '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Ir a una página seleccionada al azar",
|
||||
"searchbox-tooltip": "Buscar '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Dos o más libros en diferentes idiomas participarían en la búsqueda, lo que puede llevar a resultados confusos.",
|
||||
"welcome-page-overzealous-filter": "Sin resultados. ¿Quieres <a href=\"{{URL}}\">restablecer el filtro</a> ?",
|
||||
"powered-by-kiwix-html": "Desarrollado por <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Buscar",
|
||||
"book-filtering-all-categories": "Todas las categorías",
|
||||
"book-filtering-all-languages": "Todos los idiomas",
|
||||
"count-of-matching-books": "{{COUNT}} libro(s)",
|
||||
"download": "Descargar",
|
||||
"direct-download-link-text": "Directamente",
|
||||
"direct-download-alt-text": "descarga directa",
|
||||
"hash-download-link-text": "hash sha256",
|
||||
"hash-download-alt-text": "descargar hash",
|
||||
"magnet-link-text": "Enlace magnético",
|
||||
"magnet-alt-text": "Descargar link magnético",
|
||||
"torrent-download-link-text": "Archivo de torrent",
|
||||
"torrent-download-alt-text": "descargar torrent",
|
||||
"filter-by-tag": "Filtrar por etiqueta \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Dejar de filtrar por etiqueta \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Feed OPDS de la biblioteca: entradas que coinciden con {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nEtiqueta: {{TAG}} {{/TAG}}{{#Q}}\nConsulta: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Bienvenido al servidor Kiwix",
|
||||
"download-links-heading": "Enlaces de descarga para <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Descargar libro",
|
||||
"preview-book": "Previsualizar"
|
||||
}
|
||||
@@ -2,11 +2,14 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Gomoko",
|
||||
"Stephane",
|
||||
"Thibaut120094",
|
||||
"Verdy p"
|
||||
"Verdy p",
|
||||
"Vikoula5",
|
||||
"Wladek92"
|
||||
]
|
||||
},
|
||||
"name": "français",
|
||||
"name": "Français",
|
||||
"suggest-full-text-search": "contenant « {{{SEARCH_TERMS}}} »...",
|
||||
"no-such-book": "Aucun livre avec ce nom : {{BOOK_NAME}}",
|
||||
"too-many-books": "Trop de livres demandés ({{NB_BOOKS}}) alors que la limite est de {{LIMIT}}",
|
||||
@@ -15,6 +18,7 @@
|
||||
"suggest-search": "Faire une recherche en texte intégral de « <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> »",
|
||||
"random-article-failure": "Oups ! Échec de sélection d’un article aléatoire :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} n’est pas une requête valide pour du contenu brut.",
|
||||
"invalid-request": "L'URL demandée \"{{{url}}}\" n'est pas une requête valide.",
|
||||
"no-value-for-arg": "Aucune valeur fournie pour l’argument {{ARGUMENT}}",
|
||||
"no-query": "Aucune requête fournie.",
|
||||
"raw-entry-not-found": "Impossible de trouver l’entrée « {{ENTRY}} » de type « {{DATATYPE}} »",
|
||||
@@ -24,6 +28,7 @@
|
||||
"404-page-heading": "Non trouvé",
|
||||
"500-page-title": "Erreur interne du serveur",
|
||||
"500-page-heading": "Erreur interne du serveur",
|
||||
"500-page-text": "Une erreur de serveur interne s'est produite. Nous en sommes désolés :/",
|
||||
"fulltext-search-unavailable": "Recherche en texte intégral non disponible",
|
||||
"no-search-results": "Le moteur de recherche en texte intégral n’est pas disponible pour ce contenu.",
|
||||
"library-button-text": "Aller à la page de bienvenue",
|
||||
@@ -53,5 +58,6 @@
|
||||
"welcome-to-kiwix-server": "Bienvenue sur le Serveur Kiwix",
|
||||
"download-links-heading": "Liens de téléchargement pour <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Télécharger le livre",
|
||||
"preview-book": "Aperçu"
|
||||
"preview-book": "Aperçu",
|
||||
"unknown-error": "Erreur inconnue"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"suggest-search": "לעשות חיפוש טקסט מלא עבור <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "אוי! לא עבדה בחירת ערך אקראי :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} הוא לא בקשה תקינה של תוכן גולמי.",
|
||||
"invalid-request": "הכתובת המבוקשת \"{{{url}}}\" אינה בקשה תקינה.",
|
||||
"no-value-for-arg": "לא סופק ערך לארגומנט {{ARGUMENT}}",
|
||||
"no-query": "לא סופקה שאילתה.",
|
||||
"raw-entry-not-found": "לא ניתן למצוא את רשומת ה־{{DATATYPE}} בשם {{ENTRY}}",
|
||||
@@ -23,6 +24,7 @@
|
||||
"404-page-heading": "לא נמצא",
|
||||
"500-page-title": "שגיאת שרת פנימית",
|
||||
"500-page-heading": "שגיאת שרת פנימית",
|
||||
"500-page-text": "אירעה שגיאת שרת פנימית. אנחנו מצטערים על זה :/",
|
||||
"fulltext-search-unavailable": "חיפוש בטקסט מלא אינו זמין",
|
||||
"no-search-results": "מנוע החיפוש בטקסט מלא אינו זמין עבור התוכן הזה.",
|
||||
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
|
||||
@@ -52,5 +54,6 @@
|
||||
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
|
||||
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "הורדת ספר",
|
||||
"preview-book": "תצוגה מקדימה"
|
||||
"preview-book": "תצוגה מקדימה",
|
||||
"unknown-error": "שגיאה בלתי־ידועה"
|
||||
}
|
||||
|
||||
56
static/skin/i18n/hi.json
Normal file
56
static/skin/i18n/hi.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Abijeet Patro",
|
||||
"Juuz0"
|
||||
]
|
||||
},
|
||||
"name": "हिन्दी",
|
||||
"suggest-full-text-search": "जिसमें '{{{SEARCH_TERMS}}}' शामिल है...",
|
||||
"no-such-book": "ऐसी कोई किताब नहीं: {{BOOK_NAME}}",
|
||||
"too-many-books": "बहुत सारी पुस्तकों का अनुरोध किया गया है ({{NB_BOOKS}}) जहां सीमा {{LIMIT}} है",
|
||||
"no-book-found": "कोई भी पुस्तक चयन मानदंड से मेल नहीं खाती",
|
||||
"url-not-found": "अनुरोधित URL \"{{url}}\" इस सर्वर पर नहीं मिला।",
|
||||
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> के लिए पूर्ण पाठ खोज करें",
|
||||
"random-article-failure": "उफ़! एक यादृच्छिक लेख चुनने में विफल :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} कच्ची सामग्री के लिए वैध अनुरोध नहीं है।",
|
||||
"no-value-for-arg": "तर्क के लिए कोई मूल्य प्रदान नहीं किया गया {{ARGUMENT}}",
|
||||
"no-query": "कोई प्रश्न नहीं दिया गया.",
|
||||
"raw-entry-not-found": "{{DATATYPE}} प्रविष्टि {{ENTRY}} नहीं मिल सकी",
|
||||
"400-page-title": "अवैध आवेदन",
|
||||
"400-page-heading": "अवैध आवेदन",
|
||||
"404-page-title": "सामग्री नहीं मिली",
|
||||
"404-page-heading": "नहीं मिला",
|
||||
"500-page-title": "आंतरिक सर्वर त्रुटि",
|
||||
"500-page-heading": "आंतरिक सर्वर त्रुटि",
|
||||
"fulltext-search-unavailable": "पूर्णपाठ खोज अनुपलब्ध",
|
||||
"no-search-results": "इस सामग्री के लिए पूर्णपाठ खोज इंजन उपलब्ध नहीं है।",
|
||||
"library-button-text": "स्वागत पृष्ठ पर जाएँ",
|
||||
"home-button-text": "'{{BOOK_TITLE}}' के मुख्य पृष्ठ पर जाएँ",
|
||||
"random-page-button-text": "यादृच्छिक रूप से चयनित पृष्ठ पर जाएँ",
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' खोजें",
|
||||
"confusion-of-tongues": "विभिन्न भाषाओं की दो या दो से अधिक पुस्तकें खोज में भाग लेंगी, जिससे भ्रमित करने वाले परिणाम मिल सकते हैं।",
|
||||
"welcome-page-overzealous-filter": "कोई परिणाम नहीं। क्या आप <a href=\"{{URL}}\">फ़िल्टर रीसेट करना</a> चाहेंगे?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">किविक्स</a> द्वारा संचालित",
|
||||
"search": "खोजें",
|
||||
"book-filtering-all-categories": "सब वर्ग",
|
||||
"book-filtering-all-languages": "सभी भाषाएँ",
|
||||
"count-of-matching-books": "{{COUNT}} पुस्तक/पुस्तकें",
|
||||
"download": "डाउनलोड",
|
||||
"direct-download-link-text": "प्रत्यक्ष",
|
||||
"direct-download-alt-text": "प्रत्यक्षत: डाउनलोड",
|
||||
"hash-download-link-text": "शा256 हैश",
|
||||
"hash-download-alt-text": "हैश डाउनलोड करें",
|
||||
"magnet-link-text": "MAGNET लिंक",
|
||||
"magnet-alt-text": "MAGNET डाउनलोड करें",
|
||||
"torrent-download-link-text": "टोरेंट फ़ाइल",
|
||||
"torrent-download-alt-text": "टोरेंट डाउनलोड करें",
|
||||
"library-opds-feed-all-entries": "लाइब्रेरी ओपीडीएस फ़ीड - सभी प्रविष्टियाँ",
|
||||
"filter-by-tag": "टैग \"{{TAG}}\" द्वारा फ़िल्टर करें",
|
||||
"stop-filtering-by-tag": "\"{{TAG}}\" टैग द्वारा फ़िल्टर करना बंद करें",
|
||||
"library-opds-feed-parameterised": "लाइब्रेरी ओपीडीएस फ़ीड - मिलान वाली प्रविष्टियाँ {{#LANG}}\nभाषा: {{LANG}} {/LANG}}{{#CATEGORY}}\nश्रेणी: {{CATEGORY}} {/CATEGORY}} {{#TAG}}\nटैग: {{TAG}} {{/TAG}}{{#Q}}\nक्वेरी: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "कीविक्स सर्वर में आपका स्वागत है",
|
||||
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> के लिए डाउनलोड लिंक",
|
||||
"download-links-title": "पुस्तक डाउनलोड करें",
|
||||
"preview-book": "पूर्वावलोकन"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"authors": [
|
||||
"Albano",
|
||||
"Beta16",
|
||||
"Luca.favorido",
|
||||
"McDutchie"
|
||||
]
|
||||
},
|
||||
@@ -14,6 +15,7 @@
|
||||
"url-not-found": "L'URL richiesto \"{{url}}\" non è stato trovato in questo server.",
|
||||
"suggest-search": "Effettua una ricerca di testo completo per <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ops! Impossibile selezionare un articolo casuale :(",
|
||||
"invalid-request": "L'URL richiesto \"{{{url}}}\" non è una richiesta valida.",
|
||||
"no-value-for-arg": "Nessun valore fornito per l'argomento {{ARGUMENT}}",
|
||||
"400-page-title": "Richiesta non valida",
|
||||
"400-page-heading": "Richiesta non valida",
|
||||
@@ -21,6 +23,7 @@
|
||||
"404-page-heading": "Non trovato",
|
||||
"500-page-title": "Errore interno del server",
|
||||
"500-page-heading": "Errore interno del server",
|
||||
"500-page-text": "Si è verificato un errore interno del server. Ci dispiace :/",
|
||||
"library-button-text": "Vai alla pagina di benvenuto",
|
||||
"home-button-text": "Vai alla pagina principale di '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Vai a una pagina selezionata casualmente",
|
||||
@@ -30,5 +33,6 @@
|
||||
"count-of-matching-books": "{{COUNT}} libro/i",
|
||||
"download": "Scarica",
|
||||
"download-links-title": "Scarica libro",
|
||||
"preview-book": "Anteprima"
|
||||
"preview-book": "Anteprima",
|
||||
"unknown-error": "Errore sconosciuto"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"404-page-heading": "Net fonnt",
|
||||
"500-page-title": "Interne Feeler um Server",
|
||||
"500-page-heading": "Interne Feeler um Server",
|
||||
"500-page-text": "Et ass en interne Serverfeeler opgetrueden. Mir entschëllegen eis dofir :/",
|
||||
"fulltext-search-unavailable": "Volltext-Sich net verfügbar",
|
||||
"home-button-text": "Gitt op d'Haaptsäit vun '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Gitt op eng zoufälleg gewielte Säit",
|
||||
@@ -23,5 +24,7 @@
|
||||
"book-filtering-all-languages": "All Sproochen",
|
||||
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
|
||||
"download": "Eroflueden",
|
||||
"direct-download-link-text": "Direkt"
|
||||
"direct-download-link-text": "Direkt",
|
||||
"download-links-title": "Buch eroflueden",
|
||||
"unknown-error": "Onbekannte Feeler"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"suggest-search": "Побарајте го <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> по целиот текст",
|
||||
"random-article-failure": "Упс! Не успеав да изберам случајна статија :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} не претставува важечко барање за сирова содржина.",
|
||||
"invalid-request": "Побараната URL „{{{url}}}“ не претставува важечко барање.",
|
||||
"no-value-for-arg": "Нема укажано вредност за аргументот {{ARGUMENT}}",
|
||||
"no-query": "Не е укажано барање.",
|
||||
"raw-entry-not-found": "Не можам да ја најдам {{DATATYPE}}-ставката {{ENTRY}}",
|
||||
@@ -23,6 +24,7 @@
|
||||
"404-page-heading": "Не е најдено",
|
||||
"500-page-title": "Внатрешна грешка во опслужувачот",
|
||||
"500-page-heading": "Внатрешна грешка во опслужувачот",
|
||||
"500-page-text": "Настана внатрешна грешка во опслужувачот. Жал ни е :/",
|
||||
"fulltext-search-unavailable": "Целотекстното пребарување е недостапно",
|
||||
"no-search-results": "Погонот за целотекстно пребарување не е достапен за оваа содржина.",
|
||||
"library-button-text": "Оди на воведната страница",
|
||||
@@ -52,5 +54,6 @@
|
||||
"welcome-to-kiwix-server": "Добре дојдовте на Опслужувачот на Кивикс",
|
||||
"download-links-heading": "Врски за преземање на <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Преземи книга",
|
||||
"preview-book": "Преглед"
|
||||
"preview-book": "Преглед",
|
||||
"unknown-error": "Непозната грешка"
|
||||
}
|
||||
|
||||
20
static/skin/i18n/ms.json
Normal file
20
static/skin/i18n/ms.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Tofeiku"
|
||||
]
|
||||
},
|
||||
"name": "Bahasa Melayu",
|
||||
"404-page-heading": "Tidak Dijumpai",
|
||||
"500-page-title": "Ralat Pelayan Dalaman",
|
||||
"500-page-heading": "Ralat Pelayan Dalaman",
|
||||
"library-button-text": "Pergi ke laman selamat datang",
|
||||
"searchbox-tooltip": "Cari '{{BOOK_TITLE}}'",
|
||||
"search": "Cari",
|
||||
"book-filtering-all-categories": "Semua kategori",
|
||||
"book-filtering-all-languages": "Semua bahasa",
|
||||
"download": "Muat turun",
|
||||
"direct-download-link-text": "Langsung",
|
||||
"direct-download-alt-text": "muat turun langsung",
|
||||
"download-links-title": "Muat turun buku"
|
||||
}
|
||||
@@ -6,10 +6,31 @@
|
||||
"Vistaus"
|
||||
]
|
||||
},
|
||||
"too-many-books": "Er zijn teveel boeken opgevraagd ({{NB_BOOKS}}). Het limiet is {{LIMIT}}.",
|
||||
"name": "Nederlands",
|
||||
"suggest-full-text-search": "bevat ‘{{{SEARCH_TERMS}}}’…",
|
||||
"no-such-book": "Boek bestaat niet: {{BOOK_NAME}}",
|
||||
"too-many-books": "Er zijn teveel boeken opgevraagd ({{NB_BOOKS}}). De limiet is {{LIMIT}}.",
|
||||
"no-book-found": "Er zijn geen boeken die overeenkomen met de zoekcriteria",
|
||||
"url-not-found": "De opgevraagde URL “{{url}}” is niet gevonden op deze server.",
|
||||
"suggest-search": "In volledige tekst zoeen naar <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Oeps! Kan geen willekeurig artikel kiezen :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} is geen geldig verzoek voor onbewerkte inhoud.",
|
||||
"no-value-for-arg": "Er is geen waarde opgegeven bij {{ARGUMENT}}",
|
||||
"no-query": "Er is geen zoekterm opgegeven.",
|
||||
"raw-entry-not-found": "Kan het {{DATATYPE}}-item {{ENTRY}} niet vinden",
|
||||
"400-page-title": "Ongeldig verzoek",
|
||||
"400-page-heading": "Ongeldig verzoek",
|
||||
"404-page-title": "Inhoud niet gevonden",
|
||||
"404-page-heading": "Niet gevonden",
|
||||
"500-page-title": "Interne serverfout",
|
||||
"500-page-heading": "Interne serverfout",
|
||||
"fulltext-search-unavailable": "Zoeken in volledige tekst is niet beschikbaar",
|
||||
"no-search-results": "De zoekmachine voor volledige tekst is niet beschikbaar voor deze inhoud.",
|
||||
"library-button-text": "Naar de welkomstpagina",
|
||||
"home-button-text": "Naar de hoofdpagina van ‘{{BOOK_TITLE}}’",
|
||||
"random-page-button-text": "Naar een willekeurig geselecteerde pagina gaan",
|
||||
"searchbox-tooltip": "Naar ‘{{BOOK_TITLE}}’ zoeken",
|
||||
"confusion-of-tongues": "Er zouden twee of meer boeken in verschillende talen deelnemen aan de zoekopdracht, wat tot verwarrende resultaten kan leiden.",
|
||||
"welcome-page-overzealous-filter": "Geen resultaat. Wilt u <a href=\"{{URL}}\">het filter resetten</a>?",
|
||||
"powered-by-kiwix-html": "Mogelijk gemaakt door <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Zoeken",
|
||||
@@ -25,6 +46,12 @@
|
||||
"magnet-alt-text": "magnet-link van de download",
|
||||
"torrent-download-link-text": "Torrent-bestand",
|
||||
"torrent-download-alt-text": "torrent downloaden",
|
||||
"filter-by-tag": "Filteren op tag \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Stoppen met filteren op tag \"{{TAG}}\""
|
||||
"library-opds-feed-all-entries": "OPDS-feed bibliotheek: alle vermeldingen",
|
||||
"filter-by-tag": "Filteren op label “{{TAG}}”",
|
||||
"stop-filtering-by-tag": "Niet meer filteren op label “{{TAG}}”",
|
||||
"library-opds-feed-parameterised": "OPDS-feed bibliotheek: vermeldingen die overeenkomen met {{#LANG}}\nTaal: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategorie: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nLabel: {{TAG}} {{/TAG}}{{#Q}}\nZoekopdracht: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Welkom bij de Kiwix-server",
|
||||
"download-links-heading": "Downloadkoppelingen voor <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Boek downloaden",
|
||||
"preview-book": "Voorvertoning"
|
||||
}
|
||||
|
||||
55
static/skin/i18n/or.json
Normal file
55
static/skin/i18n/or.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Gouri"
|
||||
]
|
||||
},
|
||||
"name": "ଓଡ଼ିଆ",
|
||||
"suggest-full-text-search": "${{{SEARCH_TERMS}}} ଧାରଣ କରିଛି ...",
|
||||
"no-such-book": "ଏପରି କୌଣସି ପୁସ୍ତକ ନାହିଁଃ ${{BOOK_NAME}}",
|
||||
"too-many-books": "ଅତ୍ୟଧିକ ବହି ଅନୁରୋଧ (${{NB_BOOKS}}) ଯେଉଁଠାରେ ସୀମା ${{LIMIT}} |",
|
||||
"no-book-found": "କୌଣସି ପୁସ୍ତକ ଚଯ଼ନ ମାନଦଣ୍ଡ ସହ ମେଳ ଖାଉନାହିଁ ।",
|
||||
"url-not-found": "ଅନୁରୋଧ କରାଯାଇଥିବା URL \"{{url}}\" ଏହି ସର୍ଭରରେ ମିଳିଲା ନାହିଁ |",
|
||||
"suggest-search": "<a href=\"${{{SEARCH_URL}}}\">${{PATTERN}} for</a> ପାଇଁ ଏକ ସମ୍ପୂର୍ଣ୍ଣ ପାଠ ସନ୍ଧାନ କର |",
|
||||
"random-article-failure": "ଓହୋ! ଏକ ଅନିୟମିତ ପ୍ରବନ୍ଧ ବାଛିବାରେ ବିଫଳ :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} କଞ୍ଚା ବିଷୟବସ୍ତୁ ପାଇଁ ଏକ ବ valid ଧ ଅନୁରୋଧ ନୁହେଁ |",
|
||||
"no-value-for-arg": "ଯୁକ୍ତି ପାଇଁ କୌଣସି ମୂଲ୍ଯ଼ ପ୍ରଦାନ କରାଯାଇନାହିଁ ${{ARGUMENT}}",
|
||||
"no-query": "କୌଣସି ପ୍ରଶ୍ନ ପ୍ରଦାନ କରାଯାଇନାହିଁ ।",
|
||||
"raw-entry-not-found": "${{DATATYPE}} ${{ENTRY}} କୁ ଖୋଜି ପାରୁନାହିଁ|",
|
||||
"400-page-title": "ଅବୈଧ ଅନୁରୋଧ",
|
||||
"400-page-heading": "ଅବୈଧ ଅନୁରୋଧ",
|
||||
"404-page-title": "ବିଷଯ଼ବସ୍ତୁ ମିଳୁନାହିଁ",
|
||||
"404-page-heading": "ମିଳିଲାନାହିଁ",
|
||||
"500-page-title": "ଆନ୍ତରିକ ସର୍ଭର ତୃଟି",
|
||||
"500-page-heading": "ଆନ୍ତରିକ ସର୍ଭର ତୃଟି",
|
||||
"fulltext-search-unavailable": "ପୂର୍ଣ୍ଣ ପାଠ ସନ୍ଧାନ ଉପଲବ୍ଧ ନାହିଁ |",
|
||||
"no-search-results": "ଏହି ବିଷୟବସ୍ତୁ ପାଇଁ ଫୁଲ୍ ଟେକ୍ସଟ୍ ସର୍ଚ୍ଚ ଇଞ୍ଜିନ୍ ଉପଲବ୍ଧ ନାହିଁ |",
|
||||
"library-button-text": "ସ୍ୱାଗତ ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ |",
|
||||
"home-button-text": "'{{BOOK_TITLE}}' ର ମୁଖ୍ୟ ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ |",
|
||||
"random-page-button-text": "ଏକ ଅନିୟମିତ ଭାବରେ ମନୋନୀତ ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ |",
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' ସନ୍ଧାନ କରନ୍ତୁ |",
|
||||
"confusion-of-tongues": "ବିଭିନ୍ନ ଭାଷାରେ ଦୁଇ କିମ୍ବା ଅଧିକ ପୁସ୍ତକ ସର୍ଚ୍ଚରେ ଅଂଶଗ୍ରହଣ କରିବେ ଯାହା ବିଭ୍ରାନ୍ତିକର ଫଳାଫଳ ଆଣିପାରେ |",
|
||||
"welcome-page-overzealous-filter": "କୌଣସି ଫଳାଫଳ ନାହିଁ | ଆପଣ <a href=\"{{URL}}\">ଫିଲ୍ଟର ପୁନ res ସେଟ୍</a> କରିବାକୁ ଚାହୁଁଛନ୍ତି କି?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">କିୱିକ୍ସ</a> ଦ୍ୱାରା ଚାଳିତ |",
|
||||
"search": "ଖୋଜନ୍ତୁ",
|
||||
"book-filtering-all-categories": "ସମସ୍ତ ବର୍ଗ |",
|
||||
"book-filtering-all-languages": "ସମସ୍ତ ଭାଷା",
|
||||
"count-of-matching-books": "{{COUNT}} ପୁସ୍ତକ (ଗୁଡିକ)",
|
||||
"download": "ଡାଉନଲୋଡ଼",
|
||||
"direct-download-link-text": "ସିଧାସଳଖ |",
|
||||
"direct-download-alt-text": "ସିଧାସଳଖ ଡାଉନଲୋଡ୍ |",
|
||||
"hash-download-link-text": "Sha256 ହ୍ୟାସ୍ |",
|
||||
"hash-download-alt-text": "ହ୍ୟାସ୍ ଡାଉନଲୋଡ୍ କରନ୍ତୁ |",
|
||||
"magnet-link-text": "ଚୁମ୍ବକୀୟ ଲିଙ୍କ୍",
|
||||
"magnet-alt-text": "ଚୁମ୍ବକ ଡାଉନଲୋଡ୍ କରନ୍ତୁ",
|
||||
"torrent-download-link-text": "ଟୋରେଣ୍ଟ ଫାଇଲ୍ |",
|
||||
"torrent-download-alt-text": "torrent ଡାଉନଲୋଡ୍ କରନ୍ତୁ",
|
||||
"library-opds-feed-all-entries": "ଲାଇବ୍ରେରୀ OPDS ଫିଡ୍ - ସମସ୍ତ ଏଣ୍ଟ୍ରିଗୁଡିକ |",
|
||||
"filter-by-tag": "ଟ୍ୟାଗ୍ ଦ୍ୱାରା ଫିଲ୍ଟର୍ \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "\"${{TAG}}\" ଟ୍ୟାଗ୍ ଦ୍ୱାରା ଫିଲ୍ଟର କରିବା ବନ୍ଦ କରନ୍ତୁ |",
|
||||
"library-opds-feed-parameterised": "ଲାଇବ୍ରେରୀ OPDS ଫିଡ୍ - ଏଣ୍ଟ୍ରିଗୁଡିକ {{#LANG}}! N! ଭାଷା! ${{LANG}} ! {#TAG}} Category: ${{CATEGORY}} ଟ୍ୟାଗ୍: ${{TAG}} {{/ TAG}} {{# Q}}! ପ୍ରଶ୍ନ: {{Q}} {{/ Q}}",
|
||||
"welcome-to-kiwix-server": "Kiwix ସର୍ଭରକୁ ସ୍ୱାଗତ |",
|
||||
"download-links-heading": "<b><i>${{BOOK_TITLE}} for</i></b> ପାଇଁ ଲିଙ୍କ୍ ଡାଉନଲୋଡ୍ କରନ୍ତୁ |",
|
||||
"download-links-title": "ବହି ଡାଉନଲୋଡ୍ କରନ୍ତୁ |",
|
||||
"preview-book": "ପୂର୍ବରୂପ"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Amire80",
|
||||
"Matthieu Gautier",
|
||||
"Veloman Yunkan",
|
||||
"Verdy p"
|
||||
@@ -15,6 +16,7 @@
|
||||
"suggest-search": "Suggest a search when the URL points to a non existing article",
|
||||
"random-article-failure": "Failure of the random article selection procedure",
|
||||
"invalid-raw-data-type": "Invalid DATATYPE was used with the /raw endpoint (/raw/<book>/DATATYPE/...); allowed values are 'meta' and 'content'",
|
||||
"invalid-request": "Error text for malformed URLs.",
|
||||
"no-value-for-arg": "Error text when no value has been provided for ARGUMENT in the request's query string",
|
||||
"no-query": "Error text when no query has been provided for fulltext search",
|
||||
"raw-entry-not-found": "Entry requested via the /raw endpoint was not found",
|
||||
@@ -24,8 +26,14 @@
|
||||
"404-page-heading": "Heading of the 404 error page",
|
||||
"500-page-title": "Title of the 500 error page",
|
||||
"500-page-heading": "Heading of the 500 error page",
|
||||
"500-page-text": "Text of the 500 error page",
|
||||
"fulltext-search-unavailable": "Title of the error page returned when search is attempted in a book without fulltext search database",
|
||||
"no-search-results": "Text of the error page returned when search is attempted in a book without fulltext search database",
|
||||
"search-results-page-title": "Title of the search results page",
|
||||
"search-results-page-header": "Header of the search results page",
|
||||
"empty-search-results-page-header": "Header of the empty search results page",
|
||||
"search-result-book-info": "Reference to the book where the search result belongs (this is displayed AFTER the search result)",
|
||||
"word-count": "Word count information",
|
||||
"library-button-text": "Tooltip of the button leading to the welcome page",
|
||||
"home-button-text": "Tooltip of the button leading to the main page of a book",
|
||||
"random-page-button-text": "Tooltip of the button opening a randomly selected page",
|
||||
@@ -52,5 +60,7 @@
|
||||
"welcome-to-kiwix-server": "Title shown in browser's title bar/page tab",
|
||||
"download-links-heading": "Heading for no-js download page",
|
||||
"download-links-title": "Title for no-js download page",
|
||||
"preview-book": "Tooltip of book-tile leading to the book"
|
||||
"preview-book": "Tooltip of book-tile leading to the book",
|
||||
"non-translated-text": "{{ignored}}\nUsed to display text that is generated at runtime and cannot be translated. Nothing to translate about this one.",
|
||||
"unknown-error": "Unknown error"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"Fenixs-ru",
|
||||
"Kareyac",
|
||||
"Okras",
|
||||
"Pacha Tchernof"
|
||||
"Pacha Tchernof",
|
||||
"Razno0",
|
||||
"Smavrina"
|
||||
]
|
||||
},
|
||||
"name": "русский",
|
||||
@@ -32,7 +34,23 @@
|
||||
"random-page-button-text": "Перейти на случайно выбранную страницу",
|
||||
"searchbox-tooltip": "Искать '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "В поиске будут участвовать две или более книг на разных языках, что может привести к запутанным результатам.",
|
||||
"powered-by-kiwix-html": "При поддержке <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Найти",
|
||||
"book-filtering-all-categories": "Все категории",
|
||||
"book-filtering-all-languages": "Все языки",
|
||||
"download": "Скачать"
|
||||
"count-of-matching-books": "{{COUNT}} книг(и)",
|
||||
"download": "Скачать",
|
||||
"direct-download-alt-text": "прямая загрузка",
|
||||
"hash-download-link-text": "Хэш Sha256",
|
||||
"hash-download-alt-text": "скачать хэш",
|
||||
"magnet-link-text": "Магнитная ссылка",
|
||||
"torrent-download-link-text": "Торрент-файл",
|
||||
"torrent-download-alt-text": "скачать торрент",
|
||||
"library-opds-feed-all-entries": "Канал библиотеки OPDS – все записи",
|
||||
"filter-by-tag": "Фильтровать по тегу \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Прекратить фильтрацию по тегу \"{{TAG}}\"",
|
||||
"welcome-to-kiwix-server": "Добро пожаловать на сервер Kiwix",
|
||||
"download-links-heading": "Ссылки для скачивания <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Скачать книгу",
|
||||
"preview-book": "Предпросмотр"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"suggest-search": "Preiščite celotno besedilo za <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ups! Ni bilo mogoče izbrati naključnega članka :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ni veljaven zahtevek za neobdelano vsebino.",
|
||||
"invalid-request": "Zahtevani URL »{{{url}}}« ni veljaven zahtevek.",
|
||||
"no-value-for-arg": "Argument {{ARGUMENT}} nima določene nobene vrednosti",
|
||||
"no-query": "Poizvedba ni podana.",
|
||||
"raw-entry-not-found": "Ni mogoče najti vnosa {{ENTRY}} tipa {{DATATYPE}}",
|
||||
@@ -23,6 +24,7 @@
|
||||
"404-page-heading": "Ni najdeno",
|
||||
"500-page-title": "Notranja napaka strežnika",
|
||||
"500-page-heading": "Notranja napaka strežnika",
|
||||
"500-page-text": "Prišlo je do notranje napake strežnika. Žal nam je za to. :/",
|
||||
"fulltext-search-unavailable": "Iskanje po celotnem besedilu ni na voljo",
|
||||
"no-search-results": "Iskalnik po celotnem besedilu za to vsebino ni na voljo.",
|
||||
"library-button-text": "Pojdite na pozdravno stran",
|
||||
@@ -52,5 +54,6 @@
|
||||
"welcome-to-kiwix-server": "Pozdravljeni na strežniku Kiwix",
|
||||
"download-links-heading": "Povezave za prenos za <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Prenesi knjigo",
|
||||
"preview-book": "Predogled"
|
||||
"preview-book": "Predogled",
|
||||
"unknown-error": "Neznana napaka"
|
||||
}
|
||||
|
||||
55
static/skin/i18n/sq.json
Normal file
55
static/skin/i18n/sq.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Besnik b"
|
||||
]
|
||||
},
|
||||
"name": "Shqip",
|
||||
"suggest-full-text-search": "që përmban '{{{SEARCH_TERMS}}}'…",
|
||||
"no-such-book": "S’ka libër të tillë: {{BOOK_NAME}}",
|
||||
"too-many-books": "U kërkuan shumë libra ({{NB_BOOKS}}), teksa kufiri është {{LIMIT}}",
|
||||
"no-book-found": "S’ka libër me përputhje me kriteret e përzgjedhjes",
|
||||
"url-not-found": "URL “{{url}}” e kërkuar s’u gjet në këtë shërbyes.",
|
||||
"suggest-search": "Bëni një kërkim të plotë teksti për <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Oh! S’u arrit të merrej një artikull kuturu :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} s’është varg i vlefshëm kërkimi për lëndë të papërpunuar.",
|
||||
"no-value-for-arg": "S’u dha vlerë për argumentin {{ARGUMENT}}",
|
||||
"no-query": "S’u dha varg kërkimi.",
|
||||
"raw-entry-not-found": "S’gjendet dot zëri {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "Kërkesë e pavlefshme",
|
||||
"400-page-heading": "Kërkesë e pavlefshme",
|
||||
"404-page-title": "S’u gjet lëndë",
|
||||
"404-page-heading": "S’u Gjet",
|
||||
"500-page-title": "Gabim i Brendshëm Shërbyesi",
|
||||
"500-page-heading": "Gabim i Brendshëm Shërbyesi",
|
||||
"fulltext-search-unavailable": "Kërkim teksti të plotë jo i mundshëm",
|
||||
"no-search-results": "S’është i passhëm motori i kërkimit të tekstit të plotë për këtë lëndë.",
|
||||
"library-button-text": "Kalo te faqja e mirëseardhjes",
|
||||
"home-button-text": "Kalo te faqja krye e '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Kalo te një faqe e përzgjedhur kuturu",
|
||||
"searchbox-tooltip": "Kërko në '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Kërkimi do të merrej me dy ose më tepër libra në gjuhë të ndryshme, çka mund të sjellë përfundime të ngatërruara.",
|
||||
"welcome-page-overzealous-filter": "S’ka përfundime. Do të donit të <a href=\"{{URL}}\">riujdisni filtrimin</a>?",
|
||||
"powered-by-kiwix-html": "Bazuar në <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Kërko",
|
||||
"book-filtering-all-categories": "Krejt kategoritë",
|
||||
"book-filtering-all-languages": "Krejt gjuhët",
|
||||
"count-of-matching-books": "{{COUNT}} libër(a)",
|
||||
"download": "Shkarkoje",
|
||||
"direct-download-link-text": "Drejtpërsëdrejti",
|
||||
"direct-download-alt-text": "shkarkim i drejtpërdrejt",
|
||||
"hash-download-link-text": "Hash sha256",
|
||||
"hash-download-alt-text": "shkarko hashin",
|
||||
"magnet-link-text": "Lidhje Magnet",
|
||||
"magnet-alt-text": "shkarko magnetin",
|
||||
"torrent-download-link-text": "Kartelë Torrent",
|
||||
"torrent-download-alt-text": "shkarko torrent-in",
|
||||
"library-opds-feed-all-entries": "Prurje OPDS Biblioteke - Krejt zërat",
|
||||
"filter-by-tag": "Filtroji sipas etiketës “{{TAG}}”",
|
||||
"stop-filtering-by-tag": "Resht së filtruari sipas etiketë “{{TAG}}”",
|
||||
"library-opds-feed-parameterised": "Prurje OPDS Biblioteke - zëra që kanë përputhje me {{#LANG}}\nGjuhë: {{LANG}} {{/LANG}}{{#CATEGORY}}\nKategori: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nEtiketë: {{TAG}} {{/TAG}}{{#Q}}\nVarg Kërkimi: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Mirë se vini në Shërbyesin Kiwix",
|
||||
"download-links-heading": "Lidhje shkarkimi për <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Shkarkoje librin",
|
||||
"preview-book": "Paraparje"
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
"suggest-search": "Utför en fulltextsökning för <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Hoppsan! Kunde inte välja en slumpartikel :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} är ingen giltig begäran för oformaterat innehåll.",
|
||||
"invalid-request": "Den begärda webbadressen \"{{{url}}}\" är inte en giltig begäran.",
|
||||
"no-value-for-arg": "Inget värde angett för argumentet {{ARGUMENT}}",
|
||||
"no-query": "Ingen fråga tillhandahålls.",
|
||||
"raw-entry-not-found": "Kunde inte hitta {{DATATYPE}}-inlägget {{ENTRY}}",
|
||||
@@ -24,6 +25,7 @@
|
||||
"404-page-heading": "Hittades inte",
|
||||
"500-page-title": "Internt serverfel",
|
||||
"500-page-heading": "Internt serverfel",
|
||||
"500-page-text": "Ett internt serverfel uppstod. Vi ber om ursäkt för det :/",
|
||||
"fulltext-search-unavailable": "Fulltextsökning är inte tillgänglig",
|
||||
"no-search-results": "Sökmaskinen för fulltext är inte tillgänglig för detta innehåll.",
|
||||
"library-button-text": "Gå till hemsidan",
|
||||
@@ -53,5 +55,6 @@
|
||||
"welcome-to-kiwix-server": "Välkommen till Kiwix Server",
|
||||
"download-links-heading": "Nedladdningslänkar för <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Ladda ned bok",
|
||||
"preview-book": "Förhandsgranska"
|
||||
"preview-book": "Förhandsgranska",
|
||||
"unknown-error": "Okänt fel"
|
||||
}
|
||||
|
||||
58
static/skin/i18n/te.json
Normal file
58
static/skin/i18n/te.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Amire80",
|
||||
"Chaduvari",
|
||||
"Rishitha 1238",
|
||||
"V Bhavya"
|
||||
]
|
||||
},
|
||||
"name": "ఇంగ్లీషు",
|
||||
"suggest-full-text-search": "'{{{SEARCH_TERMS}}}'ని కలిగి ఉంది...",
|
||||
"no-such-book": "అలాంటి పుస్తకం లేదు: {{BOOK_NAME}}",
|
||||
"too-many-books": "పరిమితి {{LIMIT}} ఉన్న చాలా పుస్తకాలు అభ్యర్థించబడ్డాయి ({{NB_BOOKS}})",
|
||||
"no-book-found": "ఎంపిక ప్రమాణాలకు ఏ పుస్తకం సరిపోలలేదు",
|
||||
"url-not-found": "అభ్యర్థించిన URL \"{{url}}\" ఈ సర్వర్లో కనుగొనబడలేదు.",
|
||||
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> కోసం పూర్తి వచన శోధన చేయండి",
|
||||
"random-article-failure": "అయ్యో! యాదృచ్ఛిక కథనాన్ని ఎంచుకోవడంలో విఫలమైంది :(",
|
||||
"invalid-raw-data-type": "ముడి కంటెంట్ కోసం {{DATATYPE}} చెల్లుబాటు అయ్యే అభ్యర్థన కాదు.",
|
||||
"no-value-for-arg": "ఆర్గ్యుమెంట్ {{ARGUMENT}}కి విలువ అందించబడలేదు",
|
||||
"no-query": "ప్రశ్న ఏదీ అందించబడలేదు.",
|
||||
"raw-entry-not-found": "{{DATATYPE}} ఎంట్రీ {{ENTRY}} కనుగొనబడలేదు",
|
||||
"400-page-title": "చెల్లని అభ్యర్థన",
|
||||
"400-page-heading": "చెల్లని అభ్యర్థన",
|
||||
"404-page-title": "కంటెంట్ కనుగొనబడలేదు",
|
||||
"404-page-heading": "దొరకలేదు",
|
||||
"500-page-title": "అంతర్గత సర్వర్ లోపం",
|
||||
"500-page-heading": "అంతర్గత సర్వర్ లోపం",
|
||||
"fulltext-search-unavailable": "పూర్తి వచన శోధన అందుబాటులో లేదు",
|
||||
"no-search-results": "ఈ కంటెంట్ కోసం ఫుల్టెక్స్ట్ శోధన ఇంజిన్ అందుబాటులో లేదు.",
|
||||
"library-button-text": "స్వాగత పేజీకి వెళ్లండి",
|
||||
"home-button-text": "'{{BOOK_TITLE}}' యొక్క ప్రధాన పేజీకి వెళ్లండి",
|
||||
"random-page-button-text": "యాదృచ్ఛికంగా ఎంచుకున్న పేజీకి వెళ్లండి",
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}'ని శోధించండి",
|
||||
"confusion-of-tongues": "వివిధ భాషల్లోని రెండు లేదా అంతకంటే ఎక్కువ పుస్తకాలు శోధనలో పాల్గొంటాయి, ఇది గందరగోళ ఫలితాలకు దారితీయవచ్చు.",
|
||||
"welcome-page-overzealous-filter": "ఫలితం లేదు. <a href=\"{{URL}}\">వడపోతను రీసెట్</a> చేస్తారా?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> ద్వారా ఆధారితం",
|
||||
"search": "వెతుకు",
|
||||
"book-filtering-all-categories": "వర్గాలన్నీ",
|
||||
"book-filtering-all-languages": "అన్ని భాషలు",
|
||||
"count-of-matching-books": "{{COUNT}} పుస్తకం(లు)",
|
||||
"download": "డౌన్లోడ్",
|
||||
"direct-download-link-text": "డైరెక్ట్",
|
||||
"direct-download-alt-text": "నేరుగా డౌన్లోడ్",
|
||||
"hash-download-link-text": "షా256 హాష్",
|
||||
"hash-download-alt-text": "హాష్ని డౌన్లోడ్ చేయండి",
|
||||
"magnet-link-text": "మాగ్నెట్ లింక్",
|
||||
"magnet-alt-text": "అయస్కాంతాన్ని డౌన్లోడ్ చేయండి",
|
||||
"torrent-download-link-text": "టోరెంట్ ఫైల్",
|
||||
"torrent-download-alt-text": "టొరెంట్ని డౌన్లోడ్ చేయండి",
|
||||
"library-opds-feed-all-entries": "లైబ్రరీ OPDS ఫీడ్ - అన్ని ఎంట్రీలు",
|
||||
"filter-by-tag": "\"{{TAG}}\" ట్యాగ్ ద్వారా ఫిల్టర్ చేయండి",
|
||||
"stop-filtering-by-tag": "\"{{TAG}}\" ట్యాగ్ ద్వారా ఫిల్టర్ చేయడాన్ని ఆపివేయి",
|
||||
"library-opds-feed-parameterised": "లైబ్రరీ OPDS ఫీడ్ - ఎంట్రీలు సరిపోలే {{#LANG}}\nభాష: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nట్యాగ్: {{TAG}} {{/TAG}}{{#Q}}\nప్రశ్న: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Kiwix సర్వర్కి స్వాగతం",
|
||||
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> కోసం లింక్లను డౌన్లోడ్ చేయండి",
|
||||
"download-links-title": "పుస్తకాన్ని డౌన్లోడ్ చేయండి",
|
||||
"preview-book": "ప్రివ్యూ"
|
||||
}
|
||||
@@ -40,4 +40,11 @@
|
||||
, "download-links-heading": "[I18N] Download links for <b><i>{{BOOK_TITLE}}</i></b> [TESTING]"
|
||||
, "download-links-title": "[I18N TESTING]Download book"
|
||||
, "preview-book": "[I18N] Preview [TESTING]"
|
||||
, "no-query" : "[I18N TESTING] Kiwix can read your thoughts but it is against GDPR. Please provide your query explicitly."
|
||||
, "invalid-request" : "[I18N TESTING] Invalid URL: \"{{{url}}}\""
|
||||
, "search-results-page-title": "[I18N TESTING] Search: {{SEARCH_PATTERN}}"
|
||||
, "search-results-page-header": "[I18N TESTING] Results <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
|
||||
, "empty-search-results-page-header": "[I18N TESTING] No results were found for <b>\"{{{SEARCH_PATTERN}}}\"</b>"
|
||||
, "search-result-book-info": "from [I18N TESTING] {{BOOK_TITLE}}"
|
||||
, "word-count": "{{COUNT}} [I18N TESTING] words"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"GuoPC",
|
||||
"StarrySky"
|
||||
"StarrySky",
|
||||
"Sunai",
|
||||
"XtexChooser"
|
||||
]
|
||||
},
|
||||
"name": "英语",
|
||||
@@ -12,5 +14,12 @@
|
||||
"404-page-heading": "未找到",
|
||||
"500-page-title": "内部服务器错误",
|
||||
"500-page-heading": "内部服务器错误",
|
||||
"library-button-text": "前往欢迎页面"
|
||||
"library-button-text": "前往欢迎页面",
|
||||
"search": "搜索",
|
||||
"book-filtering-all-categories": "所有分类",
|
||||
"book-filtering-all-languages": "所有语言",
|
||||
"download": "下载",
|
||||
"magnet-link-text": "磁力链接",
|
||||
"torrent-download-link-text": "种子文件",
|
||||
"preview-book": "预览"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"suggest-search": "建立 <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> 使用的全文搜尋",
|
||||
"random-article-failure": "哎呀!隨機挑選條目失敗 :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}}不是原始內容的有效請求。",
|
||||
"invalid-request": "請求的 URL「{{{url}}}」不是有效的請求。",
|
||||
"no-value-for-arg": "沒有為引數 {{ARGUMENT}} 提供內容",
|
||||
"no-query": "未提供查詢。",
|
||||
"raw-entry-not-found": "找不到{{DATATYPE}}項目{{ENTRY}}",
|
||||
@@ -24,6 +25,7 @@
|
||||
"404-page-heading": "查無頁面",
|
||||
"500-page-title": "內部伺服器錯誤",
|
||||
"500-page-heading": "內部伺服器錯誤",
|
||||
"500-page-text": "內部伺服器發生錯誤。對此我們深感抱歉:/",
|
||||
"fulltext-search-unavailable": "全文搜尋無效",
|
||||
"no-search-results": "全文搜尋引擎不適用此內容。",
|
||||
"library-button-text": "前往歡迎首頁",
|
||||
@@ -53,5 +55,6 @@
|
||||
"welcome-to-kiwix-server": "歡迎來到 Kiwix 伺服器",
|
||||
"download-links-heading": "下載<b><i>{{BOOK_TITLE}}</i></b>的連結",
|
||||
"download-links-title": "下載書籍",
|
||||
"preview-book": "預覽"
|
||||
"preview-book": "預覽",
|
||||
"unknown-error": "不明錯誤"
|
||||
}
|
||||
|
||||
@@ -1,29 +1,7 @@
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #00b4e4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.kiwixNav {
|
||||
background-color: #f4f6f8;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
padding: 10px 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
@@ -59,10 +37,10 @@ body {
|
||||
background-image: none;
|
||||
border-radius: 1px;
|
||||
width: 195px;
|
||||
height: 35px;
|
||||
height: 30px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
padding: 7px 10px 10px;
|
||||
padding: 0px 10px 0px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -74,7 +52,7 @@ body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 231px;
|
||||
height: 35px;
|
||||
height: 30px;
|
||||
line-height: 3;
|
||||
background: #909090;
|
||||
overflow: hidden;
|
||||
@@ -104,7 +82,7 @@ body {
|
||||
}
|
||||
|
||||
.kiwixSearch {
|
||||
height: 35px;
|
||||
height: 30px;
|
||||
width: 231px;
|
||||
border-radius: 10px;
|
||||
border: solid 1px #b5b2b2;
|
||||
@@ -118,7 +96,7 @@ body {
|
||||
|
||||
.kiwixButton {
|
||||
margin: 0 17px;
|
||||
height: 35px;
|
||||
height: 30px;
|
||||
width: 100px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
@@ -162,29 +140,6 @@ body {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#uiLanguageSelector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-heading {
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-content #ui_language {
|
||||
font-size: 1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#uiLanguageSelectorButton {
|
||||
margin: 0 12px 0 0;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.book__list {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
@@ -325,58 +280,6 @@ body {
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
background-color: rgba(0, 0, 0, 30%);
|
||||
}
|
||||
|
||||
.modal {
|
||||
color: #444343;
|
||||
height: 280px;
|
||||
width: 250px;
|
||||
margin: 15px;
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-heading {
|
||||
background-color: #f0f0f0;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #ececec;
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
font-family: poppins;
|
||||
}
|
||||
|
||||
.modal-close-button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content div {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
@@ -469,7 +372,7 @@ body {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.feedLogo, #uiLanguageSelectorButton {
|
||||
.feedLogo {
|
||||
height: 30px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
window.modalUILanguageSelector.close();
|
||||
const s = document.getElementById("ui_language");
|
||||
const lang = s.options[s.selectedIndex].value;
|
||||
setPermanentGlobalCookie('userlang', lang);
|
||||
localStorage.setItem('userlang', lang);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
@@ -197,6 +197,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
function makeURLSearchString(params, keysToURIEncode) {
|
||||
let output = '';
|
||||
for (const [key, value] of params.entries()) {
|
||||
let finalValue = (keysToURIEncode.indexOf(key) >= 0) ? encodeURIComponent(value) : value;
|
||||
output += `&${key}=${finalValue}`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/* hack for library.kiwix.org magnet links (created by MirrorBrain)
|
||||
See https://github.com/kiwix/container-images/issues/242 */
|
||||
async function getFixedMirrorbrainMagnet(magnetLink) {
|
||||
// parse as query parameters
|
||||
const params = new URLSearchParams(
|
||||
magnetLink.replaceAll('&', '&').replace(/^magnet:/, ''));
|
||||
|
||||
const zimUrl = params.get('as'); // as= is fallback URL
|
||||
// download metalink to build list of mirrored URLs
|
||||
let mirrorUrls = [];
|
||||
|
||||
const metalink = await fetch(`${zimUrl}.meta4`).then(response => {
|
||||
return response.ok ? response.text() : '';
|
||||
}).catch((_error) => '');
|
||||
if (metalink) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(metalink, "application/xml");
|
||||
doc.querySelectorAll("url").forEach((node) => {
|
||||
if (node.hasAttribute("priority")) { // ensures its a mirror link
|
||||
mirrorUrls.push(node.innerHTML);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// not a big deal, magnet will only contain primary URL
|
||||
console.debug(`Failed to parse mirror links for ${zimUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// set webseed (ws=) URL to primary download URL (redirects to mirror)
|
||||
params.set('ws', zimUrl);
|
||||
// if we got metalink mirror URLs, append them all
|
||||
if (mirrorUrls) {
|
||||
mirrorUrls.forEach((url) => {
|
||||
params.append('ws', url);
|
||||
});
|
||||
}
|
||||
|
||||
params.set('xs', `${zimUrl}.torrent`); // adding xs= to point to torrent URL
|
||||
|
||||
return 'magnet:?' + makeURLSearchString(params, ['ws', 'as', 'dn', 'xs', 'tr']);
|
||||
}
|
||||
|
||||
async function getMagnetLink(downloadLink) {
|
||||
const magnetUrl = downloadLink + '.magnet';
|
||||
const controller = new AbortController();
|
||||
@@ -204,6 +256,9 @@
|
||||
const magnetLink = await fetch(magnetUrl, { signal: controller.signal }).then(response => {
|
||||
return response.ok ? response.text() : '';
|
||||
}).catch((_error) => '');
|
||||
if (magnetLink) {
|
||||
return await getFixedMirrorbrainMagnet(magnetLink);
|
||||
}
|
||||
return magnetLink;
|
||||
}
|
||||
|
||||
@@ -585,11 +640,6 @@
|
||||
setInterval(updateNavVisibilityState, 250);
|
||||
};
|
||||
|
||||
// required by i18n.js:setUserLanguage()
|
||||
window.setPermanentGlobalCookie = function(name, value) {
|
||||
document.cookie = `${name}=${value};path=${root};max-age=31536000`;
|
||||
}
|
||||
|
||||
window.onload = () => { setUserLanguage(getUserLanguage(), onload); }
|
||||
})();
|
||||
|
||||
|
||||
107
static/skin/kiwix.css
Normal file
107
static/skin/kiwix.css
Normal file
@@ -0,0 +1,107 @@
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #00b4e4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
background-color: rgba(0, 0, 0, 30%);
|
||||
}
|
||||
|
||||
.modal {
|
||||
color: #444343;
|
||||
height: 280px;
|
||||
width: 250px;
|
||||
margin: 15px;
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-heading {
|
||||
background-color: #f0f0f0;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #ececec;
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
font-family: poppins;
|
||||
}
|
||||
|
||||
.modal-close-button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#uiLanguageSelector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-heading {
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-content #ui_language {
|
||||
font-size: 1.6rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#uiLanguageSelectorButton {
|
||||
margin: 0px 12px;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "poppins";
|
||||
src: url("../skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "roboto";
|
||||
src: url("../skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
@@ -1,80 +1,172 @@
|
||||
const uiLanguages = [
|
||||
{
|
||||
"الإنجليزية": "ar"
|
||||
"iso_code": "ar",
|
||||
"self_name": "الإنجليزية",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"বাংলা": "bn"
|
||||
"iso_code": "bn",
|
||||
"self_name": "বাংলা",
|
||||
"translation_count": 12
|
||||
},
|
||||
{
|
||||
"Čeština": "cs"
|
||||
"iso_code": "cs",
|
||||
"self_name": "Čeština",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"Deutsch": "de"
|
||||
"iso_code": "de",
|
||||
"self_name": "Deutsch",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"English": "en"
|
||||
"iso_code": "en",
|
||||
"self_name": "English",
|
||||
"translation_count": 58
|
||||
},
|
||||
{
|
||||
"suomi": "fi"
|
||||
"iso_code": "es",
|
||||
"self_name": "español",
|
||||
"translation_count": 48
|
||||
},
|
||||
{
|
||||
"français": "fr"
|
||||
"iso_code": "fi",
|
||||
"self_name": "suomi",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"עברית": "he"
|
||||
"iso_code": "fr",
|
||||
"self_name": "Français",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"Հայերեն": "hy"
|
||||
"iso_code": "he",
|
||||
"self_name": "עברית",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"interlingua": "ia"
|
||||
"iso_code": "hi",
|
||||
"self_name": "हिन्दी",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"italiano": "it"
|
||||
"iso_code": "hy",
|
||||
"self_name": "Հայերեն",
|
||||
"translation_count": 15
|
||||
},
|
||||
{
|
||||
"日本語": "ja"
|
||||
"iso_code": "ia",
|
||||
"self_name": "interlingua",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"한국어": "ko"
|
||||
"iso_code": "it",
|
||||
"self_name": "italiano",
|
||||
"translation_count": 29
|
||||
},
|
||||
{
|
||||
"kurdî": "ku-latn"
|
||||
"iso_code": "ja",
|
||||
"self_name": "日本語",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"Lëtzebuergesch": "lb"
|
||||
"iso_code": "ko",
|
||||
"self_name": "한국어",
|
||||
"translation_count": 13
|
||||
},
|
||||
{
|
||||
"македонски": "mk"
|
||||
"iso_code": "ku-latn",
|
||||
"self_name": "kurdî",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"ߒߞߏ": "nqo"
|
||||
"iso_code": "lb",
|
||||
"self_name": "Lëtzebuergesch",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"Polski": "pl"
|
||||
"iso_code": "mk",
|
||||
"self_name": "македонски",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"русский": "ru"
|
||||
"iso_code": "ms",
|
||||
"self_name": "Bahasa Melayu",
|
||||
"translation_count": 14
|
||||
},
|
||||
{
|
||||
"Sardu": "sc"
|
||||
"iso_code": "nl",
|
||||
"self_name": "Nederlands",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"slovenčina": "sk"
|
||||
"iso_code": "nqo",
|
||||
"self_name": "ߒߞߏ",
|
||||
"translation_count": 43
|
||||
},
|
||||
{
|
||||
"slovenščina": "sl"
|
||||
"iso_code": "or",
|
||||
"self_name": "ଓଡ଼ିଆ",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"Svenska": "sv"
|
||||
"iso_code": "pl",
|
||||
"self_name": "Polski",
|
||||
"translation_count": 24
|
||||
},
|
||||
{
|
||||
"Türkçe": "tr"
|
||||
"iso_code": "ru",
|
||||
"self_name": "русский",
|
||||
"translation_count": 45
|
||||
},
|
||||
{
|
||||
"英语": "zh-hans"
|
||||
"iso_code": "sc",
|
||||
"self_name": "Sardu",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"繁體中文": "zh-hant"
|
||||
"iso_code": "sk",
|
||||
"self_name": "slovenčina",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"iso_code": "skr-arab",
|
||||
"self_name": "سرائیکی",
|
||||
"translation_count": 20
|
||||
},
|
||||
{
|
||||
"iso_code": "sl",
|
||||
"self_name": "slovenščina",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"iso_code": "sq",
|
||||
"self_name": "Shqip",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "sv",
|
||||
"self_name": "Svenska",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"iso_code": "te",
|
||||
"self_name": "ఇంగ్లీషు",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "tr",
|
||||
"self_name": "Türkçe",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hans",
|
||||
"self_name": "英语",
|
||||
"translation_count": 16
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hant",
|
||||
"self_name": "繁體中文",
|
||||
"translation_count": 52
|
||||
}
|
||||
]
|
||||
@@ -3,8 +3,9 @@
|
||||
transition: 0.3s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #e3e3e3;
|
||||
background: #f4f6f8;
|
||||
border-bottom: 1px solid #aaa;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#kiwixtoolbar>a {
|
||||
@@ -21,8 +22,9 @@
|
||||
}
|
||||
|
||||
.kiwixsearch {
|
||||
font-size: 1.6rem;
|
||||
position: relative;
|
||||
height: 26px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 0;
|
||||
@@ -41,6 +43,7 @@
|
||||
|
||||
.kiwix .kiwix_centered {
|
||||
max-width: 720px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -48,9 +51,8 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#kiwix_button_show_toggle:checked~label~.kiwix_button_cont,
|
||||
#kiwix_button_show_toggle:checked~label~.kiwix_button_cont>a {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#kiwix_button_show_toggle:not(:checked)~label~.kiwix_button_cont {
|
||||
@@ -59,12 +61,12 @@
|
||||
|
||||
label[for="kiwix_button_show_toggle"] {
|
||||
display: inline-block;
|
||||
height: 26px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
label[for="kiwix_button_show_toggle"] img {
|
||||
transition: 0.1s;
|
||||
height: 26px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
#kiwix_button_show_toggle:checked~label img {
|
||||
@@ -83,11 +85,11 @@ label[for="kiwix_button_show_toggle"],
|
||||
.kiwix #kiwixtoolbar button,
|
||||
.kiwix #kiwixtoolbar input[type="submit"] {
|
||||
box-sizing: border-box !important;
|
||||
height: 26px !important;
|
||||
height: 30px !important;
|
||||
line-height: 20px !important;
|
||||
margin-right: 5px !important;
|
||||
padding: 2px 6px !important;
|
||||
border: 1px solid #999 !important;
|
||||
border: 1px solid #b5b2b2 !important;
|
||||
border-radius: 3px !important;
|
||||
background-color: #ededed !important;
|
||||
font-weight: normal !important;
|
||||
@@ -99,9 +101,9 @@ label[for="kiwix_button_show_toggle"],
|
||||
left: 0;
|
||||
box-sizing: border-box !important;
|
||||
width: 100%;
|
||||
height: 26px !important;
|
||||
height: 30px !important;
|
||||
line-height: 20px !important;
|
||||
border: 1px solid #999 !important;
|
||||
border: 1px solid #b5b2b2 !important;
|
||||
border-radius: 3px !important;
|
||||
background-color: #fff !important;
|
||||
padding: 2px 2px 2px 27px !important;
|
||||
@@ -114,11 +116,12 @@ label[for=kiwixsearchbox] {
|
||||
height: 100%;
|
||||
left: 5px;
|
||||
font-size: 90%;
|
||||
line-height: 26px;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
|
||||
font-size: 1.6rem;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
@@ -129,81 +132,6 @@ a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
|
||||
column-count: 1 !important;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
background-color: rgba(0, 0, 0, 30%);
|
||||
}
|
||||
|
||||
.modal {
|
||||
color: #444343;
|
||||
height: 280px;
|
||||
width: 250px;
|
||||
margin: 15px;
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-heading {
|
||||
background-color: #f0f0f0;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #ececec;
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
font-family: poppins;
|
||||
}
|
||||
|
||||
.modal-close-button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#uiLanguageSelector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-heading {
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
#uiLanguageSelector .modal-content #ui_language {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#uiLanguageSelectorButton {
|
||||
margin: 0px 12px 6px 12px;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
@media(min-width:420px) {
|
||||
.kiwix_button_cont {
|
||||
display: inline-block !important;
|
||||
@@ -250,8 +178,12 @@ a.suggest, a.suggest:visited, a.suggest:hover, a.suggest:active {
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:415px) {
|
||||
@media(max-width:419px) {
|
||||
.kiwix_searchform {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.kiwix_button_cont {
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,22 @@ let viewerState = {
|
||||
uiLanguage: 'en',
|
||||
};
|
||||
|
||||
function dropUserLang(query) {
|
||||
const q = new URLSearchParams(query);
|
||||
q.delete('userlang');
|
||||
const pre = (query.startsWith('?') && q.size != 0 ? '?' : '');
|
||||
return pre + q.toString();
|
||||
}
|
||||
|
||||
function userUrl2IframeUrl(url) {
|
||||
if ( url == '' ) {
|
||||
return blankPageUrl;
|
||||
}
|
||||
|
||||
if ( url.startsWith('search?') ) {
|
||||
return `${root}/${url}`;
|
||||
const q = new URLSearchParams(url.slice("search?".length));
|
||||
q.set('userlang', viewerState.uiLanguage);
|
||||
return `${root}/search?${q.toString()}`;
|
||||
}
|
||||
|
||||
return `${root}/content/${url}`;
|
||||
@@ -73,7 +82,7 @@ function quasiUriEncode(s, specialSymbols) {
|
||||
function performSearch() {
|
||||
const searchbox = document.getElementById('kiwixsearchbox');
|
||||
const q = encodeURIComponent(searchbox.value);
|
||||
gotoUrl(`/search?books.name=${currentBook}&pattern=${q}`);
|
||||
gotoUrl(`/search?books.name=${currentBook}&pattern=${q}&userlang=${viewerState.uiLanguage}`);
|
||||
}
|
||||
|
||||
function makeJSLink(jsCodeString, linkText, linkAttr="") {
|
||||
@@ -148,7 +157,7 @@ function iframeUrl2UserUrl(url, query) {
|
||||
}
|
||||
|
||||
if ( url == `${root}/search` ) {
|
||||
return `search${query}`;
|
||||
return `search${dropUserLang(query)}`;
|
||||
}
|
||||
|
||||
url = url.slice(root.length);
|
||||
@@ -249,6 +258,25 @@ function handle_location_hash_change() {
|
||||
history.replaceState(viewerState, null);
|
||||
}
|
||||
|
||||
function translateErrorPageIfNeeded() {
|
||||
const cw = contentIframe.contentWindow;
|
||||
if ( cw.KIWIX_RESPONSE_TEMPLATE && cw.KIWIX_RESPONSE_DATA ) {
|
||||
const template = htmlDecode(cw.KIWIX_RESPONSE_TEMPLATE);
|
||||
|
||||
// cw.KIWIX_RESPONSE_DATA belongs to the iframe context and running
|
||||
// I18n.render() on it directly in the top context doesn't work correctly
|
||||
// because the type checks (obj.__proto__ == ???.prototype) in
|
||||
// I18n.instantiateParameterizedMessages() always fail (String.prototype
|
||||
// refers to different objects in different contexts).
|
||||
// Work arround that issue by copying the object into our context.
|
||||
const params = JSON.parse(JSON.stringify(cw.KIWIX_RESPONSE_DATA));
|
||||
|
||||
const html = I18n.render(template, params);
|
||||
const htmlDoc = new DOMParser().parseFromString(html, "text/html");
|
||||
cw.document.documentElement.innerHTML = htmlDoc.documentElement.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
function handle_content_url_change() {
|
||||
const iframeLocation = contentIframe.contentWindow.location;
|
||||
console.log('handle_content_url_change: ' + iframeLocation.href);
|
||||
@@ -258,6 +286,7 @@ function handle_content_url_change() {
|
||||
const newHash = iframeUrl2UserUrl(iframeContentUrl, iframeContentQuery);
|
||||
history.replaceState(viewerState, null, makeURL(location.search, newHash));
|
||||
updateCurrentBookIfNeeded(newHash);
|
||||
translateErrorPageIfNeeded();
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -290,12 +319,23 @@ function isExternalUrl(url) {
|
||||
|| url.startsWith("https:");
|
||||
}
|
||||
|
||||
function getRealHref(target) {
|
||||
// In case of wombat in the middle, wombat will rewrite the href value to the original url (external link)
|
||||
// This is not what we want. Let's ask wombat to not rewrite href
|
||||
const old_no_rewrite = target._no_rewrite;
|
||||
target._no_rewrite = true;
|
||||
const target_href = target.href;
|
||||
target._no_rewrite = old_no_rewrite;
|
||||
return target_href;
|
||||
}
|
||||
|
||||
function onClickEvent(e) {
|
||||
const iframeDocument = contentIframe.contentDocument;
|
||||
const target = matchingAncestorElement(e.target, iframeDocument, "a");
|
||||
if (target !== null && "href" in target) {
|
||||
if ( isExternalUrl(target.href) ) {
|
||||
const possiblyBlockedLink = blockLink(target.href);
|
||||
const target_href = getRealHref(target);
|
||||
if (isExternalUrl(target_href)) {
|
||||
const possiblyBlockedLink = blockLink(target_href);
|
||||
if ( e.ctrlKey || e.shiftKey ) {
|
||||
// The link will be loaded in a new tab/window - update the link
|
||||
// and let the browser handle the rest.
|
||||
@@ -485,6 +525,7 @@ function changeUILanguage() {
|
||||
viewerState.uiLanguage = lang;
|
||||
setUserLanguage(lang, () => {
|
||||
updateUIText();
|
||||
translateErrorPageIfNeeded();
|
||||
history.pushState(viewerState, null);
|
||||
});
|
||||
}
|
||||
@@ -505,9 +546,8 @@ function setupViewer() {
|
||||
const lang = getUserLanguage();
|
||||
setUserLanguage(lang, finishViewerSetupOnceTranslationsAreLoaded);
|
||||
viewerState.uiLanguage = lang;
|
||||
const q = new URLSearchParams(window.location.search);
|
||||
q.delete('userlang');
|
||||
const rewrittenURL = makeURL(q.toString(), location.hash);
|
||||
const cleanedUpQuery = dropUserLang(window.location.search);
|
||||
const rewrittenURL = makeURL(cleanedUpQuery, location.hash);
|
||||
history.replaceState(viewerState, null, rewrittenURL);
|
||||
|
||||
kiwixToolBarWrapper.style.display = 'block';
|
||||
@@ -549,7 +589,3 @@ function finishViewerSetupOnceTranslationsAreLoaded()
|
||||
|
||||
viewerSetupComplete = true;
|
||||
}
|
||||
|
||||
function setPermanentGlobalCookie(name, value) {
|
||||
document.cookie = `${name}=${value};path=${root};max-age=31536000`;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
{{#CSS_URL}}
|
||||
<link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />
|
||||
{{/CSS_URL}}
|
||||
{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} <script>
|
||||
window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";
|
||||
window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};
|
||||
</script>{{/KIWIX_RESPONSE_DATA}}
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{PAGE_HEADING}}</h1>
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link type="root" href="{{root}}">
|
||||
<title>Welcome to Kiwix Server</title>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/kiwix.css?KIWIXCACHEID"
|
||||
rel="Stylesheet"
|
||||
/>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/index.css?KIWIXCACHEID"
|
||||
@@ -26,17 +31,7 @@
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="msapplication-config" content="{{root}}/skin/favicon/browserconfig.xml?KIWIXCACHEID">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "poppins";
|
||||
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "roboto";
|
||||
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript" src="./viewer_settings.js"></script>
|
||||
<script type="module" src="{{root}}/skin/i18n.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/languages.js?KIWIXCACHEID" defer></script>
|
||||
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link type="root" href="{{root}}">
|
||||
<title>{{translations.welcome-to-kiwix-server}}</title>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/kiwix.css?KIWIXCACHEID"
|
||||
rel="Stylesheet"
|
||||
/>
|
||||
<link
|
||||
type="text/css"
|
||||
href="{{root}}/skin/index.css?KIWIXCACHEID"
|
||||
@@ -137,4 +142,4 @@
|
||||
</div>
|
||||
<div id="kiwixfooter" class="kiwixfooter">{{{translations.powered-by-kiwix-html}}}</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -102,23 +102,11 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
<title>Search: {{query.pattern}}</title>
|
||||
<title>{{PAGE_TITLE}}</title>
|
||||
</head>
|
||||
<body bgcolor="white">
|
||||
<div class="header">
|
||||
{{#results.hasResults}}
|
||||
Results
|
||||
<b>
|
||||
{{results.start}}-{{results.end}}
|
||||
</b> of <b>
|
||||
{{results.count}}
|
||||
</b> for <b>
|
||||
"{{{query.pattern}}}"
|
||||
</b>
|
||||
{{/results.hasResults}}
|
||||
{{^results.hasResults}}
|
||||
No results were found for <b>"{{{query.pattern}}}"</b>
|
||||
{{/results.hasResults}}
|
||||
{{{PAGE_HEADER}}}
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
@@ -131,12 +119,12 @@
|
||||
{{#snippet}}
|
||||
<cite>{{>snippet}}...</cite>
|
||||
{{/snippet}}
|
||||
{{#bookTitle}}
|
||||
<div class="book-title">from {{bookTitle}}</div>
|
||||
{{/bookTitle}}
|
||||
{{#wordCount}}
|
||||
<div class="informations">{{wordCount}} words</div>
|
||||
{{/wordCount}}
|
||||
{{#bookInfo}}
|
||||
<div class="book-title">{{bookInfo}}</div>
|
||||
{{/bookInfo}}
|
||||
{{#wordCountInfo}}
|
||||
<div class="informations">{{wordCountInfo}}</div>
|
||||
{{/wordCountInfo}}
|
||||
</li>
|
||||
{{/results.items}}
|
||||
</ul>
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
object-src 'none';">
|
||||
<title>ZIM Viewer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link type="text/css" href="./skin/kiwix.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/taskbar.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="./viewer_settings.js"></script>
|
||||
<script type="module" src="./skin/i18n.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="./skin/languages.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="./skin/viewer.js?KIWIXCACHEID" defer></script>
|
||||
<script type="text/javascript" src="./skin/autoComplete.min.js?KIWIXCACHEID"></script>
|
||||
<script type="text/javascript" src="./skin/autoComplete/autoComplete.min.js?KIWIXCACHEID"></script>
|
||||
<script>
|
||||
function getRootLocation() {
|
||||
const p = location.pathname;
|
||||
@@ -34,13 +35,6 @@
|
||||
<div class="kiwix" style="display:none" id="kiwixtoolbarwrapper">
|
||||
<div id="kiwixtoolbar" class="ui-widget-header">
|
||||
<div class="kiwix_centered">
|
||||
<a id="uiLanguageSelectorButton"
|
||||
onclick="window.modalUILanguageSelector.show()"
|
||||
alt="Select UI language"
|
||||
aria-label="Select UI language"
|
||||
title="Select UI language">
|
||||
<img src="./skin/langSelector.svg?KIWIXCACHEID">
|
||||
</a>
|
||||
<div class="kiwix_searchform">
|
||||
<form class="kiwixsearch" method="GET" action="javascript:performSearch()" id="kiwixsearchform">
|
||||
<label for="kiwixsearchbox">🔍</label>
|
||||
@@ -65,6 +59,13 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a onclick="window.modalUILanguageSelector.show()"
|
||||
alt="Select UI language"
|
||||
aria-label="Select UI language"
|
||||
title="Select UI language">
|
||||
<img id="uiLanguageSelectorButton"
|
||||
src="./skin/langSelector.svg?KIWIXCACHEID">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1516,7 +1516,7 @@ inline bool bind_ip_address(socket_t sock, const char *host) {
|
||||
}
|
||||
|
||||
inline std::string if2ip(const std::string &ifn) {
|
||||
#ifndef _WIN32
|
||||
#if !defined(_WIN32) && !defined(__HAIKU__)
|
||||
struct ifaddrs *ifap;
|
||||
getifaddrs(&ifap);
|
||||
for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) {
|
||||
|
||||
50
test/i18n.cpp
Normal file
50
test/i18n.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "../src/server/i18n.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace kiwix;
|
||||
|
||||
TEST(ParameterizedMessage, parameterlessMessages)
|
||||
{
|
||||
{
|
||||
const ParameterizedMessage msg("404-page-title", {});
|
||||
|
||||
EXPECT_EQ(msg.getText("en"), "Content not found");
|
||||
EXPECT_EQ(msg.getText("test"), "[I18N TESTING] Not Found - Try Again");
|
||||
}
|
||||
|
||||
{
|
||||
// Make sure that msgId influences the result of getText()
|
||||
const ParameterizedMessage msg("random-page-button-text", {});
|
||||
|
||||
EXPECT_EQ(msg.getText("en"), "Go to a randomly selected page");
|
||||
EXPECT_EQ(msg.getText("test"), "[I18N TESTING] I am tired of determinism");
|
||||
}
|
||||
|
||||
{
|
||||
// Demonstrate that unwanted parameters are silently ignored
|
||||
const ParameterizedMessage msg("404-page-title", {{"abc", "xyz"}});
|
||||
|
||||
EXPECT_EQ(msg.getText("en"), "Content not found");
|
||||
EXPECT_EQ(msg.getText("test"), "[I18N TESTING] Not Found - Try Again");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParameterizedMessage, messagesWithParameters)
|
||||
{
|
||||
{
|
||||
const ParameterizedMessage msg("filter-by-tag",
|
||||
{{"TAG", "scifi"}}
|
||||
);
|
||||
|
||||
EXPECT_EQ(msg.getText("en"), "Filter by tag \"scifi\"");
|
||||
EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"scifi\"");
|
||||
}
|
||||
|
||||
{
|
||||
// Omitting expected parameters amounts to using empty values for them
|
||||
const ParameterizedMessage msg("filter-by-tag", {});
|
||||
|
||||
EXPECT_EQ(msg.getText("en"), "Filter by tag \"\"");
|
||||
EXPECT_EQ(msg.getText("test"), "Filter [I18N] by [TESTING] tag \"\"");
|
||||
}
|
||||
}
|
||||
41
test/languageTools.cpp
Normal file
41
test/languageTools.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Nikhil Tanwar (2002nikhiltanwar@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
|
||||
* NON-INFRINGEMENT. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "../include/tools.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
TEST(LanguageToolsTest, englishTest)
|
||||
{
|
||||
EXPECT_EQ(kiwix::getLanguageSelfName("eng"), "English");
|
||||
}
|
||||
|
||||
TEST(LanguageToolsTest, manualValuesTest)
|
||||
{
|
||||
EXPECT_EQ(kiwix::getLanguageSelfName("dty"), "डोटेली");
|
||||
}
|
||||
|
||||
TEST(LanguageToolsTest, emptyStringTest)
|
||||
{
|
||||
EXPECT_EQ(kiwix::getLanguageSelfName(""), "Undetermined");
|
||||
}
|
||||
|
||||
}
|
||||
141
test/library.cpp
141
test/library.cpp
@@ -218,7 +218,7 @@ const char sampleLibraryXML[] = R"(
|
||||
creator="Wikibooks"
|
||||
publisher="Kiwix & Some Enthusiasts"
|
||||
date="2021-04-11"
|
||||
name="wikibooks_de"
|
||||
name="wikibooks.de"
|
||||
tags="unittest;wikibooks;_category:wikibooks"
|
||||
articleCount="12"
|
||||
mediaCount="0"
|
||||
@@ -238,14 +238,14 @@ typedef std::vector<std::string> Langs;
|
||||
|
||||
TEST(LibraryOpdsImportTest, allInOne)
|
||||
{
|
||||
kiwix::Library lib;
|
||||
kiwix::Manager manager(&lib);
|
||||
auto lib = kiwix::Library::create();
|
||||
kiwix::Manager manager(lib);
|
||||
manager.readOpds(sampleOpdsStream, "library-opds-import.unittests.dev");
|
||||
|
||||
EXPECT_EQ(10U, lib.getBookCount(true, true));
|
||||
EXPECT_EQ(10U, lib->getBookCount(true, true));
|
||||
|
||||
{
|
||||
const kiwix::Book& book1 = lib.getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd");
|
||||
const kiwix::Book& book1 = lib->getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd");
|
||||
|
||||
EXPECT_EQ(book1.getTitle(), "Encyclopédie de la Tunisie");
|
||||
EXPECT_EQ(book1.getName(), "wikipedia_fr_tunisie_novid_2018-10");
|
||||
@@ -271,7 +271,7 @@ TEST(LibraryOpdsImportTest, allInOne)
|
||||
}
|
||||
|
||||
{
|
||||
const kiwix::Book& book2 = lib.getBookById("0189d9be-2fd0-b4b6-7300-20fab0b5cdc8");
|
||||
const kiwix::Book& book2 = lib->getBookById("0189d9be-2fd0-b4b6-7300-20fab0b5cdc8");
|
||||
EXPECT_EQ(book2.getTitle(), "TED talks - Business");
|
||||
EXPECT_EQ(book2.getName(), "");
|
||||
EXPECT_EQ(book2.getFlavour(), "");
|
||||
@@ -301,8 +301,10 @@ class LibraryTest : public ::testing::Test {
|
||||
typedef kiwix::Library::BookIdCollection BookIdCollection;
|
||||
typedef std::vector<std::string> TitleCollection;
|
||||
|
||||
LibraryTest(): lib(kiwix::Library::create()) {}
|
||||
|
||||
void SetUp() override {
|
||||
kiwix::Manager manager(&lib);
|
||||
kiwix::Manager manager(lib);
|
||||
manager.readOpds(sampleOpdsStream, "foo.urlHost");
|
||||
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
|
||||
}
|
||||
@@ -316,25 +318,25 @@ class LibraryTest : public ::testing::Test {
|
||||
TitleCollection ids2Titles(const BookIdCollection& ids) {
|
||||
TitleCollection titles;
|
||||
for ( const auto& bookId : ids ) {
|
||||
titles.push_back(lib.getBookById(bookId).getTitle());
|
||||
titles.push_back(lib->getBookById(bookId).getTitle());
|
||||
}
|
||||
std::sort(titles.begin(), titles.end());
|
||||
return titles;
|
||||
}
|
||||
|
||||
kiwix::Library lib;
|
||||
std::shared_ptr<kiwix::Library> lib;
|
||||
};
|
||||
|
||||
TEST_F(LibraryTest, getBookMarksTest)
|
||||
{
|
||||
auto bookId1 = lib.getBooksIds()[0];
|
||||
auto bookId2 = lib.getBooksIds()[1];
|
||||
auto bookId1 = lib->getBooksIds()[0];
|
||||
auto bookId2 = lib->getBooksIds()[1];
|
||||
|
||||
lib.addBookmark(createBookmark(bookId1));
|
||||
lib.addBookmark(createBookmark("invalid-bookmark-id"));
|
||||
lib.addBookmark(createBookmark(bookId2));
|
||||
auto onlyValidBookmarks = lib.getBookmarks();
|
||||
auto allBookmarks = lib.getBookmarks(false);
|
||||
lib->addBookmark(createBookmark(bookId1));
|
||||
lib->addBookmark(createBookmark("invalid-bookmark-id"));
|
||||
lib->addBookmark(createBookmark(bookId2));
|
||||
auto onlyValidBookmarks = lib->getBookmarks();
|
||||
auto allBookmarks = lib->getBookmarks(false);
|
||||
|
||||
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
|
||||
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
|
||||
@@ -346,11 +348,11 @@ TEST_F(LibraryTest, getBookMarksTest)
|
||||
|
||||
TEST_F(LibraryTest, sanityCheck)
|
||||
{
|
||||
EXPECT_EQ(lib.getBookCount(true, true), 12U);
|
||||
EXPECT_EQ(lib.getBooksLanguages(),
|
||||
EXPECT_EQ(lib->getBookCount(true, true), 12U);
|
||||
EXPECT_EQ(lib->getBooksLanguages(),
|
||||
std::vector<std::string>({"deu", "eng", "fra", "ita", "spa"})
|
||||
);
|
||||
EXPECT_EQ(lib.getBooksCreators(), std::vector<std::string>({
|
||||
EXPECT_EQ(lib->getBooksCreators(), std::vector<std::string>({
|
||||
"Islam Stack Exchange",
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
@@ -361,7 +363,7 @@ TEST_F(LibraryTest, sanityCheck)
|
||||
"Wikipedia",
|
||||
"Wikiquote"
|
||||
}));
|
||||
EXPECT_EQ(lib.getBooksPublishers(), std::vector<std::string>({
|
||||
EXPECT_EQ(lib->getBooksPublishers(), std::vector<std::string>({
|
||||
"",
|
||||
"Kiwix",
|
||||
"Kiwix & Some Enthusiasts",
|
||||
@@ -371,22 +373,22 @@ TEST_F(LibraryTest, sanityCheck)
|
||||
|
||||
TEST_F(LibraryTest, categoryHandling)
|
||||
{
|
||||
EXPECT_EQ("", lib.getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd").getCategory());
|
||||
EXPECT_EQ("category_defined_via_tags_only", lib.getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2").getCategory());
|
||||
EXPECT_EQ("category_defined_via_category_element_only", lib.getBookById("0ea1cde6-441d-6c58-f2c7-21c2838e659f").getCategory());
|
||||
EXPECT_EQ("category_element_overrides_tags", lib.getBookById("1123e574-6eef-6d54-28fc-13e4caeae474").getCategory());
|
||||
EXPECT_EQ("category_element_overrides_tags", lib.getBookById("14829621-c490-c376-0792-9de558b57efa").getCategory());
|
||||
EXPECT_EQ("", lib->getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd").getCategory());
|
||||
EXPECT_EQ("category_defined_via_tags_only", lib->getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2").getCategory());
|
||||
EXPECT_EQ("category_defined_via_category_element_only", lib->getBookById("0ea1cde6-441d-6c58-f2c7-21c2838e659f").getCategory());
|
||||
EXPECT_EQ("category_element_overrides_tags", lib->getBookById("1123e574-6eef-6d54-28fc-13e4caeae474").getCategory());
|
||||
EXPECT_EQ("category_element_overrides_tags", lib->getBookById("14829621-c490-c376-0792-9de558b57efa").getCategory());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, emptyFilter)
|
||||
{
|
||||
const auto bookIds = lib.filter(kiwix::Filter());
|
||||
EXPECT_EQ(bookIds, lib.getBooksIds());
|
||||
const auto bookIds = lib->filter(kiwix::Filter());
|
||||
EXPECT_EQ(bookIds, lib->getBooksIds());
|
||||
}
|
||||
|
||||
#define EXPECT_FILTER_RESULTS(f, ...) \
|
||||
EXPECT_EQ( \
|
||||
ids2Titles(lib.filter(f)), \
|
||||
ids2Titles(lib->filter(f)), \
|
||||
TitleCollection({ __VA_ARGS__ }) \
|
||||
)
|
||||
|
||||
@@ -678,17 +680,38 @@ TEST_F(LibraryTest, filterByPublisher)
|
||||
|
||||
TEST_F(LibraryTest, filterByName)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikibooks_de"),
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikibooks.de"),
|
||||
"An example ZIM archive"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("name:wikibooks_de"),
|
||||
"An example ZIM archive"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("wikibooks_de"),
|
||||
// Parsing the query with `name:` prefix splits the token on the dot, as if it was 2 sentences.
|
||||
// It creates a query "XNwikibook@1 PHRASE 2 XNde@2".
|
||||
// I haven't found the syntax to not split on dot.
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("name:wikibooks.de"),
|
||||
/* no results */
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikibooks"),
|
||||
/* no results */
|
||||
);
|
||||
|
||||
// Wikibooks is in `tags` so it matches.
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("wikibooks"),
|
||||
"An example ZIM archive"
|
||||
);
|
||||
|
||||
// But "wikibooks.de" is only in name and `query` doesn't looks in name.
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("wikibooks.de"),
|
||||
/* no results */
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikipedia_en_ray_charles"),
|
||||
"Ray Charles"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("name:wikipedia_en_ray_charles"),
|
||||
"Ray Charles"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, filterByCategory)
|
||||
@@ -736,33 +759,33 @@ TEST_F(LibraryTest, filterByMultipleCriteria)
|
||||
|
||||
TEST_F(LibraryTest, getBookByPath)
|
||||
{
|
||||
kiwix::Book book = lib.getBookById(lib.getBooksIds()[0]);
|
||||
kiwix::Book book = lib->getBookById(lib->getBooksIds()[0]);
|
||||
#ifdef _WIN32
|
||||
auto path = "C:\\some\\abs\\path.zim";
|
||||
#else
|
||||
auto path = "/some/abs/path.zim";
|
||||
#endif
|
||||
book.setPath(path);
|
||||
lib.addBook(book);
|
||||
EXPECT_EQ(lib.getBookByPath(path).getId(), book.getId());
|
||||
EXPECT_THROW(lib.getBookByPath("non/existant/path.zim"), std::out_of_range);
|
||||
lib->addBook(book);
|
||||
EXPECT_EQ(lib->getBookByPath(path).getId(), book.getId());
|
||||
EXPECT_THROW(lib->getBookByPath("non/existant/path.zim"), std::out_of_range);
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, removeBookByIdRemovesTheBook)
|
||||
{
|
||||
const auto initialBookCount = lib.getBookCount(true, true);
|
||||
const auto initialBookCount = lib->getBookCount(true, true);
|
||||
ASSERT_GT(initialBookCount, 0U);
|
||||
EXPECT_NO_THROW(lib.getBookById("raycharles"));
|
||||
lib.removeBookById("raycharles");
|
||||
EXPECT_EQ(initialBookCount - 1, lib.getBookCount(true, true));
|
||||
EXPECT_THROW(lib.getBookById("raycharles"), std::out_of_range);
|
||||
EXPECT_NO_THROW(lib->getBookById("raycharles"));
|
||||
lib->removeBookById("raycharles");
|
||||
EXPECT_EQ(initialBookCount - 1, lib->getBookCount(true, true));
|
||||
EXPECT_THROW(lib->getBookById("raycharles"), std::out_of_range);
|
||||
};
|
||||
|
||||
TEST_F(LibraryTest, removeBookByIdDropsTheReader)
|
||||
{
|
||||
EXPECT_NE(nullptr, lib.getArchiveById("raycharles"));
|
||||
lib.removeBookById("raycharles");
|
||||
EXPECT_THROW(lib.getArchiveById("raycharles"), std::out_of_range);
|
||||
EXPECT_NE(nullptr, lib->getArchiveById("raycharles"));
|
||||
lib->removeBookById("raycharles");
|
||||
EXPECT_THROW(lib->getArchiveById("raycharles"), std::out_of_range);
|
||||
};
|
||||
|
||||
TEST_F(LibraryTest, removeBookByIdUpdatesTheSearchDB)
|
||||
@@ -770,17 +793,17 @@ TEST_F(LibraryTest, removeBookByIdUpdatesTheSearchDB)
|
||||
kiwix::Filter f;
|
||||
f.local(true).valid(true).query(R"(title:"ray charles")", false);
|
||||
|
||||
EXPECT_NO_THROW(lib.getBookById("raycharles"));
|
||||
EXPECT_EQ(1U, lib.filter(f).size());
|
||||
EXPECT_NO_THROW(lib->getBookById("raycharles"));
|
||||
EXPECT_EQ(1U, lib->filter(f).size());
|
||||
|
||||
lib.removeBookById("raycharles");
|
||||
lib->removeBookById("raycharles");
|
||||
|
||||
EXPECT_THROW(lib.getBookById("raycharles"), std::out_of_range);
|
||||
EXPECT_EQ(0U, lib.filter(f).size());
|
||||
EXPECT_THROW(lib->getBookById("raycharles"), std::out_of_range);
|
||||
EXPECT_EQ(0U, lib->filter(f).size());
|
||||
|
||||
// make sure that Library::filter() doesn't add an empty book with
|
||||
// an id surviving in the search DB
|
||||
EXPECT_THROW(lib.getBookById("raycharles"), std::out_of_range);
|
||||
EXPECT_THROW(lib->getBookById("raycharles"), std::out_of_range);
|
||||
};
|
||||
|
||||
TEST_F(LibraryTest, removeBooksNotUpdatedSince)
|
||||
@@ -800,18 +823,18 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
|
||||
"Wikiquote"
|
||||
);
|
||||
|
||||
const uint64_t rev = lib.getRevision();
|
||||
for ( const auto& id : lib.filter(kiwix::Filter().query("exchange")) ) {
|
||||
lib.addBook(lib.getBookByIdThreadSafe(id));
|
||||
const uint64_t rev = lib->getRevision();
|
||||
for ( const auto& id : lib->filter(kiwix::Filter().query("exchange")) ) {
|
||||
lib->addBook(lib->getBookByIdThreadSafe(id));
|
||||
}
|
||||
|
||||
EXPECT_GT(lib.getRevision(), rev);
|
||||
EXPECT_GT(lib->getRevision(), rev);
|
||||
|
||||
const uint64_t rev2 = lib.getRevision();
|
||||
const uint64_t rev2 = lib->getRevision();
|
||||
|
||||
EXPECT_EQ(9u, lib.removeBooksNotUpdatedSince(rev));
|
||||
EXPECT_EQ(9u, lib->removeBooksNotUpdatedSince(rev));
|
||||
|
||||
EXPECT_GT(lib.getRevision(), rev2);
|
||||
EXPECT_GT(lib->getRevision(), rev2);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter(),
|
||||
"Islam Stack Exchange",
|
||||
|
||||
@@ -301,20 +301,41 @@ TEST_F(LibraryServerTest, catalog_search_by_tag)
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_by_category)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/search?category=jazz");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (category=jazz)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/search?category=jazz");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (category=jazz)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/search?category=jazz,wikipedia");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
OPDS_FEED_TAG
|
||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||
" <title>Filtered zims (category=jazz%2Cwikipedia)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
CATALOG_LINK_TAGS
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_search_by_language)
|
||||
@@ -793,6 +814,40 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_language)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_category)
|
||||
{
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?category=jazz");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?category=jazz")
|
||||
" <title>Filtered Entries (category=jazz)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>1</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>1</itemsPerPage>\n"
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries?category=jazz,wikipedia");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("?category=jazz%2Cwikipedia")
|
||||
" <title>Filtered Entries (category=jazz%2Cwikipedia)</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
" <totalResults>2</totalResults>\n"
|
||||
" <startIndex>0</startIndex>\n"
|
||||
" <itemsPerPage>2</itemsPerPage>\n"
|
||||
RAY_CHARLES_CATALOG_ENTRY
|
||||
CHARLES_RAY_CATALOG_ENTRY
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_multiple_filters)
|
||||
{
|
||||
{
|
||||
@@ -973,7 +1028,12 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" <title>Welcome to Kiwix Server</title>\n" \
|
||||
" <link\n" \
|
||||
" type=\"text/css\"\n" \
|
||||
" href=\"/ROOT%23%3F/skin/index.css?cacheid=e4d76d16\"\n" \
|
||||
" href=\"/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9\"\n" \
|
||||
" rel=\"Stylesheet\"\n" \
|
||||
" />\n" \
|
||||
" <link\n" \
|
||||
" type=\"text/css\"\n" \
|
||||
" href=\"/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf\"\n" \
|
||||
" rel=\"Stylesheet\"\n" \
|
||||
" />\n" \
|
||||
" <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3\">\n" \
|
||||
@@ -1090,7 +1150,7 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" </div>\n" \
|
||||
" <div id=\"kiwixfooter\" class=\"kiwixfooter\">Powered by <a href=\"https://kiwix.org\">Kiwix</a></div>\n" \
|
||||
" </body>\n" \
|
||||
"</html>"
|
||||
"</html>\n"
|
||||
|
||||
#define FILTERS_HTML(SELECTED_ENG) \
|
||||
" <div class=\"kiwixNav__filters\">\n" \
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
|
||||
TEST(ManagerTest, addBookFromPathAndGetIdTest)
|
||||
{
|
||||
kiwix::Library lib;
|
||||
kiwix::Manager manager = kiwix::Manager(&lib);
|
||||
auto lib = kiwix::Library::create();
|
||||
kiwix::Manager manager = kiwix::Manager(lib);
|
||||
|
||||
auto bookId = manager.addBookFromPathAndGetId("./test/example.zim");
|
||||
ASSERT_NE(bookId, "");
|
||||
kiwix::Book book = lib.getBookById(bookId);
|
||||
kiwix::Book book = lib->getBookById(bookId);
|
||||
EXPECT_EQ(book.getPath(), kiwix::computeAbsolutePath("", "./test/example.zim"));
|
||||
|
||||
const std::string pathToSave = "./pathToSave";
|
||||
const std::string url = "url";
|
||||
bookId = manager.addBookFromPathAndGetId("./test/example.zim", pathToSave, url, true);
|
||||
book = lib.getBookById(bookId);
|
||||
book = lib->getBookById(bookId);
|
||||
auto savedPath = kiwix::computeAbsolutePath(kiwix::removeLastPathElement(manager.writableLibraryPath), pathToSave);
|
||||
EXPECT_EQ(book.getPath(), savedPath);
|
||||
EXPECT_EQ(book.getUrl(), url);
|
||||
@@ -48,11 +48,11 @@ const char sampleLibraryXML[] = R"(
|
||||
|
||||
TEST(ManagerTest, readXml)
|
||||
{
|
||||
kiwix::Library lib;
|
||||
kiwix::Manager manager = kiwix::Manager(&lib);
|
||||
auto lib = kiwix::Library::create();
|
||||
kiwix::Manager manager = kiwix::Manager(lib);
|
||||
|
||||
EXPECT_EQ(true, manager.readXml(sampleLibraryXML, true, "/data/lib.xml", true));
|
||||
kiwix::Book book = lib.getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2");
|
||||
kiwix::Book book = lib->getBookById("0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2");
|
||||
EXPECT_EQ("/data/zimfiles/unittest.zim", book.getPath());
|
||||
EXPECT_EQ("https://example.com/zimfiles/unittest.zim", book.getUrl());
|
||||
EXPECT_EQ("Unit Test", book.getTitle());
|
||||
@@ -70,24 +70,24 @@ TEST(ManagerTest, readXml)
|
||||
|
||||
TEST(Manager, reload)
|
||||
{
|
||||
kiwix::Library lib;
|
||||
kiwix::Manager manager(&lib);
|
||||
auto lib = kiwix::Library::create();
|
||||
kiwix::Manager manager(lib);
|
||||
|
||||
manager.reload({ "./test/library.xml" });
|
||||
EXPECT_EQ(lib.getBooksIds(), (kiwix::Library::BookIdCollection{
|
||||
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
|
||||
"charlesray",
|
||||
"raycharles",
|
||||
"raycharles_uncategorized"
|
||||
}));
|
||||
|
||||
lib.removeBookById("raycharles");
|
||||
EXPECT_EQ(lib.getBooksIds(), (kiwix::Library::BookIdCollection{
|
||||
lib->removeBookById("raycharles");
|
||||
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
|
||||
"charlesray",
|
||||
"raycharles_uncategorized"
|
||||
}));
|
||||
|
||||
manager.reload({ "./test/library.xml" });
|
||||
EXPECT_EQ(lib.getBooksIds(), kiwix::Library::BookIdCollection({
|
||||
EXPECT_EQ(lib->getBooksIds(), kiwix::Library::BookIdCollection({
|
||||
"charlesray",
|
||||
"raycharles",
|
||||
"raycharles_uncategorized"
|
||||
|
||||
@@ -5,13 +5,17 @@ tests = [
|
||||
'stringTools',
|
||||
'pathTools',
|
||||
'otherTools',
|
||||
'opdsParsingTools',
|
||||
'languageTools',
|
||||
'kiwixserve',
|
||||
'book',
|
||||
'manager',
|
||||
'name_mapper',
|
||||
'opds_catalog',
|
||||
'server_helper',
|
||||
'lrucache'
|
||||
'lrucache',
|
||||
'i18n',
|
||||
'response'
|
||||
]
|
||||
|
||||
if build_machine.system() != 'windows'
|
||||
|
||||
@@ -18,18 +18,20 @@ const char libraryXML[] = R"(
|
||||
)";
|
||||
|
||||
class NameMapperTest : public ::testing::Test {
|
||||
public:
|
||||
NameMapperTest(): lib(kiwix::Library::create()) {}
|
||||
protected:
|
||||
void SetUp() override {
|
||||
kiwix::Manager manager(&lib);
|
||||
kiwix::Manager manager(lib);
|
||||
manager.readXml(libraryXML, false, "./library.xml", true);
|
||||
for ( const std::string& id : lib.getBooksIds() ) {
|
||||
kiwix::Book bookCopy = lib.getBookById(id);
|
||||
for ( const std::string& id : lib->getBooksIds() ) {
|
||||
kiwix::Book bookCopy = lib->getBookById(id);
|
||||
bookCopy.setPathValid(true);
|
||||
lib.addBook(bookCopy);
|
||||
lib->addBook(bookCopy);
|
||||
}
|
||||
}
|
||||
|
||||
kiwix::Library lib;
|
||||
std::shared_ptr<kiwix::Library> lib;
|
||||
};
|
||||
|
||||
class CapturedStderr
|
||||
@@ -73,13 +75,13 @@ void checkUnaliasedEntriesInNameMapper(const kiwix::NameMapper& nm)
|
||||
TEST_F(NameMapperTest, HumanReadableNameMapperWithoutAliases)
|
||||
{
|
||||
CapturedStderr stderror;
|
||||
kiwix::HumanReadableNameMapper nm(lib, false);
|
||||
kiwix::HumanReadableNameMapper nm(*lib, false);
|
||||
EXPECT_EQ("", std::string(stderror));
|
||||
|
||||
checkUnaliasedEntriesInNameMapper(nm);
|
||||
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
|
||||
|
||||
lib.removeBookById("04-2021-10");
|
||||
lib->removeBookById("04-2021-10");
|
||||
EXPECT_EQ("zero_four_2021-10", nm.getNameForId("04-2021-10"));
|
||||
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four_2021-10"));
|
||||
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
|
||||
@@ -88,7 +90,7 @@ TEST_F(NameMapperTest, HumanReadableNameMapperWithoutAliases)
|
||||
TEST_F(NameMapperTest, HumanReadableNameMapperWithAliases)
|
||||
{
|
||||
CapturedStderr stderror;
|
||||
kiwix::HumanReadableNameMapper nm(lib, true);
|
||||
kiwix::HumanReadableNameMapper nm(*lib, true);
|
||||
EXPECT_EQ(
|
||||
"Path collision: /data/zero_four_2021-10.zim and"
|
||||
" /data/zero_four_2021-11.zim can't share the same URL path 'zero_four'."
|
||||
@@ -99,7 +101,7 @@ TEST_F(NameMapperTest, HumanReadableNameMapperWithAliases)
|
||||
checkUnaliasedEntriesInNameMapper(nm);
|
||||
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four"));
|
||||
|
||||
lib.removeBookById("04-2021-10");
|
||||
lib->removeBookById("04-2021-10");
|
||||
EXPECT_EQ("zero_four_2021-10", nm.getNameForId("04-2021-10"));
|
||||
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four_2021-10"));
|
||||
EXPECT_EQ("04-2021-10", nm.getIdForName("zero_four"));
|
||||
@@ -114,7 +116,7 @@ TEST_F(NameMapperTest, UpdatableNameMapperWithoutAliases)
|
||||
checkUnaliasedEntriesInNameMapper(nm);
|
||||
EXPECT_THROW(nm.getIdForName("zero_four"), std::out_of_range);
|
||||
|
||||
lib.removeBookById("04-2021-10");
|
||||
lib->removeBookById("04-2021-10");
|
||||
nm.update();
|
||||
EXPECT_THROW(nm.getNameForId("04-2021-10"), std::out_of_range);
|
||||
EXPECT_THROW(nm.getIdForName("zero_four_2021-10"), std::out_of_range);
|
||||
@@ -137,7 +139,7 @@ TEST_F(NameMapperTest, UpdatableNameMapperWithAliases)
|
||||
|
||||
{
|
||||
CapturedStderr nmUpdateStderror;
|
||||
lib.removeBookById("04-2021-10");
|
||||
lib->removeBookById("04-2021-10");
|
||||
nm.update();
|
||||
EXPECT_EQ("", std::string(nmUpdateStderror));
|
||||
}
|
||||
|
||||
131
test/opdsParsingTools.cpp
Normal file
131
test/opdsParsingTools.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Nikhil Tanwar (2002nikhiltanwar@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
|
||||
* NON-INFRINGEMENT. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
*/
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "../include/tools.h"
|
||||
typedef kiwix::FeedLanguages FeedLanguages;
|
||||
typedef kiwix::FeedCategories FeedCategories;
|
||||
|
||||
namespace
|
||||
{
|
||||
const char sampleLanguageOpdsStream[] = R"(
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:dc="http://purl.org/dc/terms/"
|
||||
xmlns:opds="https://specs.opds.io/opds-1.2"
|
||||
xmlns:thr="http://purl.org/syndication/thread/1.0">
|
||||
<id>1e587935-0f7b-dad6-eddc-ef3fafd4c3ed</id>
|
||||
<link rel="self"
|
||||
href="/catalog/v2/languages"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start"
|
||||
href="/catalog/v2/root.xml"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<title>List of languages</title>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
|
||||
<entry>
|
||||
<title>Abkhazian</title>
|
||||
<dc:language>abk</dc:language>
|
||||
<thr:count>3</thr:count>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?lang=abk"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
<id>2e4d9a1c-9750-0418-8124-a0c663e206f7</id>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>isiZulu</title>
|
||||
<dc:language>zul</dc:language>
|
||||
<thr:count>4</thr:count>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?lang=zul"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
<id>76eec223-994d-9b95-e309-baee06e585b0</id>
|
||||
</entry>
|
||||
</feed>
|
||||
)";
|
||||
|
||||
const char sampleCategoriesOpdsStream[] = R"(
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:opds="https://specs.opds.io/opds-1.2">
|
||||
<id>231da20c-0fe0-7345-11b2-d29f50364108</id>
|
||||
<link rel="self"
|
||||
href="/catalog/v2/categories"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<link rel="start"
|
||||
href="/catalog/v2/root.xml"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||
<title>List of categories</title>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
|
||||
<entry>
|
||||
<title>gutenberg</title>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?category=gutenberg"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
<id>401dbe68-2f7a-5503-b431-054801c30bab</id>
|
||||
<content type="text">All entries with category of 'gutenberg'.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>iFixit</title>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?category=iFixit"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
<id>c18e5459-af23-5fbf-0622-ff271bd9a5ad</id>
|
||||
<content type="text">All entries with category of 'iFixit'.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>wikivoyage</title>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?category=wikivoyage"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
<id>9a75be6c-7a35-6f52-1a69-bee9ad248459</id>
|
||||
<content type="text">All entries with category of 'wikivoyage'.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>wiktionary</title>
|
||||
<link rel="subsection"
|
||||
href="/catalog/v2/entries?category=wiktionary"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>2023-07-11T15:35:09Z</updated>
|
||||
<id>7adb9f1a-73d7-0391-1238-d2e2c300ddaa</id>
|
||||
<content type="text">All entries with category of 'wiktionary'.</content>
|
||||
</entry>
|
||||
</feed>
|
||||
)";
|
||||
|
||||
TEST(OpdsParsingTest, languageTest)
|
||||
{
|
||||
FeedLanguages expectedLanguagesFromFeed = {{"abk", "Abkhazian"}, {"zul", "isiZulu"}};
|
||||
EXPECT_EQ(kiwix::readLanguagesFromFeed(sampleLanguageOpdsStream), expectedLanguagesFromFeed);
|
||||
}
|
||||
|
||||
TEST(OpdsParsingTest, categoryTest)
|
||||
{
|
||||
FeedCategories expectedCategoriesFromFeed = {"gutenberg", "iFixit", "wikivoyage", "wiktionary"};
|
||||
EXPECT_EQ(kiwix::readCategoriesFromFeed(sampleCategoriesOpdsStream), expectedCategoriesFromFeed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -100,7 +100,7 @@ TEST(Suggestions, specialCharHandling)
|
||||
{
|
||||
// HTML special symbols (<, >, &, ", and ') must be HTML-escaped
|
||||
// Backslash symbols (\) must be duplicated.
|
||||
const std::string SYMBOLS(R"(\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?)");
|
||||
const std::string SYMBOLS("\t\n\r" R"(\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?)");
|
||||
{
|
||||
kiwix::Suggestions s;
|
||||
s.add(zim::SuggestionItem("Title with " + SYMBOLS,
|
||||
@@ -110,10 +110,10 @@ TEST(Suggestions, specialCharHandling)
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "Title with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippet with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"value" : "Title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippet with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"kind" : "path"
|
||||
, "path" : "Path with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
, "path" : "Path with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
}
|
||||
]
|
||||
)EXPECTEDJSON"
|
||||
@@ -128,10 +128,10 @@ R"EXPECTEDJSON([
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "Snippetless title with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippetless title with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"value" : "Snippetless title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippetless title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"kind" : "path"
|
||||
, "path" : "Path with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
, "path" : "Path with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
}
|
||||
]
|
||||
)EXPECTEDJSON"
|
||||
@@ -145,8 +145,8 @@ R"EXPECTEDJSON([
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "text with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.? ",
|
||||
"label" : "containing 'text with \\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?'...",
|
||||
"value" : "text with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.? ",
|
||||
"label" : "containing 'text with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?'...",
|
||||
"kind" : "pattern"
|
||||
//EOLWHITESPACEMARKER
|
||||
}
|
||||
|
||||
101
test/response.cpp
Normal file
101
test/response.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "../src/server/response.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "../src/server/request_context.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using namespace kiwix;
|
||||
|
||||
RequestContext makeHttpGetRequest(const std::string& url,
|
||||
const RequestContext::NameValuePairs& headers,
|
||||
const RequestContext::NameValuePairs& queryArgs)
|
||||
{
|
||||
return RequestContext("", url, "GET", "1.1", headers, queryArgs);
|
||||
}
|
||||
|
||||
std::string getResponseContent(const ContentResponseBlueprint& crb)
|
||||
{
|
||||
return crb.generateResponseObject()->getContent();
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
|
||||
|
||||
TEST(HTTPErrorResponse, shouldBeInEnglishByDefault) {
|
||||
const RequestContext req = makeHttpGetRequest("/asdf", {}, {});
|
||||
HTTPErrorResponse errResp(req, MHD_HTTP_NOT_FOUND,
|
||||
"404-page-title",
|
||||
"404-page-heading",
|
||||
"/css/error.css",
|
||||
/*includeKiwixResponseData=*/true);
|
||||
|
||||
errResp += ParameterizedMessage("suggest-search",
|
||||
{
|
||||
{ "PATTERN", "asdf" },
|
||||
{ "SEARCH_URL", "/search?q=asdf" }
|
||||
});
|
||||
|
||||
EXPECT_EQ(getResponseContent(errResp),
|
||||
R"(<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>Content not found</title>
|
||||
<link type="text/css" href="/css/error.css" rel="Stylesheet" />
|
||||
<script>
|
||||
window.KIWIX_RESPONSE_TEMPLATE = "<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml">\n <head>\n <meta content="text/html;charset=UTF-8" http-equiv="content-type" />\n <title>{{PAGE_TITLE}}</title>\n{{#CSS_URL}}\n <link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />\n{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} <script>\n window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n </script>{{/KIWIX_RESPONSE_DATA}}\n </head>\n <body>\n <h1>{{PAGE_HEADING}}</h1>\n{{#details}}\n <p>\n {{{p}}}\n </p>\n{{/details}}\n </body>\n</html>\n";
|
||||
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/css/error.css", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "asdf", "SEARCH_URL" : "/search?q=asdf" } } } ] };
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
Make a full text search for <a href="/search?q=asdf">asdf</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
)");
|
||||
}
|
||||
|
||||
TEST(HTTPErrorResponse, shouldBeTranslatable) {
|
||||
const RequestContext req = makeHttpGetRequest("/asdf",
|
||||
/* headers */ {},
|
||||
/* query args */ {{"userlang", "test"}}
|
||||
);
|
||||
|
||||
HTTPErrorResponse errResp(req, MHD_HTTP_NOT_FOUND,
|
||||
"404-page-title",
|
||||
"404-page-heading",
|
||||
"/css/error.css",
|
||||
/*includeKiwixResponseData=*/true);
|
||||
|
||||
errResp += ParameterizedMessage("suggest-search",
|
||||
{
|
||||
{ "PATTERN", "asdf" },
|
||||
{ "SEARCH_URL", "/search?q=asdf" }
|
||||
});
|
||||
|
||||
EXPECT_EQ(getResponseContent(errResp),
|
||||
R"(<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>[I18N TESTING] Not Found - Try Again</title>
|
||||
<link type="text/css" href="/css/error.css" rel="Stylesheet" />
|
||||
<script>
|
||||
window.KIWIX_RESPONSE_TEMPLATE = "<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml">\n <head>\n <meta content="text/html;charset=UTF-8" http-equiv="content-type" />\n <title>{{PAGE_TITLE}}</title>\n{{#CSS_URL}}\n <link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />\n{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} <script>\n window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n </script>{{/KIWIX_RESPONSE_DATA}}\n </head>\n <body>\n <h1>{{PAGE_HEADING}}</h1>\n{{#details}}\n <p>\n {{{p}}}\n </p>\n{{/details}}\n </body>\n</html>\n";
|
||||
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/css/error.css", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "asdf", "SEARCH_URL" : "/search?q=asdf" } } } ] };
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
[I18N TESTING] Make a full text search for <a href="/search?q=asdf">asdf</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
)");
|
||||
}
|
||||
386
test/server.cpp
386
test/server.cpp
@@ -54,26 +54,28 @@ const ResourceCollection resources200Compressible{
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/viewer" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/viewer?cacheid=whatever" },
|
||||
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete.min.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete.min.js?cacheid=1191aaaf" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/css/autoComplete.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/css/autoComplete.css?cacheid=08951e06" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=2cf0f8c5" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=e4d76d16" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=07b06fca" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=ce19da2a" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/mustache.min.js?cacheid=bd23c4fb" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=bbdaf425" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css?cacheid=e014a885" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/viewer.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=bb748367" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/viewer.js?cacheid=5fc4badf" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/fonts/Roboto.ttf" },
|
||||
@@ -81,6 +83,8 @@ const ResourceCollection resources200Compressible{
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json" },
|
||||
// TODO: implement cache management of i18n resources
|
||||
//{ STATIC_CONTENT, "/ROOT%23%3F/skin/i18n/test.json?cacheid=unknown" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=9ccd43fd" },
|
||||
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
|
||||
|
||||
@@ -146,8 +150,6 @@ const ResourceCollection resources200Uncompressible{
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/search-icon.svg?cacheid=b10ae7ed" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/search_results.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/languages.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/languages.js?cacheid=648526e1" },
|
||||
|
||||
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" },
|
||||
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Description" },
|
||||
@@ -274,7 +276,8 @@ TEST_F(ServerTest, CacheIdsOfStaticResources)
|
||||
const std::vector<UrlAndExpectedResult> testData{
|
||||
{
|
||||
/* url */ "/ROOT%23%3F/",
|
||||
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=e4d76d16"
|
||||
R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
|
||||
href="/ROOT%23%3F/skin/index.css?cacheid=1e78e7cf"
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/ROOT%23%3F/skin/favicon/favicon-32x32.png?cacheid=79ded625">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/ROOT%23%3F/skin/favicon/favicon-16x16.png?cacheid=a986fedc">
|
||||
@@ -282,15 +285,19 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=e4d76d16"
|
||||
<link rel="mask-icon" href="/ROOT%23%3F/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="/ROOT%23%3F/skin/favicon/favicon.ico?cacheid=92663314">
|
||||
<meta name="msapplication-config" content="/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a">
|
||||
src: url("/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837") format("truetype");
|
||||
src: url("/ROOT%23%3F/skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
|
||||
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=2cf0f8c5" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=648526e1" defer></script>
|
||||
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=071abc9a" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=9ccd43fd" defer></script>
|
||||
<script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
|
||||
<script src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=07b06fca" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=ce19da2a" defer></script>
|
||||
<img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f"
|
||||
<img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961"
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
{
|
||||
/* url */ "/ROOT%23%3F/skin/kiwix.css",
|
||||
R"EXPECTEDRESULT( src: url("../skin/fonts/Poppins.ttf?cacheid=af705837") format("truetype");
|
||||
src: url("../skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
{
|
||||
@@ -308,15 +315,16 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download
|
||||
},
|
||||
{
|
||||
/* url */ "/ROOT%23%3F/viewer",
|
||||
R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=bbdaf425" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/css/autoComplete.css?cacheid=08951e06" rel="Stylesheet" />
|
||||
<script type="module" src="./skin/i18n.js?cacheid=2cf0f8c5" defer></script>
|
||||
<script type="text/javascript" src="./skin/languages.js?cacheid=648526e1" defer></script>
|
||||
<script type="text/javascript" src="./skin/viewer.js?cacheid=bb748367" defer></script>
|
||||
<script type="text/javascript" src="./skin/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||
R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fad9" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/taskbar.css?cacheid=e014a885" rel="Stylesheet" />
|
||||
<link type="text/css" href="./skin/autoComplete/css/autoComplete.css?cacheid=ef30cd42" rel="Stylesheet" />
|
||||
<script type="module" src="./skin/i18n.js?cacheid=071abc9a" defer></script>
|
||||
<script type="text/javascript" src="./skin/languages.js?cacheid=9ccd43fd" defer></script>
|
||||
<script type="text/javascript" src="./skin/viewer.js?cacheid=5fc4badf" defer></script>
|
||||
<script type="text/javascript" src="./skin/autoComplete/autoComplete.min.js?cacheid=1191aaaf"></script>
|
||||
const blankPageUrl = root + "/skin/blank.html?cacheid=6b1fa032";
|
||||
<img src="./skin/langSelector.svg?cacheid=00b59961">
|
||||
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
|
||||
src="./skin/langSelector.svg?cacheid=00b59961">
|
||||
src="./skin/blank.html?cacheid=6b1fa032" title="ZIM content" width="100%"
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
@@ -329,6 +337,7 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/taskbar.css?cacheid=bbda
|
||||
// a page rendered from static/templates/no_search_result_html
|
||||
/* url */ "/ROOT%23%3F/search?content=poor&pattern=whatever",
|
||||
R"EXPECTEDRESULT( <link type="text/css" href="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" rel="Stylesheet" />
|
||||
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] };
|
||||
)EXPECTEDRESULT"
|
||||
},
|
||||
};
|
||||
@@ -371,7 +380,7 @@ const char* urls404[] = {
|
||||
"/ROOT%23%3",
|
||||
"/ROOT%23%3Fxyz",
|
||||
"/ROOT%23%3F/skin/non-existent-skin-resource",
|
||||
"/ROOT%23%3F/skin/autoComplete.min.js?cacheid=wrongcacheid",
|
||||
"/ROOT%23%3F/skin/autoComplete/autoComplete.min.js?cacheid=wrongcacheid",
|
||||
"/ROOT%23%3F/catalog",
|
||||
"/ROOT%23%3F/catalog/",
|
||||
"/ROOT%23%3F/catalog/non-existent-item",
|
||||
@@ -527,6 +536,7 @@ struct ExpectedResponseData
|
||||
{
|
||||
const std::string expectedPageTitle;
|
||||
const std::string expectedCssUrl;
|
||||
const std::string expectedKiwixResponseData;
|
||||
const std::string bookName;
|
||||
const std::string bookTitle;
|
||||
const std::string expectedBody;
|
||||
@@ -536,6 +546,7 @@ enum ExpectedResponseDataType
|
||||
{
|
||||
expected_page_title,
|
||||
expected_css_url,
|
||||
expected_kiwix_response_data,
|
||||
book_name,
|
||||
book_title,
|
||||
expected_body
|
||||
@@ -548,11 +559,13 @@ ExpectedResponseData operator==(ExpectedResponseDataType t, std::string s)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case expected_page_title: return ExpectedResponseData{s, "", "", "", ""};
|
||||
case expected_css_url: return ExpectedResponseData{"", s, "", "", ""};
|
||||
case book_name: return ExpectedResponseData{"", "", s, "", ""};
|
||||
case book_title: return ExpectedResponseData{"", "", "", s, ""};
|
||||
case expected_body: return ExpectedResponseData{"", "", "", "", s};
|
||||
case expected_page_title: return ExpectedResponseData{s, "", "", "", "", ""};
|
||||
case expected_css_url: return ExpectedResponseData{"", s, "", "", "", ""};
|
||||
case expected_kiwix_response_data:
|
||||
return ExpectedResponseData{"", "", s, "", "", ""};
|
||||
case book_name: return ExpectedResponseData{"", "", "", s, "", ""};
|
||||
case book_title: return ExpectedResponseData{"", "", "", "", s, ""};
|
||||
case expected_body: return ExpectedResponseData{"", "", "", "", "", s};
|
||||
default: assert(false); return ExpectedResponseData{};
|
||||
}
|
||||
}
|
||||
@@ -571,6 +584,7 @@ ExpectedResponseData operator&&(const ExpectedResponseData& a,
|
||||
return ExpectedResponseData{
|
||||
selectNonEmpty(a.expectedPageTitle, b.expectedPageTitle),
|
||||
selectNonEmpty(a.expectedCssUrl, b.expectedCssUrl),
|
||||
selectNonEmpty(a.expectedKiwixResponseData, b.expectedKiwixResponseData),
|
||||
selectNonEmpty(a.bookName, b.bookName),
|
||||
selectNonEmpty(a.bookTitle, b.bookTitle),
|
||||
selectNonEmpty(a.expectedBody, b.expectedBody)
|
||||
@@ -599,19 +613,29 @@ private:
|
||||
std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||
{
|
||||
const std::string frag[] = {
|
||||
// frag[0]
|
||||
R"FRAG(<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>)FRAG",
|
||||
|
||||
// frag[1]
|
||||
R"FRAG(</title>
|
||||
)FRAG",
|
||||
|
||||
R"FRAG(
|
||||
// frag[2]
|
||||
R"( <script>
|
||||
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
|
||||
window.KIWIX_RESPONSE_DATA = )",
|
||||
|
||||
// frag[3]
|
||||
R"FRAG(;
|
||||
</script>
|
||||
</head>
|
||||
<body>)FRAG",
|
||||
|
||||
// frag[4]
|
||||
R"FRAG( </body>
|
||||
</html>
|
||||
)FRAG"
|
||||
@@ -622,8 +646,10 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||
+ frag[1]
|
||||
+ pageCssLink()
|
||||
+ frag[2]
|
||||
+ expectedKiwixResponseData
|
||||
+ frag[3]
|
||||
+ expectedBody
|
||||
+ frag[3];
|
||||
+ frag[4];
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::pageTitle() const
|
||||
@@ -640,7 +666,8 @@ std::string TestContentIn404HtmlResponse::pageCssLink() const
|
||||
|
||||
return R"( <link type="text/css" href=")"
|
||||
+ expectedCssUrl
|
||||
+ R"(" rel="Stylesheet" />)";
|
||||
+ R"(" rel="Stylesheet" />)"
|
||||
+ "\n";
|
||||
}
|
||||
|
||||
class TestContentIn400HtmlResponse : public TestContentIn404HtmlResponse
|
||||
@@ -668,6 +695,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
using namespace TestingOfHtmlResponses;
|
||||
const std::vector<TestContentIn404HtmlResponse> testData{
|
||||
{ /* url */ "/ROOT%23%3F/random?content=non-existent-book",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -677,6 +705,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/random?content=non-existent-book&userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existent-book" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
@@ -685,6 +714,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/suggest?content=no-such-book&term=whatever",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -693,6 +723,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/catalog/",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -702,6 +733,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/catalog/?userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
@@ -710,6 +742,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/catalog/invalid_endpoint",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/invalid_endpoint" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -719,6 +752,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/catalog/invalid_endpoint?userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/catalog/invalid_endpoint" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
@@ -727,6 +761,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/content/invalid-book/whatever",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/invalid-book/whatever" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "whatever", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=whatever" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -740,6 +775,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -751,6 +787,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ R"(/ROOT%23%3F/content/"><svg onload=alert(1)>)",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -764,6 +801,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
{ /* url */ R"(/ROOT%23%3F/content/zimfile/"><svg onload=alert(1)>)",
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/\"><svg onload%3Dalert(1)>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "\"><svg onload=alert(1)>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=%22%3E%3Csvg%20onload%3Dalert(1)%3E" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -774,10 +812,27 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
|
||||
// XXX: This test case is against a "</script>" string appearing inside
|
||||
// XXX: javascript code that will confuse the HTML parser
|
||||
{ /* url */ R"(/ROOT%23%3F/content/zimfile/</script>)",
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/</scr\ipt>" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "script>", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=script%3E" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
The requested URL "/ROOT%23%3F/content/zimfile/</script>" was not found on this server.
|
||||
</p>
|
||||
<p>
|
||||
Make a full text search for <a href="/ROOT%23%3F/search?content=zimfile&pattern=script%3E">script></a>
|
||||
</p>
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Not Found - Try Again" &&
|
||||
book_name=="zimfile" &&
|
||||
book_title=="Ray Charles" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/content/zimfile/invalid-article" } } }, { "p" : { "msgid" : "suggest-search", "params" : { "PATTERN" : "invalid-article", "SEARCH_URL" : "/ROOT%23%3F/search?content=zimfile&pattern=invalid-article" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>[I18N TESTING] Content not found, but at least the server is alive</h1>
|
||||
<p>
|
||||
@@ -789,6 +844,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/raw/no-such-book/meta/Title",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/no-such-book/meta/Title" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "no-such-book" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -800,6 +856,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/raw/zimfile/XYZ",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/XYZ" } } }, { "p" : { "msgid" : "invalid-raw-data-type", "params" : { "DATATYPE" : "XYZ" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -811,6 +868,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/meta/invalid-metadata" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "meta", "ENTRY" : "invalid-metadata" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -822,6 +880,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
)" },
|
||||
|
||||
{ /* url */ "/ROOT%23%3F/raw/zimfile/content/invalid-article",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "404-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "url-not-found", "params" : { "url" : "/ROOT%23%3F/raw/zimfile/content/invalid-article" } } }, { "p" : { "msgid" : "raw-entry-not-found", "params" : { "DATATYPE" : "content", "ENTRY" : "invalid-article" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -837,6 +896,7 @@ TEST_F(ServerTest, Http404HtmlError)
|
||||
expected_css_url=="/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84" &&
|
||||
book_name=="poor" &&
|
||||
book_title=="poor" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : "/ROOT%23%3F/skin/search_results.css?cacheid=76d39c84", "PAGE_HEADING" : { "msgid" : "404-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "fulltext-search-unavailable", "params" : { } }, "details" : [ { "p" : { "msgid" : "no-search-results", "params" : { } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
@@ -858,6 +918,7 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
using namespace TestingOfHtmlResponses;
|
||||
const std::vector<TestContentIn400HtmlResponse> testData{
|
||||
{ /* url */ "/ROOT%23%3F/search",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search" } } }, { "p" : { "msgid" : "too-many-books", "params" : { "LIMIT" : "3", "NB_BOOKS" : "4" } } } ] })" &&
|
||||
expected_body== R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
@@ -868,6 +929,7 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
{ /* url */ "/ROOT%23%3F/search?content=zimfile",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=zimfile" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
@@ -878,6 +940,7 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
{ /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=non-existing-book&pattern=asdfqwerty" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existing-book" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
@@ -888,6 +951,7 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
{ /* url */ "/ROOT%23%3F/search?content=non-existing-book&pattern=a\"<script foo>",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=non-existing-book&pattern=a%22%3Cscript%20foo%3E" } } }, { "p" : { "msgid" : "no-such-book", "params" : { "BOOK_NAME" : "non-existing-book" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
@@ -900,6 +964,7 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
// There is a flaw in our way to handle query string, we cannot differenciate
|
||||
// between `pattern` and `pattern=`
|
||||
{ /* url */ "/ROOT%23%3F/search?books.filter.lang=eng&pattern",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?books.filter.lang=eng&pattern" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
@@ -910,6 +975,7 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
</p>
|
||||
)" },
|
||||
{ /* url */ "/ROOT%23%3F/search?pattern=foo",
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?pattern=foo" } } }, { "p" : { "msgid" : "too-many-books", "params" : { "LIMIT" : "3", "NB_BOOKS" : "4" } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>Invalid request</h1>
|
||||
<p>
|
||||
@@ -919,6 +985,20 @@ TEST_F(ServerTest, Http400HtmlError)
|
||||
Too many books requested (4) where limit is 3
|
||||
</p>
|
||||
)" },
|
||||
|
||||
// Testing of translation
|
||||
{ /* url */ "/ROOT%23%3F/search?content=zimfile&userlang=test",
|
||||
expected_page_title=="[I18N TESTING] Invalid request ($400 fine must be paid)" &&
|
||||
expected_kiwix_response_data==R"({ "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : "/ROOT%23%3F/search?content=zimfile&userlang=test" } } }, { "p" : { "msgid" : "no-query", "params" : { } } } ] })" &&
|
||||
expected_body==R"(
|
||||
<h1>[I18N TESTING] -400 karma for an invalid request</h1>
|
||||
<p>
|
||||
[I18N TESTING] Invalid URL: "/ROOT%23%3F/search?content=zimfile&userlang=test"
|
||||
</p>
|
||||
<p>
|
||||
[I18N TESTING] Kiwix can read your thoughts but it is against GDPR. Please provide your query explicitly.
|
||||
</p>
|
||||
)" },
|
||||
};
|
||||
|
||||
for ( const auto& t : testData ) {
|
||||
@@ -1015,7 +1095,10 @@ TEST_F(ServerTest, 500)
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>Internal Server Error</title>
|
||||
|
||||
<script>
|
||||
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
|
||||
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "500-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "500-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "500-page-text", "params" : { } } }, { "p" : { "msgid" : "non-translated-text", "params" : { "MSG" : "Entry redirect_loop.html is a redirect entry." } } } ] };
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Internal Server Error</h1>
|
||||
@@ -1033,6 +1116,7 @@ TEST_F(ServerTest, 500)
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/content/poor/A/redirect_loop.html");
|
||||
EXPECT_EQ(r->status, 500);
|
||||
EXPECT_EQ(r->body, expectedBody);
|
||||
EXPECT_EQ(r->get_header_value("Content-Type"), "text/html; charset=utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,82 +1126,174 @@ TEST_F(ServerTest, UserLanguageList)
|
||||
EXPECT_EQ(r->body,
|
||||
R"EXPECTEDRESPONSE(const uiLanguages = [
|
||||
{
|
||||
"الإنجليزية": "ar"
|
||||
"iso_code": "ar",
|
||||
"self_name": "الإنجليزية",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"বাংলা": "bn"
|
||||
"iso_code": "bn",
|
||||
"self_name": "বাংলা",
|
||||
"translation_count": 12
|
||||
},
|
||||
{
|
||||
"Čeština": "cs"
|
||||
"iso_code": "cs",
|
||||
"self_name": "Čeština",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"Deutsch": "de"
|
||||
"iso_code": "de",
|
||||
"self_name": "Deutsch",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"English": "en"
|
||||
"iso_code": "en",
|
||||
"self_name": "English",
|
||||
"translation_count": 58
|
||||
},
|
||||
{
|
||||
"suomi": "fi"
|
||||
"iso_code": "es",
|
||||
"self_name": "español",
|
||||
"translation_count": 48
|
||||
},
|
||||
{
|
||||
"français": "fr"
|
||||
"iso_code": "fi",
|
||||
"self_name": "suomi",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"עברית": "he"
|
||||
"iso_code": "fr",
|
||||
"self_name": "Français",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"Հայերեն": "hy"
|
||||
"iso_code": "he",
|
||||
"self_name": "עברית",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"interlingua": "ia"
|
||||
"iso_code": "hi",
|
||||
"self_name": "हिन्दी",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"italiano": "it"
|
||||
"iso_code": "hy",
|
||||
"self_name": "Հայերեն",
|
||||
"translation_count": 15
|
||||
},
|
||||
{
|
||||
"日本語": "ja"
|
||||
"iso_code": "ia",
|
||||
"self_name": "interlingua",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"한국어": "ko"
|
||||
"iso_code": "it",
|
||||
"self_name": "italiano",
|
||||
"translation_count": 29
|
||||
},
|
||||
{
|
||||
"kurdî": "ku-latn"
|
||||
"iso_code": "ja",
|
||||
"self_name": "日本語",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"Lëtzebuergesch": "lb"
|
||||
"iso_code": "ko",
|
||||
"self_name": "한국어",
|
||||
"translation_count": 13
|
||||
},
|
||||
{
|
||||
"македонски": "mk"
|
||||
"iso_code": "ku-latn",
|
||||
"self_name": "kurdî",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"ߒߞߏ": "nqo"
|
||||
"iso_code": "lb",
|
||||
"self_name": "Lëtzebuergesch",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"Polski": "pl"
|
||||
"iso_code": "mk",
|
||||
"self_name": "македонски",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"русский": "ru"
|
||||
"iso_code": "ms",
|
||||
"self_name": "Bahasa Melayu",
|
||||
"translation_count": 14
|
||||
},
|
||||
{
|
||||
"Sardu": "sc"
|
||||
"iso_code": "nl",
|
||||
"self_name": "Nederlands",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"slovenčina": "sk"
|
||||
"iso_code": "nqo",
|
||||
"self_name": "ߒߞߏ",
|
||||
"translation_count": 43
|
||||
},
|
||||
{
|
||||
"slovenščina": "sl"
|
||||
"iso_code": "or",
|
||||
"self_name": "ଓଡ଼ିଆ",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"Svenska": "sv"
|
||||
"iso_code": "pl",
|
||||
"self_name": "Polski",
|
||||
"translation_count": 24
|
||||
},
|
||||
{
|
||||
"Türkçe": "tr"
|
||||
"iso_code": "ru",
|
||||
"self_name": "русский",
|
||||
"translation_count": 45
|
||||
},
|
||||
{
|
||||
"英语": "zh-hans"
|
||||
"iso_code": "sc",
|
||||
"self_name": "Sardu",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"繁體中文": "zh-hant"
|
||||
"iso_code": "sk",
|
||||
"self_name": "slovenčina",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"iso_code": "skr-arab",
|
||||
"self_name": "سرائیکی",
|
||||
"translation_count": 20
|
||||
},
|
||||
{
|
||||
"iso_code": "sl",
|
||||
"self_name": "slovenščina",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"iso_code": "sq",
|
||||
"self_name": "Shqip",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "sv",
|
||||
"self_name": "Svenska",
|
||||
"translation_count": 52
|
||||
},
|
||||
{
|
||||
"iso_code": "te",
|
||||
"self_name": "ఇంగ్లీషు",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "tr",
|
||||
"self_name": "Türkçe",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hans",
|
||||
"self_name": "英语",
|
||||
"translation_count": 16
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hant",
|
||||
"self_name": "繁體中文",
|
||||
"translation_count": 52
|
||||
}
|
||||
])EXPECTEDRESPONSE");
|
||||
}
|
||||
@@ -1129,8 +1305,6 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
const std::string description;
|
||||
const std::string url;
|
||||
const std::string acceptLanguageHeader;
|
||||
const char* const requestCookie; // Cookie: header of the request
|
||||
const char* const responseSetCookie; // Set-Cookie: header of the response
|
||||
const std::string expectedH1;
|
||||
|
||||
operator TestContext() const
|
||||
@@ -1141,119 +1315,45 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
{"acceptLanguageHeader", acceptLanguageHeader},
|
||||
};
|
||||
|
||||
if ( requestCookie ) {
|
||||
ctx.push_back({"requestCookie", requestCookie});
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
};
|
||||
|
||||
const char* const NO_COOKIE = nullptr;
|
||||
|
||||
const TestData testData[] = {
|
||||
{
|
||||
"Default user language is English",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"userlang URL query parameter is respected",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"userlang URL query parameter is respected",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=test",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"'Accept-Language: *' is handled",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "*",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"Accept-Language: header is respected",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "test",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is respected",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "userlang=test",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is correctly parsed",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "anothercookie=123; userlang=test",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is correctly parsed",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "userlang=test; anothercookie=abc",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is correctly parsed",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "cookie1=abc; userlang=test; cookie2=xyz",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"Multiple userlang cookies are not a problem",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "cookie1=abc; userlang=en; userlang=test; cookie2=xyz",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang query parameter takes precedence over Accept-Language",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
|
||||
/*Accept-Language:*/ "test",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"userlang query parameter takes precedence over its cookie counterpart",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article?userlang=en",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "userlang=test",
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"userlang in cookies takes precedence over Accept-Language",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "test",
|
||||
/*Request Cookie:*/ "userlang=en",
|
||||
/*Response Set-Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
@@ -1262,8 +1362,6 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
// with quality values) the most suitable language is selected.
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "test;q=0.9, en;q=0.2",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=test;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
@@ -1272,8 +1370,6 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
// with quality values) the most suitable language is selected.
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "test;q=0.2, en;q=0.9",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/*Response Set-Cookie:*/ "userlang=en;Path=/ROOT%23%3F;Max-Age=31536000",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
};
|
||||
@@ -1285,16 +1381,8 @@ TEST_F(ServerTest, UserLanguageControl)
|
||||
if ( !t.acceptLanguageHeader.empty() ) {
|
||||
headers.insert({"Accept-Language", t.acceptLanguageHeader});
|
||||
}
|
||||
if ( t.requestCookie ) {
|
||||
headers.insert({"Cookie", t.requestCookie});
|
||||
}
|
||||
const auto r = zfs1_->GET(t.url.c_str(), headers);
|
||||
if ( t.responseSetCookie ) {
|
||||
ASSERT_TRUE(r->has_header("Set-Cookie")) << t;
|
||||
EXPECT_EQ(t.responseSetCookie, getHeaderValue(r->headers, "Set-Cookie")) << t;
|
||||
} else {
|
||||
EXPECT_FALSE(r->has_header("Set-Cookie"));
|
||||
}
|
||||
EXPECT_FALSE(r->has_header("Set-Cookie"));
|
||||
std::regex_search(r->body, h1Match, h1Regex);
|
||||
const std::string h1(h1Match[1]);
|
||||
EXPECT_EQ(h1, t.expectedH1) << t;
|
||||
|
||||
@@ -113,7 +113,7 @@ std::string makeSearchResultsHtml(const std::string& pattern,
|
||||
}
|
||||
|
||||
</style>
|
||||
<title>Search: %PATTERN%</title>
|
||||
<title>%USERLANGMARKER%Search: %PATTERN%</title>
|
||||
</head>
|
||||
<body bgcolor="white">
|
||||
<div class="header">
|
||||
@@ -173,8 +173,8 @@ struct SearchResult
|
||||
+ " " + title + "\n"
|
||||
+ " </a>\n"
|
||||
+ " <cite>" + snippet + "</cite>\n"
|
||||
+ " <div class=\"book-title\">from " + bookTitle + "</div>\n"
|
||||
+ " <div class=\"informations\">" + wordCount + " words</div>\n";
|
||||
+ " <div class=\"book-title\">from %USERLANGMARKER%" + bookTitle + "</div>\n"
|
||||
+ " <div class=\"informations\">" + wordCount + " %USERLANGMARKER%words</div>\n";
|
||||
}
|
||||
|
||||
std::string getXml() const
|
||||
@@ -737,26 +737,16 @@ struct TestData
|
||||
|
||||
std::string expectedHtmlHeader() const
|
||||
{
|
||||
if ( totalResultCount == 0 ) {
|
||||
return "\n No results were found for <b>\"" + getPattern() + "\"</b>";
|
||||
}
|
||||
|
||||
std::string header = R"( Results
|
||||
<b>
|
||||
FIRSTRESULT-LASTRESULT
|
||||
</b> of <b>
|
||||
RESULTCOUNT
|
||||
</b> for <b>
|
||||
"PATTERN"
|
||||
</b>
|
||||
)";
|
||||
std::string header = totalResultCount == 0
|
||||
? R"(No results were found for <b>"PATTERN"</b>)"
|
||||
: R"(Results <b>FIRSTRESULT-LASTRESULT</b> of <b>RESULTCOUNT</b> for <b>"PATTERN"</b>)";
|
||||
|
||||
const size_t lastResultIndex = std::min(totalResultCount, firstResultIndex + results.size() - 1);
|
||||
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
|
||||
header = replace(header, "LASTRESULT", std::to_string(lastResultIndex));
|
||||
header = replace(header, "RESULTCOUNT", std::to_string(totalResultCount));
|
||||
header = replace(header, "PATTERN", getPattern());
|
||||
return header;
|
||||
return "%USERLANGMARKER%" + header;
|
||||
}
|
||||
|
||||
std::string expectedHtmlResultsString() const
|
||||
@@ -800,12 +790,18 @@ struct TestData
|
||||
|
||||
std::string expectedHtml() const
|
||||
{
|
||||
return makeSearchResultsHtml(
|
||||
getPattern(),
|
||||
expectedHtmlHeader(),
|
||||
expectedHtmlResultsString(),
|
||||
expectedHtmlFooter()
|
||||
const std::string html = makeSearchResultsHtml(
|
||||
getPattern(),
|
||||
expectedHtmlHeader(),
|
||||
expectedHtmlResultsString(),
|
||||
expectedHtmlFooter()
|
||||
);
|
||||
|
||||
const std::string userlangMarker = extractQueryValue("userlang") == "test"
|
||||
? "[I18N TESTING] "
|
||||
: "";
|
||||
|
||||
return replace(html, "%USERLANGMARKER%", userlangMarker);
|
||||
}
|
||||
|
||||
std::string expectedXmlHeader() const
|
||||
@@ -824,7 +820,8 @@ struct TestData
|
||||
/>)";
|
||||
|
||||
const auto realResultsPerPage = resultsPerPage?resultsPerPage:25;
|
||||
const auto url = makeUrl(query + "&format=xml", firstResultIndex, realResultsPerPage);
|
||||
const auto cleanedUpQuery = replace(query, "&userlang=test", "");
|
||||
const auto url = makeUrl(cleanedUpQuery + "&format=xml", firstResultIndex, realResultsPerPage);
|
||||
header = replace(header, "URL", replace(url, "&", "&"));
|
||||
header = replace(header, "FIRSTRESULT", std::to_string(firstResultIndex));
|
||||
header = replace(header, "ITEMCOUNT", std::to_string(realResultsPerPage));
|
||||
@@ -931,6 +928,17 @@ TEST(ServerSearchTest, searchResults)
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
{
|
||||
/* query */ "pattern=velomanyunkan&books.id=" RAYCHARLESZIMID
|
||||
"&userlang=test",
|
||||
/* start */ -1,
|
||||
/* resultsPerPage */ 0,
|
||||
/* totalResultCount */ 0,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ {},
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
{
|
||||
/* query */ "pattern=razaf&books.id=" RAYCHARLESZIMID,
|
||||
/* start */ -1,
|
||||
@@ -1037,6 +1045,17 @@ TEST(ServerSearchTest, searchResults)
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
{
|
||||
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID
|
||||
"&userlang=test",
|
||||
/* start */ -1,
|
||||
/* resultsPerPage */ 100,
|
||||
/* totalResultCount */ 44,
|
||||
/* firstResultIndex */ 1,
|
||||
/* results */ LARGE_SEARCH_RESULTS,
|
||||
/* pagination */ {}
|
||||
},
|
||||
|
||||
{
|
||||
/* query */ "pattern=jazz&books.id=" RAYCHARLESZIMID,
|
||||
/* start */ -1,
|
||||
@@ -1509,7 +1528,10 @@ std::string expectedConfusionOfTonguesErrorHtml(std::string url)
|
||||
<head>
|
||||
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||
<title>Invalid request</title>
|
||||
|
||||
<script>
|
||||
window.KIWIX_RESPONSE_TEMPLATE = )" + ERROR_HTML_TEMPLATE_JS_STRING + R"(;
|
||||
window.KIWIX_RESPONSE_DATA = { "CSS_URL" : false, "PAGE_HEADING" : { "msgid" : "400-page-heading", "params" : { } }, "PAGE_TITLE" : { "msgid" : "400-page-title", "params" : { } }, "details" : [ { "p" : { "msgid" : "invalid-request", "params" : { "url" : ")" + url + R"(" } } }, { "p" : { "msgid" : "confusion-of-tongues", "params" : { } } } ] };
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Invalid request</h1>
|
||||
|
||||
@@ -98,16 +98,17 @@ private:
|
||||
void run(int serverPort, std::string indexTemplateString = "");
|
||||
|
||||
private: // data
|
||||
kiwix::Library library;
|
||||
std::shared_ptr<kiwix::Library> library;
|
||||
kiwix::Manager manager;
|
||||
std::unique_ptr<kiwix::NameMapper> nameMapper;
|
||||
std::shared_ptr<kiwix::NameMapper> nameMapper;
|
||||
std::unique_ptr<kiwix::Server> server;
|
||||
std::unique_ptr<httplib::Client> client;
|
||||
const Cfg cfg;
|
||||
};
|
||||
|
||||
ZimFileServer::ZimFileServer(int serverPort, Cfg _cfg, std::string libraryFilePath)
|
||||
: manager(&this->library)
|
||||
: library(kiwix::Library::create())
|
||||
, manager(this->library)
|
||||
, cfg(_cfg)
|
||||
{
|
||||
if ( kiwix::isRelativePath(libraryFilePath) )
|
||||
@@ -120,7 +121,8 @@ ZimFileServer::ZimFileServer(int serverPort,
|
||||
Cfg _cfg,
|
||||
const FilePathCollection& zimpaths,
|
||||
std::string indexTemplateString)
|
||||
: manager(&this->library)
|
||||
: library(kiwix::Library::create())
|
||||
, manager(this->library)
|
||||
, cfg(_cfg)
|
||||
{
|
||||
for ( const auto& zimpath : zimpaths ) {
|
||||
@@ -136,9 +138,9 @@ void ZimFileServer::run(int serverPort, std::string indexTemplateString)
|
||||
if (cfg.options & NO_NAME_MAPPER) {
|
||||
nameMapper.reset(new kiwix::IdNameMapper());
|
||||
} else {
|
||||
nameMapper.reset(new kiwix::HumanReadableNameMapper(library, false));
|
||||
nameMapper.reset(new kiwix::HumanReadableNameMapper(*library, false));
|
||||
}
|
||||
server.reset(new kiwix::Server(&library, nameMapper.get()));
|
||||
server.reset(new kiwix::Server(library, nameMapper));
|
||||
server->setRoot(cfg.root);
|
||||
server->setAddress(address);
|
||||
server->setPort(serverPort);
|
||||
@@ -188,3 +190,5 @@ protected:
|
||||
zfs1_.reset();
|
||||
}
|
||||
};
|
||||
|
||||
static const std::string ERROR_HTML_TEMPLATE_JS_STRING = R"("<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml">\n <head>\n <meta content="text/html;charset=UTF-8" http-equiv="content-type" />\n <title>{{PAGE_TITLE}}</title>\n{{#CSS_URL}}\n <link type="text/css" href="{{{CSS_URL}}}" rel="Stylesheet" />\n{{/CSS_URL}}{{#KIWIX_RESPONSE_DATA}} <script>\n window.KIWIX_RESPONSE_TEMPLATE = "{{KIWIX_RESPONSE_TEMPLATE}}";\n window.KIWIX_RESPONSE_DATA = {{{KIWIX_RESPONSE_DATA}}};\n </script>{{/KIWIX_RESPONSE_DATA}}\n </head>\n <body>\n <h1>{{PAGE_HEADING}}</h1>\n{{#details}}\n <p>\n {{{p}}}\n </p>\n{{/details}}\n </body>\n</html>\n")";
|
||||
|
||||
Reference in New Issue
Block a user