mirror of
https://github.com/kiwix/libkiwix.git
synced 2026-01-17 18:58:20 -05:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddde6db16f | ||
|
|
50d1394a0a | ||
|
|
a6040b2ecd | ||
|
|
4e755bc949 | ||
|
|
cfab4c946a | ||
|
|
57a265f73c | ||
|
|
3f945813f2 | ||
|
|
86100b39ed | ||
|
|
b2ae6d1fca | ||
|
|
e82b62c552 | ||
|
|
5fba3f434e | ||
|
|
3ac36e8ebd | ||
|
|
1babbc0e4a | ||
|
|
6b05eeb24b | ||
|
|
73b855ce6b | ||
|
|
eaca7010bc | ||
|
|
6efdc43964 | ||
|
|
7a0ab3a429 | ||
|
|
3e9d50fecb | ||
|
|
f3a604380c | ||
|
|
167e0dc4b3 | ||
|
|
14c9530afa | ||
|
|
8d97686b81 | ||
|
|
b16f6b9561 | ||
|
|
a546effa15 | ||
|
|
699f96ca0d | ||
|
|
5a0644d32b | ||
|
|
903f476f77 | ||
|
|
bf1ab03332 | ||
|
|
82cb1133e5 | ||
|
|
9b9c61a194 | ||
|
|
c768d05b5b | ||
|
|
fe018efc70 | ||
|
|
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 |
48
.github/workflows/ci.yml
vendored
48
.github/workflows/ci.yml
vendored
@@ -8,7 +8,17 @@ on:
|
||||
|
||||
jobs:
|
||||
macOS:
|
||||
runs-on: macos-13
|
||||
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:
|
||||
@@ -19,25 +29,34 @@ jobs:
|
||||
run: |
|
||||
brew update
|
||||
brew unlink python3
|
||||
# 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
|
||||
# upgrade from python@3.12 to python@3.12.2 fails to overwrite those
|
||||
rm -f /usr/local/bin/2to3 /usr/local/bin/2to3-3.12 /usr/local/bin/idle3 /usr/local/bin/idle3.12 /usr/local/bin/pydoc3 /usr/local/bin/pydoc3.12 /usr/local/bin/python3 /usr/local/bin/python3-config /usr/local/bin/python3.12 /usr/local/bin/python3.12-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
|
||||
@@ -87,11 +106,10 @@ jobs:
|
||||
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: |
|
||||
|
||||
14
ChangeLog
14
ChangeLog
@@ -1,3 +1,14 @@
|
||||
libkiwix 13.1.0
|
||||
===============
|
||||
|
||||
* Server:
|
||||
- Properly translated error pages (@veloman-yunkan #1032)
|
||||
- Properly translated search result page (@veloman-yunkan #1046)
|
||||
- Default UI language is resolved in frontend (@veloman-yunkan #1044)
|
||||
- Better support of older Web browsers by polyfilling replaceAll() (@veloman-yunkan #1054)
|
||||
* New API to migrate bookmarks between books (@mgautierfr #1043)
|
||||
* Fixed compilation on Haiku OS (@Begasus #1048)
|
||||
|
||||
libkiwix 13.0.0
|
||||
===============
|
||||
|
||||
@@ -53,8 +64,6 @@ libkiwix 12.1.0
|
||||
* Remove libkiwix android publisher from the repository (@kelson42 #884)
|
||||
* Various fixes of meson and CI. (@mgautierfr @kelson42)
|
||||
|
||||
|
||||
|
||||
libkiwix 12.0.0
|
||||
===============
|
||||
|
||||
@@ -94,7 +103,6 @@ libkiwix 12.0.0
|
||||
* Fix documentation (@kelson42 #816)
|
||||
* Udpate translation (#787 #839 #847)
|
||||
|
||||
|
||||
libkiwix 11.0.0
|
||||
===============
|
||||
|
||||
|
||||
@@ -29,19 +29,33 @@ class xml_node;
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
class Book;
|
||||
/**
|
||||
* A class to store information about a bookmark (an article in a book)
|
||||
*/
|
||||
class Bookmark
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Create an empty bookmark.
|
||||
*
|
||||
* Bookmark must be populated with `set*` methods
|
||||
*/
|
||||
Bookmark();
|
||||
|
||||
/**
|
||||
* Create a bookmark given a Book, a path and a title.
|
||||
*/
|
||||
Bookmark(const Book& book, const std::string& path, const std::string& title);
|
||||
|
||||
~Bookmark();
|
||||
|
||||
void updateFromXml(const pugi::xml_node& node);
|
||||
|
||||
const std::string& getBookId() const { return m_bookId; }
|
||||
const std::string& getBookTitle() const { return m_bookTitle; }
|
||||
const std::string& getBookName() const { return m_bookName; }
|
||||
const std::string& getBookFlavour() const { return m_bookFlavour; }
|
||||
const std::string& getUrl() const { return m_url; }
|
||||
const std::string& getTitle() const { return m_title; }
|
||||
const std::string& getLanguage() const { return m_language; }
|
||||
@@ -49,6 +63,8 @@ class Bookmark
|
||||
|
||||
void setBookId(const std::string& bookId) { m_bookId = bookId; }
|
||||
void setBookTitle(const std::string& bookTitle) { m_bookTitle = bookTitle; }
|
||||
void setBookName(const std::string& bookName) { m_bookName = bookName; }
|
||||
void setBookFlavour(const std::string& bookFlavour) { m_bookFlavour = bookFlavour; }
|
||||
void setUrl(const std::string& url) { m_url = url; }
|
||||
void setTitle(const std::string& title) { m_title = title; }
|
||||
void setLanguage(const std::string& language) { m_language = language; }
|
||||
@@ -57,6 +73,8 @@ class Bookmark
|
||||
protected:
|
||||
std::string m_bookId;
|
||||
std::string m_bookTitle;
|
||||
std::string m_bookName;
|
||||
std::string m_bookFlavour;
|
||||
std::string m_url;
|
||||
std::string m_title;
|
||||
std::string m_language;
|
||||
|
||||
@@ -55,6 +55,22 @@ enum supportedListMode {
|
||||
NOVALID = 1 << 5
|
||||
};
|
||||
|
||||
enum MigrationMode {
|
||||
/** When migrating bookmarks, do not allow to migrate to an older book than the currently pointed one
|
||||
* (or date stored in the bookmark if book is invalid)
|
||||
*
|
||||
* If no newer books are found, no upgrade is made.
|
||||
*/
|
||||
UPGRADE_ONLY = 0,
|
||||
|
||||
/** Try hard to do a migration. This mostly does:
|
||||
* - Try to find a newer book.
|
||||
* - If book is invalid: find a best book, potentially older.
|
||||
* Older book will never be returned if current book is a valid one.
|
||||
*/
|
||||
ALLOW_DOWNGRADE = 1,
|
||||
};
|
||||
|
||||
class Filter {
|
||||
public: // types
|
||||
using Tags = std::vector<std::string>;
|
||||
@@ -71,6 +87,7 @@ class Filter {
|
||||
std::string _query;
|
||||
bool _queryIsPartial;
|
||||
std::string _name;
|
||||
std::string _flavour;
|
||||
|
||||
public: // functions
|
||||
Filter();
|
||||
@@ -130,6 +147,7 @@ class Filter {
|
||||
Filter& maxSize(size_t size);
|
||||
Filter& query(std::string query, bool partial=true);
|
||||
Filter& name(std::string name);
|
||||
Filter& flavour(std::string flavour);
|
||||
Filter& clearLang();
|
||||
Filter& clearCategory();
|
||||
|
||||
@@ -152,6 +170,9 @@ class Filter {
|
||||
bool hasCreator() const;
|
||||
const std::string& getCreator() const { return _creator; }
|
||||
|
||||
bool hasFlavour() const;
|
||||
const std::string& getFlavour() const { return _flavour; }
|
||||
|
||||
const Tags& getAcceptTags() const { return _acceptTags; }
|
||||
const Tags& getRejectTags() const { return _rejectTags; }
|
||||
|
||||
@@ -250,7 +271,7 @@ class Library: public std::enable_shared_from_this<Library>
|
||||
void addBookmark(const Bookmark& bookmark);
|
||||
|
||||
/**
|
||||
* Remove a bookmarkk
|
||||
* Remove a bookmark
|
||||
*
|
||||
* @param zimId The zimId of the bookmark.
|
||||
* @param url The url of the bookmark.
|
||||
@@ -258,6 +279,66 @@ class Library: public std::enable_shared_from_this<Library>
|
||||
*/
|
||||
bool removeBookmark(const std::string& zimId, const std::string& url);
|
||||
|
||||
/**
|
||||
* Migrate all invalid bookmarks.
|
||||
*
|
||||
* All invalid bookmarks (ie pointing to unknown books, no check is made on bookmark pointing to
|
||||
* invalid articles of valid book) will be migrated (if possible) to a better book.
|
||||
* "Better book", will be determined using method `getBestTargetBookId`.
|
||||
*
|
||||
* @return A tuple<int, int>: <The number of bookmarks updated>, <Number of invalid bookmarks before migration was performed>.
|
||||
*/
|
||||
std::tuple<int, int> migrateBookmarks(MigrationMode migrationMode = ALLOW_DOWNGRADE);
|
||||
|
||||
/**
|
||||
* Migrate all bookmarks associated to a specific book.
|
||||
*
|
||||
* All bookmarks associated to `sourceBookId` book will be migrated to a better book.
|
||||
* "Better book", will be determined using method `getBestTargetBookId`.
|
||||
*
|
||||
* @param sourceBookId the source bookId of the bookmarks to migrate.
|
||||
* @param migrationMode how we will find the best book.
|
||||
* @return The number of bookmarks updated.
|
||||
*/
|
||||
int migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode = UPGRADE_ONLY);
|
||||
|
||||
/**
|
||||
* Migrate bookmarks
|
||||
*
|
||||
* Migrate all bookmarks pointing to `source` to `destination`.
|
||||
*
|
||||
* @param sourceBookId the source bookId of the bookmarks to migrate.
|
||||
* @param targetBookId the destination bookId to migrate the bookmarks to.
|
||||
* @return The number of bookmarks updated.
|
||||
*/
|
||||
int migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId);
|
||||
|
||||
/**
|
||||
* Get the best available bookId for a bookmark.
|
||||
*
|
||||
* Given a bookmark, return the best available bookId.
|
||||
* "best available bookId" is determined using heuristitcs based on book name, flavour and date.
|
||||
*
|
||||
* @param bookmark The bookmark to search the bookId for.
|
||||
* @param migrationMode The migration mode to use.
|
||||
* @return A bookId. Potentially empty string if no suitable book found.
|
||||
*/
|
||||
std::string getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const;
|
||||
|
||||
/**
|
||||
* Get the best bookId for a combination of book's name, flavour and date.
|
||||
*
|
||||
* Given a bookName (mandatory), try to find the best book.
|
||||
* If preferedFlavour is given, will try to find a book with the same flavour. If not found, return a book with a different flavour.
|
||||
* If minDate is given, return a book newer than minDate. If not found, return a empty bookId.
|
||||
*
|
||||
* @param bookName The name of the book
|
||||
* @param preferedFlavour The prefered flavour.
|
||||
* @param minDate the minimal book date acceptable. Must be a string in the format "YYYY-MM-DD".
|
||||
* @return A bookId corresponding to the query, or empty string if not found.
|
||||
*/
|
||||
std::string getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour="", const std::string& minDate="") const;
|
||||
|
||||
// XXX: This is a non-thread-safe operation
|
||||
const Book& getBookById(const std::string& id) const;
|
||||
// XXX: This is a non-thread-safe operation
|
||||
@@ -403,12 +484,13 @@ private: // functions
|
||||
AttributeCounts getBookAttributeCounts(BookStrPropMemFn p) const;
|
||||
std::vector<std::string> getBookPropValueSet(BookStrPropMemFn p) const;
|
||||
BookIdCollection filterViaBookDB(const Filter& filter) const;
|
||||
std::string getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) 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
|
||||
mutable std::mutex m_mutex;
|
||||
mutable std::recursive_mutex m_mutex;
|
||||
Library::Revision m_revision;
|
||||
std::map<std::string, Entry> m_books;
|
||||
using ArchiveCache = ConcurrentCache<std::string, std::shared_ptr<zim::Archive>>;
|
||||
|
||||
@@ -72,6 +72,13 @@ class SearchRenderer
|
||||
this->pageLength = pageLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* set user language
|
||||
*/
|
||||
void setUserLang(const std::string& lang){
|
||||
this->userlang = lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the html page with the resutls of the search.
|
||||
*
|
||||
@@ -105,6 +112,7 @@ class SearchRenderer
|
||||
unsigned int pageLength;
|
||||
unsigned int estimatedResultCount;
|
||||
unsigned int resultStart;
|
||||
std::string userlang = "en";
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('libkiwix', 'cpp',
|
||||
version : '13.0.0',
|
||||
version : '13.1.0',
|
||||
license : 'GPLv3+',
|
||||
default_options : ['c_std=c11', 'cpp_std=c++17', 'werror=true'])
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "bookmark.h"
|
||||
#include "book.h"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
@@ -28,6 +29,17 @@ Bookmark::Bookmark()
|
||||
{
|
||||
}
|
||||
|
||||
Bookmark::Bookmark(const Book& book, const std::string& path, const std::string& title):
|
||||
m_bookId(book.getId()),
|
||||
m_bookTitle(book.getTitle()),
|
||||
m_bookName(book.getName()),
|
||||
m_bookFlavour(book.getFlavour()),
|
||||
m_url(path),
|
||||
m_title(title),
|
||||
m_language(book.getCommaSeparatedLanguages()),
|
||||
m_date(book.getDate())
|
||||
{}
|
||||
|
||||
/* Destructor */
|
||||
Bookmark::~Bookmark()
|
||||
{
|
||||
@@ -38,6 +50,8 @@ void Bookmark::updateFromXml(const pugi::xml_node& node)
|
||||
auto bookNode = node.child("book");
|
||||
m_bookId = bookNode.child("id").child_value();
|
||||
m_bookTitle = bookNode.child("title").child_value();
|
||||
m_bookName = bookNode.child("name").child_value();
|
||||
m_bookFlavour = bookNode.child("flavour").child_value();
|
||||
m_language = bookNode.child("language").child_value();
|
||||
m_date = bookNode.child("date").child_value();
|
||||
m_title = node.child("title").child_value();
|
||||
|
||||
211
src/library.cpp
211
src/library.cpp
@@ -110,7 +110,7 @@ Library::~Library() = default;
|
||||
|
||||
bool Library::addBook(const Book& book)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
++m_revision;
|
||||
/* Try to find it */
|
||||
updateBookDB(book);
|
||||
@@ -141,13 +141,13 @@ bool Library::addBook(const Book& book)
|
||||
|
||||
void Library::addBookmark(const Bookmark& bookmark)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_bookmarks.push_back(bookmark);
|
||||
}
|
||||
|
||||
bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto it=m_bookmarks.begin(); it!=m_bookmarks.end(); it++) {
|
||||
if (it->getBookId() == zimId && it->getUrl() == url) {
|
||||
m_bookmarks.erase(it);
|
||||
@@ -157,6 +157,159 @@ bool Library::removeBookmark(const std::string& zimId, const std::string& url)
|
||||
return false;
|
||||
}
|
||||
|
||||
std::tuple<int, int> Library::migrateBookmarks(MigrationMode migrationMode) {
|
||||
std::set<std::string> sourceBooks;
|
||||
int invalidBookmarks = 0;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto& bookmark:m_bookmarks) {
|
||||
if (m_books.find(bookmark.getBookId()) == m_books.end()) {
|
||||
invalidBookmarks += 1;
|
||||
sourceBooks.insert(bookmark.getBookId());
|
||||
}
|
||||
}
|
||||
}
|
||||
int changed = 0;
|
||||
for(auto& sourceBook:sourceBooks) {
|
||||
changed += migrateBookmarks(sourceBook, migrationMode);
|
||||
}
|
||||
return std::make_tuple(changed, invalidBookmarks);
|
||||
}
|
||||
|
||||
std::string Library::getBestFromBookCollection(BookIdCollection books, const Bookmark& bookmark, MigrationMode migrationMode) const {
|
||||
// This function try to get the best book for a bookmark from a book collection.
|
||||
// It assumes that all books in the collection are "acceptable".
|
||||
// (this definiton is not clear but for now it is book's name is equal to bookmark's bookName)
|
||||
//
|
||||
// The algorithm first sort the colletion by "flavour equality" and date.
|
||||
// "flavour equality" is if book's flavour is same that bookmark's flavour (let's say "flavourA" here)
|
||||
// So we have the sorted collection:
|
||||
// - flavourA, date 5
|
||||
// - flavourA, date 4
|
||||
// - flavourB, date 6
|
||||
// - flavourC, date 5
|
||||
// - flavourB, date 3
|
||||
//
|
||||
// Then, depending of migrationMode:
|
||||
// - If ALLOW_DOWNGRADE => take the first one
|
||||
// - If UPGRADE_ONLY => loop on books until we find a book newer than bookmark.
|
||||
// So if bookmark date is 5 => flavourB, date 6
|
||||
// if bookmark date is 4 => flavourA, date 5
|
||||
// if bookmark date is 7 => No book
|
||||
|
||||
if (books.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
sort(books, DATE, false);
|
||||
stable_sort(books.begin(), books.end(), [&](const std::string& bookId1, const std::string& bookId2) {
|
||||
const auto& book1 = getBookById(bookId1);
|
||||
const auto& book2 = getBookById(bookId2);
|
||||
bool same_flavour1 = book1.getFlavour() == bookmark.getBookFlavour();
|
||||
bool same_flavour2 = book2.getFlavour() == bookmark.getBookFlavour();
|
||||
// return True if bookId1 is before bookId2, ie if same_flavour1 and not same_flavour2
|
||||
return same_flavour1 > same_flavour2;
|
||||
});
|
||||
|
||||
if (migrationMode == ALLOW_DOWNGRADE) {
|
||||
return books[0];
|
||||
} else {
|
||||
for (const auto& bookId: books) {
|
||||
const auto& book = getBookById(bookId);
|
||||
if (book.getDate() >= bookmark.getDate()) {
|
||||
return bookId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string remove_quote(std::string input) {
|
||||
std::replace(input.begin(), input.end(), '"', ' ');
|
||||
return input;
|
||||
}
|
||||
|
||||
std::string Library::getBestTargetBookId(const std::string& bookName, const std::string& preferedFlavour, const std::string& minDate) const {
|
||||
// Let's reuse our algorithm based on bookmark.
|
||||
MigrationMode migrationMode = UPGRADE_ONLY;
|
||||
auto bookmark = Bookmark();
|
||||
bookmark.setBookName(bookName);
|
||||
bookmark.setBookFlavour(preferedFlavour);
|
||||
|
||||
if (minDate.empty()) {
|
||||
migrationMode = ALLOW_DOWNGRADE;
|
||||
} else {
|
||||
bookmark.setDate(minDate);
|
||||
}
|
||||
|
||||
return getBestTargetBookId(bookmark, migrationMode);
|
||||
}
|
||||
|
||||
std::string Library::getBestTargetBookId(const Bookmark& bookmark, MigrationMode migrationMode) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
// Search for a existing book with the same name
|
||||
auto book_filter = Filter();
|
||||
if (!bookmark.getBookName().empty()) {
|
||||
book_filter.name(bookmark.getBookName());
|
||||
} else {
|
||||
// We don't have a name stored (older bookmarks)
|
||||
// Fallback on title (All bookmarks should have one, but let's be safe against wrongly filled bookmark)
|
||||
if (bookmark.getBookTitle().empty()) {
|
||||
// No bookName nor bookTitle, no way to find target book.
|
||||
return "";
|
||||
}
|
||||
book_filter.query("title:\"" + remove_quote(bookmark.getBookTitle()) + "\"");
|
||||
}
|
||||
auto targetBooks = filter(book_filter);
|
||||
auto bestBook = getBestFromBookCollection(targetBooks, bookmark, migrationMode);
|
||||
if (bestBook.empty()) {
|
||||
try {
|
||||
getBookById(bookmark.getBookId());
|
||||
return bookmark.getBookId();
|
||||
} catch (std::out_of_range&) {}
|
||||
}
|
||||
return bestBook;
|
||||
}
|
||||
|
||||
int Library::migrateBookmarks(const std::string& sourceBookId, MigrationMode migrationMode) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
|
||||
Bookmark firstBookmarkToChange;
|
||||
for(auto& bookmark:m_bookmarks) {
|
||||
if (bookmark.getBookId() == sourceBookId) {
|
||||
firstBookmarkToChange = bookmark;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstBookmarkToChange.getBookId().empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string betterBook = getBestTargetBookId(firstBookmarkToChange, migrationMode);
|
||||
|
||||
if (betterBook.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return migrateBookmarks(sourceBookId, betterBook);
|
||||
}
|
||||
|
||||
int Library::migrateBookmarks(const std::string& sourceBookId, const std::string& targetBookId) {
|
||||
if (sourceBookId == targetBookId) {
|
||||
return 0;
|
||||
}
|
||||
int changed = 0;
|
||||
for (auto& bookmark:m_bookmarks) {
|
||||
if (bookmark.getBookId() == sourceBookId) {
|
||||
bookmark.setBookId(targetBookId);
|
||||
changed +=1;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
||||
void Library::dropCache(const std::string& id)
|
||||
{
|
||||
@@ -166,7 +319,7 @@ void Library::dropCache(const std::string& id)
|
||||
|
||||
bool Library::removeBookById(const std::string& id)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
m_bookDB->delete_document("Q" + id);
|
||||
dropCache(id);
|
||||
// We do not change the cache size here
|
||||
@@ -184,7 +337,7 @@ bool Library::removeBookById(const std::string& id)
|
||||
|
||||
Library::Revision Library::getRevision() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return m_revision;
|
||||
}
|
||||
|
||||
@@ -192,7 +345,7 @@ uint32_t Library::removeBooksNotUpdatedSince(Revision libraryRevision)
|
||||
{
|
||||
BookIdCollection booksToRemove;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for ( const auto& entry : m_books) {
|
||||
if ( entry.second.lastUpdatedRevision <= libraryRevision ) {
|
||||
booksToRemove.push_back(entry.first);
|
||||
@@ -217,7 +370,7 @@ const Book& Library::getBookById(const std::string& id) const
|
||||
|
||||
Book Library::getBookByIdThreadSafe(const std::string& id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return getBookById(id);
|
||||
}
|
||||
|
||||
@@ -275,7 +428,7 @@ std::shared_ptr<ZimSearcher> Library::getSearcherByIds(const BookIdSet& ids)
|
||||
unsigned int Library::getBookCount(const bool localBooks,
|
||||
const bool remoteBooks) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
return getBookCount_not_protected(localBooks, remoteBooks);
|
||||
}
|
||||
|
||||
@@ -288,7 +441,7 @@ bool Library::writeToFile(const std::string& path) const
|
||||
dumper.setBaseDir(baseDir);
|
||||
std::string xml;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
xml = dumper.dumpLibXMLContent(allBookIds);
|
||||
};
|
||||
return writeTextFile(path, xml);
|
||||
@@ -304,7 +457,7 @@ bool Library::writeBookmarksToFile(const std::string& path) const
|
||||
|
||||
Library::AttributeCounts Library::getBookAttributeCounts(BookStrPropMemFn p) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
AttributeCounts propValueCounts;
|
||||
|
||||
for (const auto& pair: m_books) {
|
||||
@@ -336,7 +489,7 @@ std::vector<std::string> Library::getBooksLanguages() const
|
||||
|
||||
Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
AttributeCounts langsWithCounts;
|
||||
|
||||
for (const auto& pair: m_books) {
|
||||
@@ -352,7 +505,7 @@ Library::AttributeCounts Library::getBooksLanguagesWithCounts() const
|
||||
|
||||
std::vector<std::string> Library::getBooksCategories() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
std::set<std::string> categories;
|
||||
|
||||
for (const auto& pair: m_books) {
|
||||
@@ -383,7 +536,7 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
|
||||
}
|
||||
std::vector<kiwix::Bookmark> validBookmarks;
|
||||
auto booksId = getBooksIds();
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto& bookmark:m_bookmarks) {
|
||||
if (std::find(booksId.begin(), booksId.end(), bookmark.getBookId()) != booksId.end()) {
|
||||
validBookmarks.push_back(bookmark);
|
||||
@@ -394,7 +547,7 @@ const std::vector<kiwix::Bookmark> Library::getBookmarks(bool onlyValidBookmarks
|
||||
|
||||
Library::BookIdCollection Library::getBooksIds() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
BookIdCollection bookIds;
|
||||
|
||||
for (auto& pair: m_books) {
|
||||
@@ -437,6 +590,7 @@ void Library::updateBookDB(const Book& book)
|
||||
indexer.index_text(normalizeText(book.getCreator()), 1, "A");
|
||||
indexer.index_text(normalizeText(book.getPublisher()), 1, "XP");
|
||||
doc.add_term("XN"+normalizeText(book.getName()));
|
||||
indexer.index_text(normalizeText(book.getFlavour()), 1, "XF");
|
||||
indexer.index_text(normalizeText(book.getCategory()), 1, "XC");
|
||||
|
||||
for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) {
|
||||
@@ -477,6 +631,7 @@ Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter)
|
||||
queryParser.add_prefix("title", "S");
|
||||
queryParser.add_prefix("description", "XD");
|
||||
queryParser.add_prefix("name", "XN");
|
||||
queryParser.add_prefix("flavour", "XF");
|
||||
queryParser.add_prefix("category", "XC");
|
||||
queryParser.add_prefix("lang", "L");
|
||||
queryParser.add_prefix("publisher", "XP");
|
||||
@@ -503,6 +658,11 @@ Xapian::Query nameQuery(const std::string& name)
|
||||
return Xapian::Query("XN" + normalizeText(name));
|
||||
}
|
||||
|
||||
Xapian::Query flavourQuery(const std::string& name)
|
||||
{
|
||||
return Xapian::Query("XF" + normalizeText(name));
|
||||
}
|
||||
|
||||
Xapian::Query multipleParamQuery(const std::string& commaSeparatedList, const std::string& prefix)
|
||||
{
|
||||
Xapian::Query q;
|
||||
@@ -570,6 +730,9 @@ Xapian::Query buildXapianQuery(const Filter& filter)
|
||||
if ( filter.hasName() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, nameQuery(filter.getName()));
|
||||
}
|
||||
if ( filter.hasFlavour() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, flavourQuery(filter.getFlavour()));
|
||||
}
|
||||
if ( filter.hasCategory() ) {
|
||||
q = Xapian::Query(Xapian::Query::OP_AND, q, categoryQuery(filter.getCategory()));
|
||||
}
|
||||
@@ -600,7 +763,7 @@ Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) const
|
||||
|
||||
BookIdCollection bookIds;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
Xapian::Enquire enquire(*m_bookDB);
|
||||
enquire.set_query(query);
|
||||
const auto results = enquire.get_mset(0, m_books.size());
|
||||
@@ -615,7 +778,7 @@ Library::BookIdCollection Library::filter(const Filter& filter) const
|
||||
{
|
||||
BookIdCollection result;
|
||||
const auto preliminaryResult = filterViaBookDB(filter);
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
for(auto id : preliminaryResult) {
|
||||
if(filter.accept(m_books.at(id))) {
|
||||
result.push_back(id);
|
||||
@@ -689,7 +852,7 @@ void Library::sort(BookIdCollection& bookIds, supportedListSortBy sort, bool asc
|
||||
// NOTE: for the entire duration of the sort. Will need to obtain (under a
|
||||
// NOTE: lock) the required atributes from the books once, and then the
|
||||
// NOTE: sorting will run on a copy of data without locking.
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
switch(sort) {
|
||||
case TITLE:
|
||||
std::sort(bookIds.begin(), bookIds.end(), Comparator<TITLE>(this, ascending));
|
||||
@@ -735,6 +898,7 @@ enum filterTypes {
|
||||
QUERY = FLAG(12),
|
||||
NAME = FLAG(13),
|
||||
CATEGORY = FLAG(14),
|
||||
FLAVOUR = FLAG(15),
|
||||
};
|
||||
|
||||
Filter& Filter::local(bool accept)
|
||||
@@ -836,6 +1000,13 @@ Filter& Filter::name(std::string name)
|
||||
activeFilters |= NAME;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::flavour(std::string flavour)
|
||||
{
|
||||
_flavour = flavour;
|
||||
activeFilters |= FLAVOUR;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Filter& Filter::clearLang()
|
||||
{
|
||||
@@ -881,6 +1052,12 @@ bool Filter::hasCreator() const
|
||||
return ACTIVE(_CREATOR);
|
||||
}
|
||||
|
||||
bool Filter::hasFlavour() const
|
||||
{
|
||||
return ACTIVE(FLAVOUR);
|
||||
}
|
||||
|
||||
|
||||
bool Filter::accept(const Book& book) const
|
||||
{
|
||||
auto local = !book.getPath().empty();
|
||||
|
||||
@@ -97,11 +97,15 @@ void LibXMLDumper::handleBookmark(Bookmark bookmark, pugi::xml_node root_node) {
|
||||
auto book = library->getBookByIdThreadSafe(bookmark.getBookId());
|
||||
ADD_TEXT_ENTRY(book_node, "id", book.getId());
|
||||
ADD_TEXT_ENTRY(book_node, "title", book.getTitle());
|
||||
ADD_TEXT_ENTRY(book_node, "name", book.getName());
|
||||
ADD_TEXT_ENTRY(book_node, "flavour", book.getFlavour());
|
||||
ADD_TEXT_ENTRY(book_node, "language", book.getCommaSeparatedLanguages());
|
||||
ADD_TEXT_ENTRY(book_node, "date", book.getDate());
|
||||
} catch (...) {
|
||||
ADD_TEXT_ENTRY(book_node, "id", bookmark.getBookId());
|
||||
ADD_TEXT_ENTRY(book_node, "title", bookmark.getBookTitle());
|
||||
ADD_TEXT_ENTRY(book_node, "name", bookmark.getBookName());
|
||||
ADD_TEXT_ENTRY(book_node, "flavour", bookmark.getBookFlavour());
|
||||
ADD_TEXT_ENTRY(book_node, "language", bookmark.getLanguage());
|
||||
ADD_TEXT_ENTRY(book_node, "date", bookmark.getDate());
|
||||
}
|
||||
@@ -135,7 +139,7 @@ std::string LibXMLDumper::dumpLibXMLBookmark()
|
||||
pugi::xml_node bookmarksNode = doc.append_child("bookmarks");
|
||||
|
||||
if (library) {
|
||||
for (auto& bookmark: library->getBookmarks()) {
|
||||
for (auto& bookmark: library->getBookmarks(false)) {
|
||||
handleBookmark(bookmark, bookmarksNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,42 @@
|
||||
#include "libkiwix-resources.h"
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include "server/i18n.h"
|
||||
|
||||
namespace kiwix
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
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)
|
||||
@@ -170,10 +203,20 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
|
||||
result.set("absolutePath", absPathPrefix + urlEncode(path));
|
||||
result.set("snippet", it.getSnippet());
|
||||
if (library) {
|
||||
result.set("bookTitle", library->getBookById(zim_id).getTitle());
|
||||
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);
|
||||
@@ -181,7 +224,6 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
|
||||
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)));
|
||||
|
||||
@@ -198,12 +240,15 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str, const Na
|
||||
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);
|
||||
|
||||
|
||||
@@ -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"),
|
||||
@@ -519,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,
|
||||
@@ -535,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();
|
||||
@@ -564,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()) {
|
||||
@@ -593,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) )
|
||||
@@ -647,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,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");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,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;
|
||||
@@ -712,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);
|
||||
}
|
||||
|
||||
@@ -747,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)
|
||||
@@ -759,10 +767,9 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
|
||||
const kainjow::mustache::object data{
|
||||
{"enable_toolbar", m_withTaskbar ? "true" : "false" },
|
||||
{"enable_link_blocking", m_blockExternalLinks ? "true" : "false" },
|
||||
{"enable_library_button", m_withLibraryButton ? "true" : "false" },
|
||||
{"default_user_language", request.get_user_language() }
|
||||
{"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
|
||||
@@ -817,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
|
||||
@@ -867,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,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();
|
||||
}
|
||||
}
|
||||
@@ -942,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.
|
||||
@@ -970,15 +967,14 @@ std::unique_ptr<Response> InternalServer::handle_search_request(const RequestCon
|
||||
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(*mp_nameMapper, mp_library.get()),
|
||||
"application/rss+xml; charset=utf-8"
|
||||
);
|
||||
}
|
||||
auto response = ContentResponse::build(
|
||||
*this,
|
||||
renderer.getHtml(*mp_nameMapper, mp_library.get()),
|
||||
"text/html; charset=utf-8"
|
||||
);
|
||||
@@ -1001,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;
|
||||
@@ -1016,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);
|
||||
}
|
||||
|
||||
@@ -1024,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");
|
||||
}
|
||||
}
|
||||
@@ -1037,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)
|
||||
@@ -1056,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>
|
||||
@@ -1117,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)
|
||||
@@ -1141,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] == '/') {
|
||||
@@ -1168,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") ) {
|
||||
@@ -1189,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));
|
||||
}
|
||||
}
|
||||
@@ -1208,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);
|
||||
}
|
||||
|
||||
@@ -1225,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
|
||||
@@ -1244,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 {
|
||||
@@ -1252,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;
|
||||
}
|
||||
@@ -1260,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);
|
||||
}
|
||||
}
|
||||
@@ -1286,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,12 +71,11 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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()},
|
||||
@@ -170,7 +164,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const Reques
|
||||
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,8 +174,7 @@ 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.get(), mp_nameMapper.get());
|
||||
@@ -190,7 +182,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsFeed,
|
||||
opdsMimeType[OPDS_ENTRY]
|
||||
);
|
||||
@@ -202,7 +193,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
return ContentResponse::build(
|
||||
*this,
|
||||
opdsDumper.categoriesOPDSFeed(),
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
);
|
||||
@@ -214,7 +204,6 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const Requ
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -145,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);
|
||||
|
||||
@@ -390,7 +573,7 @@ MHD_Result Response::send(const RequestContext& request, MHD_Connection* connect
|
||||
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);
|
||||
@@ -398,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)
|
||||
{
|
||||
@@ -408,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)
|
||||
{
|
||||
@@ -439,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -327,17 +327,27 @@ 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 escapeForJSON(const std::string& s)
|
||||
std::string kiwix::escapeForJSON(const std::string& s, bool escapeQuote)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
for (char c : s) {
|
||||
if ( c == '\\' ) {
|
||||
oss << "\\\\";
|
||||
} else if ( unsigned(c) < 0x20U ) {
|
||||
oss << "\\u" << std::setw(4) << std::setfill('0') << unsigned(c);
|
||||
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;
|
||||
}
|
||||
@@ -345,6 +355,9 @@ std::string escapeForJSON(const std::string& s)
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string makeFulltextSearchSuggestion(const std::string& lang,
|
||||
const std::string& queryString)
|
||||
{
|
||||
@@ -370,10 +383,10 @@ void kiwix::Suggestions::add(const zim::SuggestionItem& suggestion)
|
||||
? suggestion.getSnippet()
|
||||
: suggestion.getTitle();
|
||||
|
||||
result.set("label", escapeForJSON(label));
|
||||
result.set("value", escapeForJSON(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", escapeForJSON(suggestion.getPath()));
|
||||
result.set("path", escapeForJSON(suggestion.getPath(), DONT_ESCAPE_QUOTE));
|
||||
result.set("first", m_data.is_empty_list());
|
||||
m_data.push_back(result);
|
||||
}
|
||||
@@ -383,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", escapeForJSON(label));
|
||||
result.set("value", escapeForJSON(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);
|
||||
|
||||
@@ -53,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)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
skin/i18n/ar.json
|
||||
skin/i18n/bn.json
|
||||
skin/i18n/br.json
|
||||
skin/i18n/cs.json
|
||||
skin/i18n/dag.json
|
||||
skin/i18n/de.json
|
||||
skin/i18n/dga.json
|
||||
skin/i18n/el.json
|
||||
@@ -8,10 +10,12 @@ skin/i18n/en.json
|
||||
skin/i18n/es.json
|
||||
skin/i18n/fi.json
|
||||
skin/i18n/fr.json
|
||||
skin/i18n/ha.json
|
||||
skin/i18n/he.json
|
||||
skin/i18n/hi.json
|
||||
skin/i18n/hy.json
|
||||
skin/i18n/ia.json
|
||||
skin/i18n/ig.json
|
||||
skin/i18n/it.json
|
||||
skin/i18n/ja.json
|
||||
skin/i18n/ko.json
|
||||
|
||||
@@ -17,6 +17,7 @@ skin/fonts/Poppins.ttf
|
||||
skin/fonts/Roboto.ttf
|
||||
skin/search_results.css
|
||||
skin/blank.html
|
||||
skin/polyfills.js
|
||||
skin/viewer.js
|
||||
skin/i18n.js
|
||||
skin/languages.js
|
||||
|
||||
@@ -69,15 +69,66 @@ function $t(msgId, params={}) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
} 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')
|
||||
|| window.localStorage.getItem('userlang')
|
||||
|| viewerSettings.defaultUserLanguage
|
||||
|| DEFAULT_UI_LANGUAGE;
|
||||
|| getDefaultUserLanguage();
|
||||
}
|
||||
|
||||
function setUserLanguage(lang, callback) {
|
||||
@@ -133,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;
|
||||
}
|
||||
@@ -145,3 +194,4 @@ window.$t = $t;
|
||||
window.getUserLanguage = getUserLanguage;
|
||||
window.setUserLanguage = setUserLanguage;
|
||||
window.initUILanguageSelector = initUILanguageSelector;
|
||||
window.I18n = I18n;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"404-page-heading": "পাওয়া যায়নি",
|
||||
"500-page-title": "অভ্যন্তরীণ সার্ভার ত্রুটি",
|
||||
"500-page-heading": "অভ্যন্তরীণ সার্ভার ত্রুটি",
|
||||
"search-result-book-info": "{{BOOK_TITLE}} থেকে",
|
||||
"word-count": "{{COUNT}}টি শব্দ",
|
||||
"library-button-text": "স্বাগত পাতায় চলুন",
|
||||
"home-button-text": "'{{BOOK_TITLE}}'-এর প্রধান পাতায় চলুন",
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' অনুসন্ধান করুন",
|
||||
|
||||
42
static/skin/i18n/br.json
Normal file
42
static/skin/i18n/br.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adriendelucca",
|
||||
"Y-M D"
|
||||
]
|
||||
},
|
||||
"name": "brezhoneg",
|
||||
"suggest-full-text-search": "E lec'h emañ \"{{{SEARCH_TERMS}}}\"...",
|
||||
"no-such-book": "N’eus ket eus al levr-mañ: {{BOOK_NAME}}",
|
||||
"no-book-found": "N’eus levr ebet a glot gant an dezverkoù-se",
|
||||
"url-not-found": "N’eo ket bet kavet an URL \"{{url}}\" goulennet war ar servijer-mañ.",
|
||||
"random-article-failure": "Chaous! N’hon eus ket gellet dibab ur pennad dre ziouer evidoc’h :(",
|
||||
"400-page-title": "Reked amwiriek",
|
||||
"400-page-heading": "Reked amwiriek",
|
||||
"404-page-heading": "N'eo ket bet kavet",
|
||||
"500-page-title": "Fazi diabarzh ar servijer",
|
||||
"500-page-heading": "Fazi diabarzh ar servijer",
|
||||
"search-results-page-title": "Klask: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Disoc’hoù <b>{{START}}-{{END}}</b> diwar <b>{{COUNT}}</b> evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Disoc’h ebet kavet evit <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "diouzh {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} a c’herioù",
|
||||
"library-button-text": "Mont d’ar bajenn degemer",
|
||||
"home-button-text": "Mont da bajenn degemer \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "Mont d’ur bajenn dre zegouezh",
|
||||
"searchbox-tooltip": "Klask '{{BOOK_TITLE}}'",
|
||||
"powered-by-kiwix-html": "Lusket gant <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Klask",
|
||||
"book-filtering-all-categories": "An holl rummadoù",
|
||||
"book-filtering-all-languages": "An holl yezhoù",
|
||||
"count-of-matching-books": "{{COUNT}} levr",
|
||||
"download": "Pellgargañ",
|
||||
"direct-download-link-text": "Eeun",
|
||||
"filter-by-tag": "Silañ gant an dikedenn \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Paouez da silañ gant an dikedenn \"{{TAG}}\"",
|
||||
"welcome-to-kiwix-server": "Degemer mat er servijer Kiwix",
|
||||
"download-links-heading": "Liammoù pellgargañ evit <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Pellgargañ al levr",
|
||||
"preview-book": "Rakwelet",
|
||||
"unknown-error": "Fazi dianav"
|
||||
}
|
||||
55
static/skin/i18n/dag.json
Normal file
55
static/skin/i18n/dag.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Kalakpagh",
|
||||
"Ruky Wunpini"
|
||||
]
|
||||
},
|
||||
"name": "Silimiinsili",
|
||||
"suggest-full-text-search": "Gbubi la '{{{SEARCH_TERMS}}}'...",
|
||||
"no-such-book": "Lala buku kani:{{BOOK_NAME}}",
|
||||
"too-many-books": "Buku nima pam ka bɛ daa suhi ({{NB_BOOKS}}) din ni ka tariga nyɛ {{LIMIT}}",
|
||||
"no-book-found": "Buku kani lu zahim a ni piigi yaɣa shɛli",
|
||||
"url-not-found": "URL \"{{url}}\" shɛli bɛ ni daa suhi daa kani n-ti tum tumda ŋɔ.",
|
||||
"suggest-search": "Niŋmi lahabali pali vihigu zaŋ n-ti <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Oops! Zaɣisiya ni di gahim piigi lahabali :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} nyɛla din suhibu bi niŋ viɛnyɛla zaŋ n-ti lahabali kahili.",
|
||||
"invalid-request": "URL \"{{{url}}}\" shɛli bɛ ni daa suhi ŋɔ nyɛla din bi suhi viɛnyɛla.",
|
||||
"no-value-for-arg": "Dariza shɛli bi ti zaŋ n-ti nangban'kpeeni {{ARGUMENT}}",
|
||||
"no-query": "Yɛlshɛli bi yiina",
|
||||
"raw-entry-not-found": "Ku tooi nya {{DATATYPE}} kpɛbu {{ENTRY}}",
|
||||
"400-page-title": "Suhigu din bi niŋ viɛnyɛla",
|
||||
"400-page-heading": "Suhigu din bi niŋ viɛnyɛla",
|
||||
"404-page-title": "Lahabali kani",
|
||||
"404-page-heading": "Kani",
|
||||
"500-page-title": "Puuni tum tumda chiriŋ",
|
||||
"500-page-heading": "Puuni tum tumda chiriŋ",
|
||||
"500-page-text": "Puuni tum tumda chiriŋ niŋya. Ti niŋ yolitem zaŋ jɛndi li :/",
|
||||
"fulltext-search-unavailable": "Lahabali pali vihigu kani",
|
||||
"search-results-page-title": "Vihima:{{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Chaɣili nima <b>{{START}}-{{END}}</b> of <b>{{COUNT}}</b> for <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Chaɣili daa kani zaŋ n-ti\n <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "yina {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} bachi nima",
|
||||
"library-button-text": "Cham solɔɣu",
|
||||
"home-button-text": "Cham yaɣili maŋmaŋ zaŋ n-ti\n'{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Cham gahim piigi yaɣili",
|
||||
"searchbox-tooltip": "Vihima '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Buku nima ayi bee gari balli koŋkoba nyɛ din yɛn be vihigu ŋɔ ni ka di ni tooi chɛ ka di laasabu wali.",
|
||||
"welcome-page-overzealous-filter": "Labisibu kani. A ni yu ni a\n<a href=\"{{URL}}\">reset filter</a>?",
|
||||
"powered-by-kiwix-html": "Din niŋ li nyɛ <a href=\"https://kiwix.org\">Kiwix</a>",
|
||||
"search": "Vihima",
|
||||
"book-filtering-all-categories": "Pubu zaa",
|
||||
"book-filtering-all-languages": "Bala zaa",
|
||||
"count-of-matching-books": "{{COUNT}} Buku(nima)",
|
||||
"download": "Yihibu",
|
||||
"direct-download-link-text": "Tibi",
|
||||
"direct-download-alt-text": "Tibi deebu",
|
||||
"hash-download-link-text": "Sha256 hash",
|
||||
"hash-download-alt-text": "Deebu daliŋ",
|
||||
"welcome-to-kiwix-server": "Maraba Kiwix tum tumda",
|
||||
"download-links-heading": "Deemi soli zaŋ n-ti <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Yaa mi buku",
|
||||
"preview-book": "Labi lihi",
|
||||
"unknown-error": "Chiriŋ din bi tooi baŋ"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"authors": [
|
||||
"IMayBeABitShy",
|
||||
"Lucas Werkmeister",
|
||||
"Rofiatmustapha12",
|
||||
"ThisCarthing"
|
||||
]
|
||||
},
|
||||
@@ -15,6 +16,7 @@
|
||||
"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",
|
||||
"invalid-request": "Die angeforderte URL „{{{url}}}“ ist keine gültige Anfrage.",
|
||||
"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.",
|
||||
@@ -24,8 +26,14 @@
|
||||
"404-page-heading": "Nicht gefunden",
|
||||
"500-page-title": "Interner Server-Fehler",
|
||||
"500-page-heading": "Interner Server-Fehler",
|
||||
"500-page-text": "Es ist ein interner Serverfehler aufgetreten. Das tut uns leid :/",
|
||||
"fulltext-search-unavailable": "Die Volltestsuche steht nicht zur Verfügung.",
|
||||
"no-search-results": "Die Volltextsuche ist für diesen Inhalt nicht verfügbar.",
|
||||
"search-results-page-title": "Suche: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Ergebnisse <b>{{START}}-{{END}}</b> von <b>{{COUNT}}</b> für <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"empty-search-results-page-header": "Für <b>„{{{SEARCH_PATTERN}}}“</b> wurden keine Ergebnisse gefunden.",
|
||||
"search-result-book-info": "von {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} Wörter",
|
||||
"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",
|
||||
@@ -53,5 +61,6 @@
|
||||
"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"
|
||||
"preview-book": "Vorschau",
|
||||
"unknown-error": "Unbekannter Fehler"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Adriendelucca",
|
||||
"Gomoko",
|
||||
"Melimeli",
|
||||
"Stephane",
|
||||
"Thibaut120094",
|
||||
"Verdy p"
|
||||
"Urhixidur",
|
||||
"Verdy p",
|
||||
"Vikoula5",
|
||||
"Wladek92"
|
||||
]
|
||||
},
|
||||
"name": "Français",
|
||||
@@ -16,6 +21,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}} »",
|
||||
@@ -25,8 +31,14 @@
|
||||
"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.",
|
||||
"search-results-page-title": "Rechercher : {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Résultats <b>{{START}}-{{END}}</b> sur<b> {{COUNT}}</b> pour <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Aucun résultat n’a été trouvé pour <b>« {{{SEARCH_PATTERN}}} »</b>",
|
||||
"search-result-book-info": "à partir de {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} mots",
|
||||
"library-button-text": "Aller à la page de bienvenue",
|
||||
"home-button-text": "Aller à la page principale de « {{BOOK_TITLE}} »",
|
||||
"random-page-button-text": "Aller à une page sélectionnée aléatoirement",
|
||||
@@ -54,5 +66,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"
|
||||
}
|
||||
|
||||
67
static/skin/i18n/ha.json
Normal file
67
static/skin/i18n/ha.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Abelidokoo",
|
||||
"El-hussain14",
|
||||
"Rofiatmustapha12",
|
||||
"Smshika",
|
||||
"Yusuf Sa'adu"
|
||||
]
|
||||
},
|
||||
"name": "Turanci",
|
||||
"suggest-full-text-search": "dauke da ''{{{SEARCH_TERMS}}}''...",
|
||||
"no-such-book": "Babu irin wannan littafin: {{BOOK_NAME}}",
|
||||
"too-many-books": "An nemi littattafai da yawa ({{NB_BOOKS}}) inda iyaka shine {{LIMIT}}",
|
||||
"no-book-found": "Babu wani littafi da ya dace da ma'aunin zaɓi",
|
||||
"url-not-found": "Ba a sami URL ɗin da ake nema \"{{url}}\" akan wannan sabar ba.",
|
||||
"suggest-search": "Yi cikakken bincike na rubutu don <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Kash! An kasa ɗaukar labarin bazuwar :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} ba ingantaccen buƙatun ɗanyen abun ciki bane.",
|
||||
"invalid-request": "URL ɗin da ake nema \"{{{url}}}\" ba buƙatu mai inganci bane.",
|
||||
"no-value-for-arg": "Babu ƙima da aka bayar don hujja {{ARGUMENT}}",
|
||||
"no-query": "Ba a bayar da tambaya ba.",
|
||||
"raw-entry-not-found": "Ba a iya samun shigarwar {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "nema mara inganci",
|
||||
"400-page-heading": "nema mara inganci",
|
||||
"404-page-title": "Ba a samo abun ciki ba",
|
||||
"404-page-heading": "Ba a Samu ba",
|
||||
"500-page-title": "Kuskuren na Cikin Saba",
|
||||
"500-page-heading": "Kuskuren na Cikin Saba",
|
||||
"500-page-text": "An sami kuskuren uwar garken ciki. Munyi nadama akan hakan :/",
|
||||
"fulltext-search-unavailable": "Babu binciken cikakken rubutu",
|
||||
"no-search-results": "Babu injin binciken cikakken rubutu don wannan abun ciki.",
|
||||
"search-results-page-title": "Bincika: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Sakamako <b>{{START}}-{{END}}</b> na <b>{{COUNT}}</b> na <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Ba a sami sakamakon <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "daga {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} kalmomi",
|
||||
"library-button-text": "Je zuwa shafin maraba",
|
||||
"home-button-text": "Jeka babban shafin '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Je zuwa shafin da aka zaɓa ba da gangan ba",
|
||||
"searchbox-tooltip": "Bincika '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Littattafai biyu ko fiye a cikin harsuna daban-daban za su shiga cikin bincike, wanda zai iya haifar da sakamako mai ruɗani.",
|
||||
"welcome-page-overzealous-filter": "Babu sakamako. Kuna so a <a href=\"{{URL}}\">sake saita tace</a>?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> ne ke ƙarfafa shi",
|
||||
"search": "Nema",
|
||||
"book-filtering-all-categories": "Dukkanin nau'o'in",
|
||||
"book-filtering-all-languages": "Duka harsuna",
|
||||
"count-of-matching-books": "{{COUNT}} littafi(s)",
|
||||
"download": "Sauke",
|
||||
"direct-download-link-text": "Kai tsaye",
|
||||
"direct-download-alt-text": "saukewa kai tsaye",
|
||||
"hash-download-link-text": "Sha256 hash",
|
||||
"hash-download-alt-text": "sauke hash",
|
||||
"magnet-link-text": "Magnet link",
|
||||
"magnet-alt-text": "Magnet ɗin saukewa",
|
||||
"torrent-download-link-text": "Torrent fayil",
|
||||
"torrent-download-alt-text": "download torrent",
|
||||
"library-opds-feed-all-entries": "Ciyarwar OPDS Library - Duk shigarwar",
|
||||
"filter-by-tag": "Tace da alamar \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Dakatar da tacewa ta hanyar \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": " OPDS ciyar wa OPDS- entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Barka da zowa manhajar Kiwix",
|
||||
"download-links-heading": "Bring out ways through which people can join hands together .{{BOOK_TITLE}}",
|
||||
"download-links-title": "Sauke littafin",
|
||||
"preview-book": "Dubawa",
|
||||
"unknown-error": "Kuskuren da ba a sani ba"
|
||||
}
|
||||
@@ -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,8 +24,14 @@
|
||||
"404-page-heading": "לא נמצא",
|
||||
"500-page-title": "שגיאת שרת פנימית",
|
||||
"500-page-heading": "שגיאת שרת פנימית",
|
||||
"500-page-text": "אירעה שגיאת שרת פנימית. אנחנו מצטערים על זה :/",
|
||||
"fulltext-search-unavailable": "חיפוש בטקסט מלא אינו זמין",
|
||||
"no-search-results": "מנוע החיפוש בטקסט מלא אינו זמין עבור התוכן הזה.",
|
||||
"search-results-page-title": "חיפוש: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "תוצאות <b>{{START}} עד {{END}}</b> מתוך <b>{{COUNT}}</b> עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "לא נמצאו תוצאות עבור <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "מתוך {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} מילים",
|
||||
"library-button-text": "מעבר לדף הבית \"ברוך בואך\"",
|
||||
"home-button-text": "מעבר לדף הראשי של \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "מעבר לדף שנבחר אקראית",
|
||||
@@ -52,5 +59,6 @@
|
||||
"welcome-to-kiwix-server": "ברוך בואך לשרת קיוויקס",
|
||||
"download-links-heading": "הורדת קישורים עבור <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "הורדת ספר",
|
||||
"preview-book": "תצוגה מקדימה"
|
||||
"preview-book": "תצוגה מקדימה",
|
||||
"unknown-error": "שגיאה בלתי־ידועה"
|
||||
}
|
||||
|
||||
65
static/skin/i18n/ig.json
Normal file
65
static/skin/i18n/ig.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Accuratecy051",
|
||||
"Ngostary2k",
|
||||
"Oby Ezeilo"
|
||||
]
|
||||
},
|
||||
"name": "Bekee",
|
||||
"suggest-full-text-search": "nwere {{{SEARCH_TERMS}}}'",
|
||||
"no-such-book": "Enweghị akwụkwọ dị otú a: {{BOOK_NAME}}",
|
||||
"too-many-books": "Arịrịọ ọtụtụ akwụkwọ ({{NB_BOOKS}}) ebe oke bụ {{LIMIT}}",
|
||||
"no-book-found": "Ọ nweghị akwụkwọ dabara na nhọpụta nhọrọ",
|
||||
"url-not-found": "Ahụghị URL a rịọrọ \"{{url}}\" na nkesa a.",
|
||||
"suggest-search": "Mee ọchụchọ ederede zuru oke maka <a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a>",
|
||||
"random-article-failure": "Ee! Ịhọrọ akụkọ enweghị usoro :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}} abụghị arịrịọ ziri ezi maka ọdịnaya raw.",
|
||||
"invalid-request": "Arịrịọ gbasara URL \"{{{url}}}\" e zighi ezi.",
|
||||
"no-value-for-arg": "Ọ nweghị uru enyere maka arụmụka {{ARGUMENT}}",
|
||||
"no-query": "Ọnweghị ajụjụ enyere.",
|
||||
"raw-entry-not-found": "Enweghị ike ịchọta ntinye {{DATATYPE}} {{ENTRY}}",
|
||||
"400-page-title": "Arịrịọ na-ezighi ezi",
|
||||
"400-page-heading": "Arịrịọ na-ezighi ezi",
|
||||
"404-page-title": "Ahụghị ọdịnaya",
|
||||
"404-page-heading": "Ahụghị",
|
||||
"500-page-title": "Mperi Sava Ime",
|
||||
"500-page-heading": "Mperi Sava Ime",
|
||||
"500-page-text": "Enwere mperi ihe nkesa dị n'ime. Ọ dị anyị nwute na nke ahụ :/",
|
||||
"fulltext-search-unavailable": "Ọchịchọ ederede zuru ezu adịghị",
|
||||
"no-search-results": "Igwe nchọta ederede zuru oke adịghị maka ọdịnaya a.",
|
||||
"search-results-page-title": "Chọọ: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Rịzọlt ga <b>{{START}}-{{END}}</b> nke <b>{{COUNT}}</b> maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Ọnweghị rịzọlt ahụrụ maka <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "sitere na {{BOOK_TITLE}}",
|
||||
"word-count": "Okwu {{COUNT}}",
|
||||
"library-button-text": "Gaa na ibe nnabata",
|
||||
"home-button-text": "Gaa na isi ibe nke '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Gaa na ibe ahọpụtara enweghị usoro",
|
||||
"searchbox-tooltip": "Chọọ '{{BOOK_TITLE}}'",
|
||||
"confusion-of-tongues": "Akwụkwọ abụọ ma ọ bụ karịa n'asụsụ dị iche iche ga-esonye na nchọ, nke nwere ike ibute nsonaazụ mgbagwoju anya.",
|
||||
"welcome-page-overzealous-filter": "Enweghị nsonaazụ. Ọ ga-amasị gị <a href=\"{{URL}}\">ịtọgharịa nzacha</a> ?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> kwadoro ya",
|
||||
"search": "Chọọ",
|
||||
"book-filtering-all-categories": "Nkeji niile",
|
||||
"book-filtering-all-languages": "Asụsụ niile",
|
||||
"count-of-matching-books": "akwụkwọ {{COUNT}}",
|
||||
"download": "Budata",
|
||||
"direct-download-link-text": "Gosi",
|
||||
"direct-download-alt-text": "nbudata ozugbo",
|
||||
"hash-download-link-text": "Sha256 hash",
|
||||
"hash-download-alt-text": "budata hash",
|
||||
"magnet-link-text": "Njikọ magnet",
|
||||
"magnet-alt-text": "ibudata magnet",
|
||||
"torrent-download-link-text": " faịlụ nke Torrent",
|
||||
"torrent-download-alt-text": "Budata torrent",
|
||||
"library-opds-feed-all-entries": "Ọbá akwụkwọ OPDS Feed - Ihe niile",
|
||||
"filter-by-tag": "Wepụta site na mkpado \"{{TAG}}\"",
|
||||
"stop-filtering-by-tag": "Kwụsị nzacha site na mkpado \"{{TAG}}\"",
|
||||
"library-opds-feed-parameterised": "Ọbá akwụkwọ OPDS nri - ndenye dabara na {{#LANG}}\nAsụsụ: {{LANG}} {{/LANG}}{{#CATEGORY}}\n Kategori: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\n Ajụjụ: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Nabata na Kiwix Server",
|
||||
"download-links-heading": "Budata njikọ maka <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Budata akwụkwọ",
|
||||
"preview-book": "Ziwe nkirimaàtụ̀",
|
||||
"unknown-error": "amaghị m njehie"
|
||||
}
|
||||
@@ -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,12 @@
|
||||
"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 :/",
|
||||
"search-results-page-title": "Cerca: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Risultati <b>{{START}}-{{END}}</b> di <b>{{COUNT}}</b> per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Non è stato trovato alcun risultato per <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "da {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} parole",
|
||||
"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 +38,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",
|
||||
@@ -24,5 +25,6 @@
|
||||
"count-of-matching-books": "{{COUNT}} Buch/Bicher",
|
||||
"download": "Eroflueden",
|
||||
"direct-download-link-text": "Direkt",
|
||||
"download-links-title": "Buch eroflueden"
|
||||
"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,8 +24,14 @@
|
||||
"404-page-heading": "Не е најдено",
|
||||
"500-page-title": "Внатрешна грешка во опслужувачот",
|
||||
"500-page-heading": "Внатрешна грешка во опслужувачот",
|
||||
"500-page-text": "Настана внатрешна грешка во опслужувачот. Жал ни е :/",
|
||||
"fulltext-search-unavailable": "Целотекстното пребарување е недостапно",
|
||||
"no-search-results": "Погонот за целотекстно пребарување не е достапен за оваа содржина.",
|
||||
"search-results-page-title": "Пребарување: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Исход <b>{{START}}-{{END}}</b> од <b>{{COUNT}}</b> за <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"empty-search-results-page-header": "Не најдов ништо за <b>„{{{SEARCH_PATTERN}}}“</b>",
|
||||
"search-result-book-info": "од {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} зборови",
|
||||
"library-button-text": "Оди на воведната страница",
|
||||
"home-button-text": "Оди на главната страница на „{{BOOK_TITLE}}“",
|
||||
"random-page-button-text": "Оди на случајно избрана страница",
|
||||
@@ -52,5 +59,6 @@
|
||||
"welcome-to-kiwix-server": "Добре дојдовте на Опслужувачот на Кивикс",
|
||||
"download-links-heading": "Врски за преземање на <b><i>{{BOOK_TITLE}}</i></b>",
|
||||
"download-links-title": "Преземи книга",
|
||||
"preview-book": "Преглед"
|
||||
"preview-book": "Преглед",
|
||||
"unknown-error": "Непозната грешка"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
"home-button-text": "Przejdź do głównej strony '{{BOOK_TITLE}}'",
|
||||
"random-page-button-text": "Przejdź do losowo wybranej strony",
|
||||
"searchbox-tooltip": "Szukaj '{{BOOK_TITLE}}'",
|
||||
"search": "Szukaj",
|
||||
"book-filtering-all-categories": "Wszystkie Kategorie",
|
||||
"book-filtering-all-languages": "Wszystkie języki",
|
||||
"download": "Pobierz",
|
||||
"direct-download-link-text": "Bezpośrednio",
|
||||
"direct-download-alt-text": "bezpośrednie pobieranie",
|
||||
"torrent-download-link-text": "Plik torrent",
|
||||
"welcome-to-kiwix-server": "Witamy na serwerze Kiwix",
|
||||
"download-links-title": "Pobierz książkę",
|
||||
"preview-book": "Podgląd"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"Okras",
|
||||
"Pacha Tchernof",
|
||||
"Razno0",
|
||||
"Rofiatmustapha12",
|
||||
"Smavrina"
|
||||
]
|
||||
},
|
||||
@@ -18,6 +19,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": "Не удаётся найти запись {{ENTRY}} типа {{DATATYPE}}",
|
||||
@@ -27,30 +29,41 @@
|
||||
"404-page-heading": "Не найдено",
|
||||
"500-page-title": "Внутренняя ошибка сервера",
|
||||
"500-page-heading": "Внутренняя ошибка сервера",
|
||||
"500-page-text": "Произошла внутренняя ошибка сервера. Мы сожалеем об этом :/",
|
||||
"fulltext-search-unavailable": "Полнотекстовый поиск недоступен",
|
||||
"no-search-results": "Полнотекстовая поисковая система недоступна для этого содержания.",
|
||||
"search-results-page-title": "Поиск: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Результаты <b>{{START}}-{{END}}</b> из <b>{{COUNT}}</b> для <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "По запросу <b>\"{{{SEARCH_PATTERN}}}\"</b> результатов не найдено.",
|
||||
"search-result-book-info": "из {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} слов",
|
||||
"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": "Хэш Sha256",
|
||||
"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}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} {{#TAG}}\nTag: {{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": "Предпросмотр"
|
||||
"preview-book": "Предпросмотр",
|
||||
"unknown-error": "Неизвестная ошибка"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Eleassar",
|
||||
"Kelson"
|
||||
"Kelson",
|
||||
"Rofiatmustapha12"
|
||||
]
|
||||
},
|
||||
"name": "slovenščina",
|
||||
@@ -14,6 +15,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,8 +25,14 @@
|
||||
"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.",
|
||||
"search-results-page-title": "Iskanje: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Zadetki <b>{{START}}–{{END}}</b> od <b>{{COUNT}}</b> za »<b>{{{SEARCH_PATTERN}}}</b>«",
|
||||
"empty-search-results-page-header": "Ni zadetkov za »<b>{{{SEARCH_PATTERN}}}</b>«",
|
||||
"search-result-book-info": "iz {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} besed",
|
||||
"library-button-text": "Pojdite na pozdravno stran",
|
||||
"home-button-text": "Pojdite na glavno stran »{{BOOK_TITLE}}«",
|
||||
"random-page-button-text": "Pojdite na naključno izbrano stran",
|
||||
@@ -52,5 +60,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"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Jopparn",
|
||||
"Rofiatmustapha12",
|
||||
"Sabelöga",
|
||||
"WikiPhoenix"
|
||||
]
|
||||
@@ -15,6 +16,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,8 +26,14 @@
|
||||
"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.",
|
||||
"search-results-page-title": "Sök: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "Resultat <b>{{START}}-{{END}}</b> av <b>{{COUNT}}</b> för <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"empty-search-results-page-header": "Inga resultat hittades för <b>\"{{{SEARCH_PATTERN}}}\"</b>",
|
||||
"search-result-book-info": "från {{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} ord",
|
||||
"library-button-text": "Gå till hemsidan",
|
||||
"home-button-text": "Gå till huvudsidan för \"{{BOOK_TITLE}}\"",
|
||||
"random-page-button-text": "Gå till en slumpmässigt utvald sida",
|
||||
@@ -53,5 +61,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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Hedda"
|
||||
"Hedda",
|
||||
"Rofiatmustapha12"
|
||||
]
|
||||
},
|
||||
"name": "Türkçe",
|
||||
@@ -13,6 +14,7 @@
|
||||
"suggest-search": "<a href=\"{{{SEARCH_URL}}}\">{{PATTERN}}</a> için tam metin araması yapın",
|
||||
"random-article-failure": "Hata! Rastgele bir madde seçilemedi :(",
|
||||
"invalid-raw-data-type": "{{DATATYPE}}, ham içerik için geçerli bir istek değil.",
|
||||
"invalid-request": "İstenen \"{{{url}}}\" URL'si geçerli bir istek değil.",
|
||||
"no-value-for-arg": "{{ARGUMENT}} bağımsız değişkeni için değer sağlanmadı",
|
||||
"no-query": "Sorgu sağlanmadı.",
|
||||
"raw-entry-not-found": "{{DATATYPE}} {{ENTRY}} girişi bulunamadı",
|
||||
@@ -22,10 +24,41 @@
|
||||
"404-page-heading": "Bulunamadı",
|
||||
"500-page-title": "İç Sunucu Hatası",
|
||||
"500-page-heading": "İç Sunucu Hatası",
|
||||
"500-page-text": "Dahili bir sunucu hatası oluştu. Bunun için üzgünüz :/",
|
||||
"fulltext-search-unavailable": "Tam metin araması kullanılamıyor",
|
||||
"no-search-results": "Tam metin arama motoru bu içerik için kullanılamaz.",
|
||||
"search-results-page-title": "Arama: {{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b> için <b>{{COUNT}}</b> sonuçtan <b>{{START}}-{{END}}</b> arası sonuçlar",
|
||||
"empty-search-results-page-header": "<b>\"{{{SEARCH_PATTERN}}}\"</b> için sonuç bulunamadı",
|
||||
"search-result-book-info": "{{BOOK_TITLE}} adlı kitaptan",
|
||||
"word-count": "{{COUNT}} kelime",
|
||||
"library-button-text": "Karşılama sayfasına git",
|
||||
"home-button-text": "'{{BOOK_TITLE}}' anasayfasına gidin",
|
||||
"random-page-button-text": "Rastgele seçilen bir sayfaya git",
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' ara"
|
||||
"searchbox-tooltip": "'{{BOOK_TITLE}}' ara",
|
||||
"confusion-of-tongues": "Aramaya farklı dillerde iki veya daha fazla kitap katılacak ve bu da kafa karıştırıcı sonuçlara yol açabilecektir.",
|
||||
"welcome-page-overzealous-filter": "Sonuç yok. <a href=\"{{URL}}\">Filtreyi sıfırlamak</a> ister misiniz?",
|
||||
"powered-by-kiwix-html": "<a href=\"https://kiwix.org\">Kiwix</a> tarafından desteklenmektedir",
|
||||
"search": "Ara",
|
||||
"book-filtering-all-categories": "Tüm kategoriler",
|
||||
"book-filtering-all-languages": "Tüm diller",
|
||||
"count-of-matching-books": "{{COUNT}} kitap",
|
||||
"download": "İndir",
|
||||
"direct-download-link-text": "Doğrudan",
|
||||
"direct-download-alt-text": "direkt indirme",
|
||||
"hash-download-link-text": "Sha256 haşesi",
|
||||
"hash-download-alt-text": "csv indir",
|
||||
"magnet-link-text": "Mıknatıs bağlantısı",
|
||||
"magnet-alt-text": "mıknatısı indir",
|
||||
"torrent-download-link-text": "Hedef dosya",
|
||||
"torrent-download-alt-text": "torrenti indir",
|
||||
"library-opds-feed-all-entries": "Kütüphane OPDS Akışı - Tüm girişler",
|
||||
"filter-by-tag": "\"{{TAG}}\" etiketine göre filtrele",
|
||||
"stop-filtering-by-tag": "\"{{TAG}}\" etiketine göre filtrelemeyi durdur",
|
||||
"library-opds-feed-parameterised": "Kütüphane OPDS Özet Akışı - {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}} ile eşleşen girişler {{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "Kiwix Sunucusuna Hoş Geldiniz",
|
||||
"download-links-heading": "<b><i>{{BOOK_TITLE}}</i></b> için indirme bağlantıları",
|
||||
"download-links-title": "Kitapları indir",
|
||||
"preview-book": "Önizleme",
|
||||
"unknown-error": "Bilinmeyen hata"
|
||||
}
|
||||
|
||||
@@ -2,24 +2,63 @@
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"GuoPC",
|
||||
"IceButBin",
|
||||
"StarrySky",
|
||||
"Sunai",
|
||||
"XtexChooser"
|
||||
]
|
||||
},
|
||||
"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}} 对原请求无效。",
|
||||
"invalid-request": "请求的URL无效:{{{url}}}",
|
||||
"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": "内部服务器错误",
|
||||
"500-page-text": "内部服务器出现错误。真的十分抱歉 (;ŏ﹏ŏ)",
|
||||
"fulltext-search-unavailable": "全文搜索不可用",
|
||||
"no-search-results": "全文搜索引擎不适用于该内容。",
|
||||
"search-results-page-title": "搜索:{{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "<b>“{{{SEARCH_PATTERN}}}”</b>的<b>第 {{START}}-{{END}}</b>个结果(共<b>{{COUNT}}</b>个)",
|
||||
"empty-search-results-page-header": "未找到<b>“{{{SEARCH_PATTERN}}}”</b>的结果",
|
||||
"search-result-book-info": "来自{{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}} 个字",
|
||||
"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": "Sha256 哈希值",
|
||||
"hash-download-alt-text": "下载哈希值",
|
||||
"magnet-link-text": "磁力链接",
|
||||
"magnet-alt-text": "下载磁力链接",
|
||||
"torrent-download-link-text": "种子文件",
|
||||
"torrent-download-alt-text": "下载种子文件",
|
||||
"library-opds-feed-all-entries": "图书馆 OPDS Feed - 所有条目",
|
||||
"filter-by-tag": "按标签“{{TAG}}”过滤",
|
||||
"stop-filtering-by-tag": "停止按标签“{{TAG}}”过滤",
|
||||
"library-opds-feed-parameterised": "图书馆 OPDS Feed - 匹配的项目 {{#LANG}}\n语言:{{LANG}} {{/LANG}}{{#CATEGORY}}\n分类:{{CATEGORY}} {{/CATEGORY}}{{#TAG}}\n标签:{{TAG}} {{/TAG}}{{#Q}}\n查询:{{Q}} {{/Q}}",
|
||||
"welcome-to-kiwix-server": "欢迎来到 Kiwix 服务器",
|
||||
"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,8 +25,14 @@
|
||||
"404-page-heading": "查無頁面",
|
||||
"500-page-title": "內部伺服器錯誤",
|
||||
"500-page-heading": "內部伺服器錯誤",
|
||||
"500-page-text": "內部伺服器發生錯誤。對此我們深感抱歉:/",
|
||||
"fulltext-search-unavailable": "全文搜尋無效",
|
||||
"no-search-results": "全文搜尋引擎不適用此內容。",
|
||||
"search-results-page-title": "搜尋:{{SEARCH_PATTERN}}",
|
||||
"search-results-page-header": "<b>「{{{SEARCH_PATTERN}}}」</b>的<b>第{{START}}-{{END}}筆</b>結果(共<b>{{COUNT}}</b>筆)",
|
||||
"empty-search-results-page-header": "未找到<b>「{{{SEARCH_PATTERN}}}」</b>的結果",
|
||||
"search-result-book-info": "來自{{BOOK_TITLE}}",
|
||||
"word-count": "{{COUNT}}個字",
|
||||
"library-button-text": "前往歡迎首頁",
|
||||
"home-button-text": "前往「{{BOOK_TITLE}}」的首頁",
|
||||
"random-page-button-text": "前往隨機選取頁面",
|
||||
@@ -53,5 +60,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,104 +1,192 @@
|
||||
const uiLanguages = [
|
||||
{
|
||||
"الإنجليزية": "ar"
|
||||
"iso_code": "ar",
|
||||
"self_name": "الإنجليزية",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"বাংলা": "bn"
|
||||
"iso_code": "bn",
|
||||
"self_name": "বাংলা",
|
||||
"translation_count": 14
|
||||
},
|
||||
{
|
||||
"Čeština": "cs"
|
||||
"iso_code": "br",
|
||||
"self_name": "brezhoneg",
|
||||
"translation_count": 35
|
||||
},
|
||||
{
|
||||
"Deutsch": "de"
|
||||
"iso_code": "cs",
|
||||
"self_name": "Čeština",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"English": "en"
|
||||
"iso_code": "dag",
|
||||
"self_name": "Silimiinsili",
|
||||
"translation_count": 24
|
||||
},
|
||||
{
|
||||
"español": "es"
|
||||
"iso_code": "de",
|
||||
"self_name": "Deutsch",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"suomi": "fi"
|
||||
"iso_code": "en",
|
||||
"self_name": "English",
|
||||
"translation_count": 58
|
||||
},
|
||||
{
|
||||
"Français": "fr"
|
||||
"iso_code": "es",
|
||||
"self_name": "español",
|
||||
"translation_count": 48
|
||||
},
|
||||
{
|
||||
"עברית": "he"
|
||||
"iso_code": "fi",
|
||||
"self_name": "suomi",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"हिन्दी": "hi"
|
||||
"iso_code": "fr",
|
||||
"self_name": "Français",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Հայերեն": "hy"
|
||||
"iso_code": "ha",
|
||||
"self_name": "Turanci",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"interlingua": "ia"
|
||||
"iso_code": "he",
|
||||
"self_name": "עברית",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"italiano": "it"
|
||||
"iso_code": "hi",
|
||||
"self_name": "हिन्दी",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"日本語": "ja"
|
||||
"iso_code": "hy",
|
||||
"self_name": "Հայերեն",
|
||||
"translation_count": 15
|
||||
},
|
||||
{
|
||||
"한국어": "ko"
|
||||
"iso_code": "ia",
|
||||
"self_name": "interlingua",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"kurdî": "ku-latn"
|
||||
"iso_code": "ig",
|
||||
"self_name": "Bekee",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Lëtzebuergesch": "lb"
|
||||
"iso_code": "it",
|
||||
"self_name": "italiano",
|
||||
"translation_count": 34
|
||||
},
|
||||
{
|
||||
"македонски": "mk"
|
||||
"iso_code": "ja",
|
||||
"self_name": "日本語",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"Bahasa Melayu": "ms"
|
||||
"iso_code": "ko",
|
||||
"self_name": "한국어",
|
||||
"translation_count": 13
|
||||
},
|
||||
{
|
||||
"Nederlands": "nl"
|
||||
"iso_code": "ku-latn",
|
||||
"self_name": "kurdî",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"ߒߞߏ": "nqo"
|
||||
"iso_code": "lb",
|
||||
"self_name": "Lëtzebuergesch",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"ଓଡ଼ିଆ": "or"
|
||||
"iso_code": "mk",
|
||||
"self_name": "македонски",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Polski": "pl"
|
||||
"iso_code": "ms",
|
||||
"self_name": "Bahasa Melayu",
|
||||
"translation_count": 14
|
||||
},
|
||||
{
|
||||
"русский": "ru"
|
||||
"iso_code": "nl",
|
||||
"self_name": "Nederlands",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"Sardu": "sc"
|
||||
"iso_code": "nqo",
|
||||
"self_name": "ߒߞߏ",
|
||||
"translation_count": 43
|
||||
},
|
||||
{
|
||||
"slovenčina": "sk"
|
||||
"iso_code": "or",
|
||||
"self_name": "ଓଡ଼ିଆ",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"سرائیکی": "skr-arab"
|
||||
"iso_code": "pl",
|
||||
"self_name": "Polski",
|
||||
"translation_count": 31
|
||||
},
|
||||
{
|
||||
"slovenščina": "sl"
|
||||
"iso_code": "ru",
|
||||
"self_name": "русский",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Shqip": "sq"
|
||||
"iso_code": "sc",
|
||||
"self_name": "Sardu",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"Svenska": "sv"
|
||||
"iso_code": "sk",
|
||||
"self_name": "slovenčina",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"ఇంగ్లీషు": "te"
|
||||
"iso_code": "skr-arab",
|
||||
"self_name": "سرائیکی",
|
||||
"translation_count": 20
|
||||
},
|
||||
{
|
||||
"Türkçe": "tr"
|
||||
"iso_code": "sl",
|
||||
"self_name": "slovenščina",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"英语": "zh-hans"
|
||||
"iso_code": "sq",
|
||||
"self_name": "Shqip",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"繁體中文": "zh-hant"
|
||||
"iso_code": "sv",
|
||||
"self_name": "Svenska",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "te",
|
||||
"self_name": "ఇంగ్లీషు",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "tr",
|
||||
"self_name": "Türkçe",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hans",
|
||||
"self_name": "英语",
|
||||
"translation_count": 54
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hant",
|
||||
"self_name": "繁體中文",
|
||||
"translation_count": 57
|
||||
}
|
||||
]
|
||||
21
static/skin/polyfills.js
Normal file
21
static/skin/polyfills.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// A few browsers do not support the use of String.prototype.replaceAll method.
|
||||
// Hence we define it once we verify that it isn't supported. For documentation
|
||||
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
|
||||
if (!String.prototype.replaceAll) {
|
||||
String.prototype.replaceAll = function (pattern, replacement) {
|
||||
// verify parameter: It must either be a string or a RegExp with a global flag.
|
||||
if (typeof pattern[Symbol.replace] === 'function') {
|
||||
// the pattern is a RegExp check for the presence of g flag.
|
||||
if (pattern.global) {
|
||||
return this.replace(pattern, replacement);
|
||||
} else {
|
||||
throw new TypeError('Global flag for regular expressions')
|
||||
}
|
||||
}
|
||||
// the pattern is not a RegExp, hence it must be a string.
|
||||
if (typeof pattern !== 'string') {
|
||||
throw new TypeError('pattern must either be a string or a RegExp with a global (g) flag.')
|
||||
}
|
||||
return this.replace(new RegExp(pattern, 'g'), replacement);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -31,6 +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">
|
||||
<script type="text/javascript" src="./skin/polyfills.js?KIWIXCACHEID"></script>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const viewerSettings = {
|
||||
toolbarEnabled: {{enable_toolbar}},
|
||||
linkBlockingEnabled: {{enable_link_blocking}},
|
||||
libraryButtonEnabled: {{enable_library_button}},
|
||||
defaultUserLanguage: "{{default_user_language}}"
|
||||
libraryButtonEnabled: {{enable_library_button}}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<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/autoComplete/css/autoComplete.css?KIWIXCACHEID" rel="Stylesheet" />
|
||||
<script type="text/javascript" src="./skin/polyfills.js?KIWIXCACHEID"></script>
|
||||
<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>
|
||||
|
||||
@@ -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 \"\"");
|
||||
}
|
||||
}
|
||||
493
test/library.cpp
493
test/library.cpp
@@ -20,7 +20,6 @@
|
||||
#include "gtest/gtest.h"
|
||||
#include <string>
|
||||
|
||||
|
||||
const char * sampleOpdsStream = R"(
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||
xmlns:dc="http://purl.org/dc/terms/"
|
||||
@@ -28,12 +27,12 @@ const char * sampleOpdsStream = R"(
|
||||
<id>00000000-0000-0000-0000-000000000000</id>
|
||||
<entry>
|
||||
<title>Encyclopédie de la Tunisie</title>
|
||||
<name>wikipedia_fr_tunisie_novid_2018-10</name>
|
||||
<flavour>unforgettable</flavour>
|
||||
<name>wikipedia_fr_tunisie</name>
|
||||
<flavour>novid</flavour>
|
||||
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd</id>
|
||||
<icon>/meta?name=favicon&content=wikipedia_fr_tunisie_novid_2018-10</icon>
|
||||
<updated>2018-10-08T00:00::00:Z</updated>
|
||||
<dc:issued>8 Oct 2018</dc:issued>
|
||||
<dc:issued>2018-10-08T00:00::00:Z</dc:issued>
|
||||
<language>fra</language>
|
||||
<summary>Le meilleur de Wikipédia sur la Tunisie</summary>
|
||||
<tags>wikipedia;novid;_ftindex</tags>
|
||||
@@ -49,9 +48,53 @@ const char * sampleOpdsStream = R"(
|
||||
<mediaCount>1100</mediaCount>
|
||||
<articleCount>172</articleCount>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>Encyclopédie de la Tunisie</title>
|
||||
<name>wikipedia_fr_tunisie</name>
|
||||
<flavour>novid</flavour>
|
||||
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater</id>
|
||||
<updated>2019-10-08T00:00::00:Z</updated>
|
||||
<dc:issued>2019-10-08T00:00::00:Z</dc:issued>
|
||||
<language>fra</language>
|
||||
<summary>Le meilleur de Wikipédia sur la Tunisie. Updated in 2019</summary>
|
||||
<author>
|
||||
<name>Wikipedia</name>
|
||||
</author>
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4" length="90030080" />
|
||||
</entry>
|
||||
<entry>
|
||||
<title>Encyclopédie de la Tunisie</title>
|
||||
<name>wikipedia_fr_tunisie</name>
|
||||
<flavour>other_flavour</flavour>
|
||||
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd_flavour</id>
|
||||
<updated>2018-10-08T00:00::00:Z</updated>
|
||||
<dc:issued>2018-10-08T00:00::00:Z</dc:issued>
|
||||
<language>fra</language>
|
||||
<summary>Le meilleur de Wikipédia sur la Tunisie. With another flavour</summary>
|
||||
<author>
|
||||
<name>Wikipedia</name>
|
||||
</author>
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4" length="90030080" />
|
||||
</entry>
|
||||
<entry>
|
||||
<title>Encyclopédie de la Tunisie</title>
|
||||
<name>wikipedia_fr_tunisie</name>
|
||||
<flavour>other_flavour</flavour>
|
||||
<id>urn:uuid:0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater_flavour</id>
|
||||
<updated>2019-10-08T00:00::00:Z</updated>
|
||||
<dc:issued>2019-10-08T00:00::00:Z</dc:issued>
|
||||
<language>fra</language>
|
||||
<summary>Le meilleur de Wikipédia sur la Tunisie. Updated in 2019, and other flavour</summary>
|
||||
<author>
|
||||
<name>Wikipedia</name>
|
||||
</author>
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/wikipedia/wikipedia_fr_tunisie_novid_2018-10.zim.meta4" length="90030080" />
|
||||
</entry>
|
||||
<entry>
|
||||
<title>Tania Louis</title>
|
||||
<id>urn:uuid:0d0bcd57-d3f6-cb22-44cc-a723ccb4e1b2</id>
|
||||
<name>biologie-tout-compris_fr_all</name>
|
||||
<flavour>full</flavour>
|
||||
<icon>/meta?name=favicon&content=biologie-tout-compris_fr_all_2018-06</icon>
|
||||
<updated>2018-06-23T00:00::00:Z</updated>
|
||||
<language>fra</language>
|
||||
@@ -67,6 +110,8 @@ const char * sampleOpdsStream = R"(
|
||||
<entry>
|
||||
<title>Wikiquote</title>
|
||||
<id>urn:uuid:0ea1cde6-441d-6c58-f2c7-21c2838e659f</id>
|
||||
<name>wikiquote_fr_all</name>
|
||||
<flavour>full</flavour>
|
||||
<icon>/meta?name=favicon&content=wikiquote_fr_all_nopic_2019-06</icon>
|
||||
<updated>2019-06-05T00:00::00:Z</updated>
|
||||
<language>fra,ita</language>
|
||||
@@ -83,6 +128,8 @@ const char * sampleOpdsStream = R"(
|
||||
<entry>
|
||||
<title>Géographie par Wikipédia</title>
|
||||
<id>urn:uuid:1123e574-6eef-6d54-28fc-13e4caeae474</id>
|
||||
<name>wikipedia_fr_geography</name>
|
||||
<flavour>full</flavour>
|
||||
<icon>/meta?name=favicon&content=wikipedia_fr_geography_nopic_2019-06</icon>
|
||||
<updated>2019-06-02T00:00::00:Z</updated>
|
||||
<summary>Une sélection d'articles de Wikipédia sur la géographie</summary>
|
||||
@@ -99,6 +146,8 @@ const char * sampleOpdsStream = R"(
|
||||
<entry>
|
||||
<title>Mathématiques</title>
|
||||
<id>urn:uuid:14829621-c490-c376-0792-9de558b57efa</id>
|
||||
<name>wikipedia_fr_mathematics</name>
|
||||
<flavour>novid</flavour>
|
||||
<icon>/meta?name=favicon&content=wikipedia_fr_mathematics_nopic_2019-05</icon>
|
||||
<updated>2019-05-13T00:00::00:Z</updated>
|
||||
<language>fra</language>
|
||||
@@ -115,6 +164,8 @@ const char * sampleOpdsStream = R"(
|
||||
<entry>
|
||||
<title>Granblue Fantasy Wiki</title>
|
||||
<id>urn:uuid:006cbd1b-16d8-b00d-a584-c1ae110a94ed</id>
|
||||
<name>grandbluefantasy_en_all</name>
|
||||
<flavour>novid</flavour>
|
||||
<icon>/meta?name=favicon&content=granbluefantasy_en_all_all_nopic_2018-10</icon>
|
||||
<updated>2018-10-14T00:00::00:Z</updated>
|
||||
<language>eng</language>
|
||||
@@ -130,6 +181,8 @@ const char * sampleOpdsStream = R"(
|
||||
<entry>
|
||||
<title>Movies & TV Stack Exchange</title>
|
||||
<id>urn:uuid:00f37b00-f4da-0675-995a-770f9c72903e</id>
|
||||
<name>movies.stackexchange.com_en_all</name>
|
||||
<flavour>novid</flavour>
|
||||
<icon>/meta?name=favicon&content=movies.stackexchange.com_en_all_2019-02</icon>
|
||||
<updated>2019-02-03T00:00::00:Z</updated>
|
||||
<language>eng</language>
|
||||
@@ -143,8 +196,10 @@ const char * sampleOpdsStream = R"(
|
||||
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&content=movies.stackexchange.com_en_all_2019-02" />
|
||||
</entry>
|
||||
<entry>
|
||||
<title>TED talks - Business</title>
|
||||
<title>TED"talks" - Business</title>
|
||||
<id>urn:uuid:0189d9be-2fd0-b4b6-7300-20fab0b5cdc8</id>
|
||||
<name>ted_en_business</name>
|
||||
<flavour>nodet</flavour>
|
||||
<icon>/meta?name=favicon&content=ted_en_business_2018-07</icon>
|
||||
<updated>2018-07-23T00:00::00:Z</updated>
|
||||
<language>eng</language>
|
||||
@@ -157,9 +212,28 @@ const char * sampleOpdsStream = R"(
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/ted/ted_en_business_2018-07.zim.meta4" length="8855827456" />
|
||||
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&content=ted_en_business_2018-07" />
|
||||
</entry>
|
||||
<entry>
|
||||
<title>Business talks about TED</title>
|
||||
<id>Dummy id </id>
|
||||
<name>speak_business</name>
|
||||
<flavour>nodet</flavour>
|
||||
<icon>/meta?name=favicon&content=ted_en_business_2018-07</icon>
|
||||
<updated>2018-08-23T00:00::00:Z</updated>
|
||||
<language>eng</language>
|
||||
<summary>Ideas worth spreading</summary>
|
||||
<tags></tags>
|
||||
<link type="text/html" href="/ted_en_business_2018-07" />
|
||||
<author>
|
||||
<name>TED</name>
|
||||
</author>
|
||||
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="http://download.kiwix.org/zim/ted/ted_en_business_2018-07.zim.meta4" length="8855827456" />
|
||||
<link rel="http://opds-spec.org/image/thumbnail" type="image/png" href="/meta?name=favicon&content=ted_en_business_2018-07" />
|
||||
</entry>
|
||||
<entry>
|
||||
<title>Mythology & Folklore Stack Exchange</title>
|
||||
<id>urn:uuid:028055ac-4acc-1d54-65e0-a96de45e1b22</id>
|
||||
<name>mythology.stackexchange.com_en_all</name>
|
||||
<flavour>novid</flavour>
|
||||
<icon>/meta?name=favicon&content=mythology.stackexchange.com_en_all_2019-02</icon>
|
||||
<updated>2019-02-03T00:00::00:Z</updated>
|
||||
<language>eng</language>
|
||||
@@ -175,6 +249,8 @@ const char * sampleOpdsStream = R"(
|
||||
<entry>
|
||||
<title>Islam Stack Exchange</title>
|
||||
<id>urn:uuid:02e9c7ff-36fc-9c6e-6ac7-cd7085989029</id>
|
||||
<name>islam.stackexchange.com_en_all</name>
|
||||
<flavour>novid</flavour>
|
||||
<icon>/meta?name=favicon&content=islam.stackexchange.com_en_all_2019-01</icon>
|
||||
<updated>2019-01-31T00:00::00:Z</updated>
|
||||
<language>eng</language>
|
||||
@@ -229,6 +305,7 @@ const char sampleLibraryXML[] = R"(
|
||||
|
||||
#include "../include/library.h"
|
||||
#include "../include/manager.h"
|
||||
#include "../include/book.h"
|
||||
#include "../include/bookmark.h"
|
||||
|
||||
namespace
|
||||
@@ -242,17 +319,17 @@ TEST(LibraryOpdsImportTest, allInOne)
|
||||
kiwix::Manager manager(lib);
|
||||
manager.readOpds(sampleOpdsStream, "library-opds-import.unittests.dev");
|
||||
|
||||
EXPECT_EQ(10U, lib->getBookCount(true, true));
|
||||
EXPECT_EQ(14U, lib->getBookCount(true, true));
|
||||
|
||||
{
|
||||
const kiwix::Book& book1 = lib->getBookById("0c45160e-f917-760a-9159-dfe3c53cdcdd");
|
||||
|
||||
EXPECT_EQ(book1.getTitle(), "Encyclopédie de la Tunisie");
|
||||
EXPECT_EQ(book1.getName(), "wikipedia_fr_tunisie_novid_2018-10");
|
||||
EXPECT_EQ(book1.getFlavour(), "unforgettable");
|
||||
EXPECT_EQ(book1.getName(), "wikipedia_fr_tunisie");
|
||||
EXPECT_EQ(book1.getFlavour(), "novid");
|
||||
EXPECT_EQ(book1.getLanguages(), Langs{ "fra" });
|
||||
EXPECT_EQ(book1.getCommaSeparatedLanguages(), "fra");
|
||||
EXPECT_EQ(book1.getDate(), "8 Oct 2018");
|
||||
EXPECT_EQ(book1.getDate(), "2018-10-08");
|
||||
EXPECT_EQ(book1.getDescription(), "Le meilleur de Wikipédia sur la Tunisie");
|
||||
EXPECT_EQ(book1.getCreator(), "Wikipedia");
|
||||
EXPECT_EQ(book1.getPublisher(), "Wikipedia Publishing House");
|
||||
@@ -272,9 +349,9 @@ TEST(LibraryOpdsImportTest, allInOne)
|
||||
|
||||
{
|
||||
const kiwix::Book& book2 = lib->getBookById("0189d9be-2fd0-b4b6-7300-20fab0b5cdc8");
|
||||
EXPECT_EQ(book2.getTitle(), "TED talks - Business");
|
||||
EXPECT_EQ(book2.getName(), "");
|
||||
EXPECT_EQ(book2.getFlavour(), "");
|
||||
EXPECT_EQ(book2.getTitle(), "TED\"talks\" - Business");
|
||||
EXPECT_EQ(book2.getName(), "ted_en_business");
|
||||
EXPECT_EQ(book2.getFlavour(), "nodet");
|
||||
EXPECT_EQ(book2.getLanguages(), Langs{ "eng" });
|
||||
EXPECT_EQ(book2.getCommaSeparatedLanguages(), "eng");
|
||||
EXPECT_EQ(book2.getDate(), "2018-07-23");
|
||||
@@ -309,11 +386,18 @@ class LibraryTest : public ::testing::Test {
|
||||
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
|
||||
}
|
||||
|
||||
kiwix::Bookmark createBookmark(const std::string &id) {
|
||||
kiwix::Bookmark bookmark;
|
||||
bookmark.setBookId(id);
|
||||
return bookmark;
|
||||
};
|
||||
kiwix::Bookmark createBookmark(const std::string &id, const std::string& url="", const std::string& title="") {
|
||||
kiwix::Bookmark bookmark;
|
||||
bookmark.setBookId(id);
|
||||
bookmark.setUrl(url);
|
||||
bookmark.setTitle(title);
|
||||
return bookmark;
|
||||
};
|
||||
|
||||
kiwix::Bookmark createBookmark(const kiwix::Book& book, const std::string& url="", const std::string& title="") {
|
||||
kiwix::Bookmark bookmark(book, url, title);
|
||||
return bookmark;
|
||||
};
|
||||
|
||||
TitleCollection ids2Titles(const BookIdCollection& ids) {
|
||||
TitleCollection titles;
|
||||
@@ -327,14 +411,35 @@ class LibraryTest : public ::testing::Test {
|
||||
std::shared_ptr<kiwix::Library> lib;
|
||||
};
|
||||
|
||||
TEST_F(LibraryTest, createBookMark)
|
||||
{
|
||||
auto bookId = "0c45160e-f917-760a-9159-dfe3c53cdcdd";
|
||||
auto book = lib->getBookById(bookId);
|
||||
|
||||
auto bookmark = createBookmark(book, "/a/url", "A title");
|
||||
|
||||
EXPECT_EQ(bookmark.getUrl(), "/a/url");
|
||||
EXPECT_EQ(bookmark.getTitle(), "A title");
|
||||
EXPECT_EQ(bookmark.getBookId(), bookId);
|
||||
EXPECT_EQ(bookmark.getBookName(), book.getName());
|
||||
EXPECT_EQ(bookmark.getBookName(), "wikipedia_fr_tunisie");
|
||||
EXPECT_EQ(bookmark.getBookTitle(), book.getTitle());
|
||||
EXPECT_EQ(bookmark.getDate(), book.getDate());
|
||||
EXPECT_EQ(bookmark.getBookFlavour(), book.getFlavour());
|
||||
EXPECT_EQ(bookmark.getLanguage(), book.getCommaSeparatedLanguages());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, getBookMarksTest)
|
||||
{
|
||||
auto bookId1 = lib->getBooksIds()[0];
|
||||
auto bookId2 = lib->getBooksIds()[1];
|
||||
auto bookId1 = "0c45160e-f917-760a-9159-dfe3c53cdcdd";
|
||||
auto bookId2 = "0189d9be-2fd0-b4b6-7300-20fab0b5cdc8";
|
||||
|
||||
lib->addBookmark(createBookmark(bookId1));
|
||||
lib->addBookmark(createBookmark("invalid-bookmark-id"));
|
||||
lib->addBookmark(createBookmark(bookId2));
|
||||
auto book1 = lib->getBookById(bookId1);
|
||||
auto book2 = lib->getBookById(bookId2);
|
||||
|
||||
lib->addBookmark(createBookmark(book1));
|
||||
lib->addBookmark(createBookmark("invalid-book-id"));
|
||||
lib->addBookmark(createBookmark(book2));
|
||||
auto onlyValidBookmarks = lib->getBookmarks();
|
||||
auto allBookmarks = lib->getBookmarks(false);
|
||||
|
||||
@@ -342,13 +447,284 @@ TEST_F(LibraryTest, getBookMarksTest)
|
||||
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
|
||||
|
||||
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1);
|
||||
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-bookmark-id");
|
||||
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
|
||||
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, bookmarksSerializationTest)
|
||||
{
|
||||
auto bookId1 = lib->getBooksIds()[0];
|
||||
auto bookId2 = lib->getBooksIds()[1];
|
||||
|
||||
auto book1 = lib->getBookById(bookId1);
|
||||
auto book2 = lib->getBookById(bookId2);
|
||||
|
||||
// Create bookmarks using three different ways.
|
||||
lib->addBookmark(createBookmark(bookId1, "a/url", "Article title1"));
|
||||
lib->addBookmark(createBookmark("invalid-book-id", "another/url", "Unknown title"));
|
||||
lib->addBookmark(createBookmark(book2, "a/url/2", "Article title2"));
|
||||
|
||||
lib->writeBookmarksToFile("__test__bookmarks.xml");
|
||||
|
||||
// Build a new library
|
||||
auto new_lib = kiwix::Library::create();
|
||||
{
|
||||
kiwix::Manager manager(new_lib);
|
||||
manager.readOpds(sampleOpdsStream, "foo.urlHost");
|
||||
manager.readXml(sampleLibraryXML, false, "./test/library.xml", true);
|
||||
manager.readBookmarkFile("__test__bookmarks.xml");
|
||||
}
|
||||
std::remove("__test__bookmarks.xml");
|
||||
|
||||
auto onlyValidBookmarks = new_lib->getBookmarks();
|
||||
auto allBookmarks = new_lib->getBookmarks(false);
|
||||
|
||||
ASSERT_EQ(onlyValidBookmarks.size(), 2U);
|
||||
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
|
||||
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
|
||||
|
||||
ASSERT_EQ(allBookmarks.size(), 3U);
|
||||
auto bookmark1 = allBookmarks[0];
|
||||
EXPECT_EQ(bookmark1.getBookId(), bookId1);
|
||||
EXPECT_EQ(bookmark1.getBookTitle(), book1.getTitle());
|
||||
EXPECT_EQ(bookmark1.getBookName(), book1.getName());
|
||||
EXPECT_EQ(bookmark1.getBookFlavour(), book1.getFlavour());
|
||||
EXPECT_EQ(bookmark1.getUrl(), "a/url");
|
||||
EXPECT_EQ(bookmark1.getTitle(), "Article title1");
|
||||
EXPECT_EQ(bookmark1.getLanguage(), book1.getCommaSeparatedLanguages());
|
||||
EXPECT_EQ(bookmark1.getDate(), book1.getDate());
|
||||
|
||||
auto bookmark2 = allBookmarks[1];
|
||||
EXPECT_EQ(bookmark2.getBookId(), "invalid-book-id");
|
||||
EXPECT_EQ(bookmark2.getBookTitle(), "");
|
||||
EXPECT_EQ(bookmark2.getBookName(), "");
|
||||
EXPECT_EQ(bookmark2.getBookFlavour(), "");
|
||||
EXPECT_EQ(bookmark2.getUrl(), "another/url");
|
||||
EXPECT_EQ(bookmark2.getTitle(), "Unknown title");
|
||||
EXPECT_EQ(bookmark2.getLanguage(), "");
|
||||
EXPECT_EQ(bookmark2.getDate(), "");
|
||||
|
||||
auto bookmark3 = allBookmarks[2];
|
||||
EXPECT_EQ(bookmark3.getBookId(), bookId2);
|
||||
EXPECT_EQ(bookmark3.getBookTitle(), book2.getTitle());
|
||||
EXPECT_EQ(bookmark3.getBookName(), book2.getName());
|
||||
EXPECT_EQ(bookmark3.getBookFlavour(), book2.getFlavour());
|
||||
EXPECT_EQ(bookmark3.getUrl(), "a/url/2");
|
||||
EXPECT_EQ(bookmark3.getTitle(), "Article title2");
|
||||
EXPECT_EQ(bookmark3.getLanguage(), book2.getCommaSeparatedLanguages());
|
||||
EXPECT_EQ(bookmark3.getDate(), book2.getDate());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, MigrateBookmark)
|
||||
{
|
||||
std::string bookId1 = "0c45160e-f917-760a-9159-dfe3c53cdcdd";
|
||||
std::string bookId2 = "0189d9be-2fd0-b4b6-7300-20fab0b5cdc8";
|
||||
|
||||
auto book1 = lib->getBookById(bookId1);
|
||||
auto book1Flavour = lib->getBookById(bookId1+"_flavour");
|
||||
auto book2 = lib->getBookById(bookId2);
|
||||
|
||||
lib->addBookmark(createBookmark(book1));
|
||||
lib->addBookmark(createBookmark("invalid-book-id"));
|
||||
lib->addBookmark(createBookmark(book2));
|
||||
|
||||
auto wrongIdBookmark = createBookmark(book1);
|
||||
wrongIdBookmark.setBookId("wrong-book-id");
|
||||
lib->addBookmark(wrongIdBookmark);
|
||||
|
||||
auto wrongIdBookmarkNoName = createBookmark(book2);
|
||||
wrongIdBookmarkNoName.setBookId("wrong-book-id-noname");
|
||||
wrongIdBookmarkNoName.setBookName("");
|
||||
lib->addBookmark(wrongIdBookmarkNoName);
|
||||
|
||||
auto wrongIdFlavourBookmark = createBookmark(book1Flavour);
|
||||
wrongIdFlavourBookmark.setBookId("wrong-book-flavour-id");
|
||||
lib->addBookmark(wrongIdFlavourBookmark);
|
||||
|
||||
auto onlyValidBookmarks = lib->getBookmarks();
|
||||
auto allBookmarks = lib->getBookmarks(false);
|
||||
|
||||
ASSERT_EQ(onlyValidBookmarks.size(), 2U);
|
||||
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
|
||||
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
|
||||
|
||||
ASSERT_EQ(allBookmarks.size(), 6U);
|
||||
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1);
|
||||
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
|
||||
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[3].getBookId(), "wrong-book-id");
|
||||
EXPECT_EQ(allBookmarks[4].getBookId(), "wrong-book-id-noname");
|
||||
EXPECT_EQ(allBookmarks[5].getBookId(), "wrong-book-flavour-id");
|
||||
|
||||
ASSERT_EQ(lib->migrateBookmarks("no-existant-book"), 0);
|
||||
|
||||
ASSERT_EQ(lib->migrateBookmarks(), std::make_tuple(3, 4));
|
||||
|
||||
onlyValidBookmarks = lib->getBookmarks();
|
||||
allBookmarks = lib->getBookmarks(false);
|
||||
|
||||
ASSERT_EQ(onlyValidBookmarks.size(), 5U);
|
||||
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId1);
|
||||
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[2].getBookId(), bookId1+"_updated1yearlater");
|
||||
EXPECT_EQ(onlyValidBookmarks[3].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[4].getBookId(), bookId1+"_updated1yearlater_flavour");
|
||||
|
||||
ASSERT_EQ(allBookmarks.size(), 6U);
|
||||
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1);
|
||||
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
|
||||
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[3].getBookId(), bookId1+"_updated1yearlater");
|
||||
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
|
||||
|
||||
ASSERT_EQ(lib->migrateBookmarks(), std::make_tuple(0, 1));
|
||||
|
||||
ASSERT_EQ(lib->migrateBookmarks(bookId1), 1);
|
||||
allBookmarks = lib->getBookmarks(false);
|
||||
ASSERT_EQ(allBookmarks.size(), 6U);
|
||||
EXPECT_EQ(allBookmarks[0].getBookId(), bookId1+"_updated1yearlater");
|
||||
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
|
||||
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[3].getBookId(), bookId1+"_updated1yearlater");
|
||||
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
|
||||
|
||||
ASSERT_EQ(lib->migrateBookmarks(bookId1, bookId2), 0); // No more bookId1 bookmark
|
||||
|
||||
ASSERT_EQ(lib->migrateBookmarks(bookId1+"_updated1yearlater", bookId2), 2);
|
||||
onlyValidBookmarks = lib->getBookmarks();
|
||||
allBookmarks = lib->getBookmarks(false);
|
||||
|
||||
ASSERT_EQ(onlyValidBookmarks.size(), 5U);
|
||||
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[2].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[3].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[4].getBookId(), bookId1+"_updated1yearlater_flavour");
|
||||
|
||||
ASSERT_EQ(allBookmarks.size(), 6U);
|
||||
EXPECT_EQ(allBookmarks[0].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[1].getBookId(), "invalid-book-id");
|
||||
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[3].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
|
||||
|
||||
ASSERT_EQ(lib->migrateBookmarks("invalid-book-id", bookId1), 1);
|
||||
|
||||
onlyValidBookmarks = lib->getBookmarks();
|
||||
allBookmarks = lib->getBookmarks(false);
|
||||
|
||||
ASSERT_EQ(onlyValidBookmarks.size(), 6U);
|
||||
EXPECT_EQ(onlyValidBookmarks[0].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[1].getBookId(), bookId1);
|
||||
EXPECT_EQ(onlyValidBookmarks[2].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[3].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[4].getBookId(), bookId2);
|
||||
EXPECT_EQ(onlyValidBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
|
||||
|
||||
|
||||
ASSERT_EQ(allBookmarks.size(), 6U);
|
||||
EXPECT_EQ(allBookmarks[0].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[1].getBookId(), bookId1);
|
||||
EXPECT_EQ(allBookmarks[2].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[3].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[4].getBookId(), bookId2);
|
||||
EXPECT_EQ(allBookmarks[5].getBookId(), bookId1+"_updated1yearlater_flavour");
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, GetBestTargetBookIdOlder)
|
||||
{
|
||||
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd");
|
||||
|
||||
auto book = lib->getBookById(bookId);
|
||||
|
||||
auto validBookmark = createBookmark(book);
|
||||
lib->addBookmark(validBookmark);
|
||||
|
||||
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::UPGRADE_ONLY), bookId+"_updated1yearlater");
|
||||
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::ALLOW_DOWNGRADE), bookId+"_updated1yearlater");
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, GetBestTargetBookIdNewer)
|
||||
{
|
||||
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater");
|
||||
|
||||
auto book = lib->getBookById(bookId);
|
||||
EXPECT_EQ(book.getDate(), "2019-10-08");
|
||||
|
||||
auto validBookmark = createBookmark(book);
|
||||
// Make the bookmark more recent than any books in the library.
|
||||
// (But still pointing to existing book)
|
||||
validBookmark.setDate("2020-10-08");
|
||||
lib->addBookmark(validBookmark);
|
||||
|
||||
// The best book for the bookmark is bookId...
|
||||
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::UPGRADE_ONLY), bookId);
|
||||
// but there is not migration to do as the bookmark already point to it.
|
||||
ASSERT_EQ(lib->migrateBookmarks(bookId, kiwix::UPGRADE_ONLY), 0);
|
||||
|
||||
ASSERT_EQ(lib->getBestTargetBookId(validBookmark, kiwix::ALLOW_DOWNGRADE), bookId);
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, GetBestTargetBookIdInvalidOlder)
|
||||
{
|
||||
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd");
|
||||
|
||||
auto book = lib->getBookById(bookId);
|
||||
|
||||
auto invalidBookmark = createBookmark(book);
|
||||
invalidBookmark.setBookId("invalid-book-id");
|
||||
lib->addBookmark(invalidBookmark);
|
||||
|
||||
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::UPGRADE_ONLY), bookId+"_updated1yearlater");
|
||||
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::ALLOW_DOWNGRADE), bookId+"_updated1yearlater");
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, GetBestTargetBookIdInvalidNewer)
|
||||
{
|
||||
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd");
|
||||
|
||||
auto book = lib->getBookById(bookId);
|
||||
EXPECT_EQ(book.getDate(), "2018-10-08");
|
||||
|
||||
auto invalidBookmark = createBookmark(book);
|
||||
invalidBookmark.setBookId("invalid-book-id");
|
||||
invalidBookmark.setDate("2020-10-08");
|
||||
lib->addBookmark(invalidBookmark);
|
||||
|
||||
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::UPGRADE_ONLY), "");
|
||||
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::ALLOW_DOWNGRADE), bookId+"_updated1yearlater");
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, GetBestTargetBookIdFlavour)
|
||||
{
|
||||
auto bookId = std::string("0c45160e-f917-760a-9159-dfe3c53cdcdd_flavour");
|
||||
|
||||
auto book = lib->getBookById(bookId);
|
||||
EXPECT_EQ(book.getDate(), "2018-10-08");
|
||||
|
||||
auto invalidBookmark = createBookmark(book);
|
||||
invalidBookmark.setBookId("invalid-book-id");
|
||||
invalidBookmark.setDate("2020-10-08");
|
||||
lib->addBookmark(invalidBookmark);
|
||||
|
||||
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::UPGRADE_ONLY), "");
|
||||
ASSERT_EQ(lib->getBestTargetBookId(invalidBookmark, kiwix::ALLOW_DOWNGRADE), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater_flavour");
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, GetBestTargetBookIdName)
|
||||
{
|
||||
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie"), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater");
|
||||
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie", "novid"), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater");
|
||||
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie", "other_flavour"), "0c45160e-f917-760a-9159-dfe3c53cdcdd_updated1yearlater_flavour");
|
||||
ASSERT_EQ(lib->getBestTargetBookId("wikipedia_fr_tunisie", "other_flavour", "2020-12-12"), "");
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, sanityCheck)
|
||||
{
|
||||
EXPECT_EQ(lib->getBookCount(true, true), 12U);
|
||||
EXPECT_EQ(lib->getBookCount(true, true), 16U);
|
||||
EXPECT_EQ(lib->getBooksLanguages(),
|
||||
std::vector<std::string>({"deu", "eng", "fra", "ita", "spa"})
|
||||
);
|
||||
@@ -400,6 +776,10 @@ TEST_F(LibraryTest, filterLocal)
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().local(false),
|
||||
"Business talks about TED",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Géographie par Wikipédia",
|
||||
@@ -407,7 +787,7 @@ TEST_F(LibraryTest, filterLocal)
|
||||
"Mathématiques",
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
"TED talks - Business",
|
||||
"TED\"talks\" - Business",
|
||||
"Tania Louis",
|
||||
"Wikiquote"
|
||||
);
|
||||
@@ -416,6 +796,10 @@ TEST_F(LibraryTest, filterLocal)
|
||||
TEST_F(LibraryTest, filterRemote)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().remote(true),
|
||||
"Business talks about TED",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Géographie par Wikipédia",
|
||||
@@ -424,7 +808,7 @@ TEST_F(LibraryTest, filterRemote)
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
"Ray Charles",
|
||||
"TED talks - Business",
|
||||
"TED\"talks\" - Business",
|
||||
"Tania Louis",
|
||||
"Wikiquote"
|
||||
);
|
||||
@@ -437,21 +821,23 @@ TEST_F(LibraryTest, filterRemote)
|
||||
TEST_F(LibraryTest, filterByLanguage)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().lang("eng"),
|
||||
"Business talks about TED",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Islam Stack Exchange",
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
"Ray Charles",
|
||||
"TED talks - Business"
|
||||
"TED\"talks\" - Business"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("lang:eng"),
|
||||
"Business talks about TED",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Islam Stack Exchange",
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
"Ray Charles",
|
||||
"TED talks - Business"
|
||||
"TED\"talks\" - Business"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("eng"),
|
||||
@@ -459,6 +845,25 @@ TEST_F(LibraryTest, filterByLanguage)
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, filterByFlavour)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().flavour("full"),
|
||||
"Géographie par Wikipédia",
|
||||
"Tania Louis",
|
||||
"Wikiquote"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("flavour:full"),
|
||||
"Géographie par Wikipédia",
|
||||
"Tania Louis",
|
||||
"Wikiquote"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("full"),
|
||||
/* no results */
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, filterByTags)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"stackexchange"}),
|
||||
@@ -558,6 +963,9 @@ TEST_F(LibraryTest, filterByQuery)
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki"),
|
||||
"An example ZIM archive", // due to the "wikibooks" tag
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Géographie par Wikipédia",
|
||||
"Mathématiques", // due to the "wikipedia" tag
|
||||
@@ -576,6 +984,10 @@ TEST_F(LibraryTest, filteringByEmptyQueryReturnsAllEntries)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query(""),
|
||||
"An example ZIM archive",
|
||||
"Business talks about TED",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Géographie par Wikipédia",
|
||||
@@ -584,7 +996,7 @@ TEST_F(LibraryTest, filteringByEmptyQueryReturnsAllEntries)
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
"Ray Charles",
|
||||
"TED talks - Business",
|
||||
"TED\"talks\" - Business",
|
||||
"Tania Louis",
|
||||
"Wikiquote"
|
||||
);
|
||||
@@ -593,6 +1005,9 @@ TEST_F(LibraryTest, filteringByEmptyQueryReturnsAllEntries)
|
||||
TEST_F(LibraryTest, filterByCreator)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Wikipedia"),
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Géographie par Wikipédia",
|
||||
"Mathématiques",
|
||||
@@ -634,6 +1049,9 @@ TEST_F(LibraryTest, filterByCreator)
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("creator:Wikipedia"),
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Géographie par Wikipédia",
|
||||
"Mathématiques",
|
||||
@@ -741,6 +1159,9 @@ TEST_F(LibraryTest, filterByMaxSize)
|
||||
TEST_F(LibraryTest, filterByMultipleCriteria)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia"),
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Géographie par Wikipédia",
|
||||
"Mathématiques", // due to the "wikipedia" tag
|
||||
@@ -748,11 +1169,17 @@ TEST_F(LibraryTest, filterByMultipleCriteria)
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia").maxSize(100000000UL),
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Ray Charles"
|
||||
);
|
||||
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia").maxSize(100000000UL).local(false),
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie"
|
||||
);
|
||||
}
|
||||
@@ -810,6 +1237,10 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
|
||||
{
|
||||
EXPECT_FILTER_RESULTS(kiwix::Filter(),
|
||||
"An example ZIM archive",
|
||||
"Business talks about TED",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Encyclopédie de la Tunisie",
|
||||
"Granblue Fantasy Wiki",
|
||||
"Géographie par Wikipédia",
|
||||
@@ -818,7 +1249,7 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
|
||||
"Movies & TV Stack Exchange",
|
||||
"Mythology & Folklore Stack Exchange",
|
||||
"Ray Charles",
|
||||
"TED talks - Business",
|
||||
"TED\"talks\" - Business",
|
||||
"Tania Louis",
|
||||
"Wikiquote"
|
||||
);
|
||||
@@ -832,7 +1263,7 @@ TEST_F(LibraryTest, removeBooksNotUpdatedSince)
|
||||
|
||||
const uint64_t rev2 = lib->getRevision();
|
||||
|
||||
EXPECT_EQ(9u, lib->removeBooksNotUpdatedSince(rev));
|
||||
EXPECT_EQ(13u, lib->removeBooksNotUpdatedSince(rev));
|
||||
|
||||
EXPECT_GT(lib->getRevision(), rev2);
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ tests = [
|
||||
'name_mapper',
|
||||
'opds_catalog',
|
||||
'server_helper',
|
||||
'lrucache'
|
||||
'lrucache',
|
||||
'i18n',
|
||||
'response'
|
||||
]
|
||||
|
||||
if build_machine.system() != 'windows'
|
||||
|
||||
@@ -110,10 +110,10 @@ TEST(Suggestions, specialCharHandling)
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "Title with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippet with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"value" : "Title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippet with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"kind" : "path"
|
||||
, "path" : "Path with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
, "path" : "Path with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
}
|
||||
]
|
||||
)EXPECTEDJSON"
|
||||
@@ -128,10 +128,10 @@ R"EXPECTEDJSON([
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "Snippetless title with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippetless title with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"value" : "Snippetless title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"label" : "Snippetless title with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?",
|
||||
"kind" : "path"
|
||||
, "path" : "Path with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
, "path" : "Path with \t\n\r\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?"
|
||||
}
|
||||
]
|
||||
)EXPECTEDJSON"
|
||||
@@ -145,8 +145,8 @@ R"EXPECTEDJSON([
|
||||
CHECK_SUGGESTIONS(s.getJSON(),
|
||||
R"EXPECTEDJSON([
|
||||
{
|
||||
"value" : "text with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.? ",
|
||||
"label" : "containing 'text with \u0009\u0010\u0013\\<>&'"~!@#$%^*()_+`-=[]{}|:;,.?'...",
|
||||
"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>
|
||||
)");
|
||||
}
|
||||
348
test/server.cpp
348
test/server.cpp
@@ -59,7 +59,7 @@ const ResourceCollection resources200Compressible{
|
||||
{ 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=6a8c6fb2" },
|
||||
{ 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=1e78e7cf" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
|
||||
@@ -75,7 +75,7 @@ const ResourceCollection resources200Compressible{
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/taskbar.css" },
|
||||
{ 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=201653b8" },
|
||||
{ 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" },
|
||||
@@ -83,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=5be77f5c" },
|
||||
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/catalog/search" },
|
||||
|
||||
@@ -144,12 +146,12 @@ const ResourceCollection resources200Uncompressible{
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/hash.png?cacheid=f836e872" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/magnet.png" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/magnet.png?cacheid=73b6bddf" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/polyfills.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/polyfills.js?cacheid=a0e0343d" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/search-icon.svg" },
|
||||
{ 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=96f2cf73" },
|
||||
|
||||
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" },
|
||||
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Description" },
|
||||
@@ -285,8 +287,9 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=2158fad9"
|
||||
<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">
|
||||
<script type="module" src="/ROOT%23%3F/skin/i18n.js?cacheid=6a8c6fb2" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=96f2cf73" defer></script>
|
||||
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></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=5be77f5c" 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=ce19da2a" defer></script>
|
||||
@@ -318,9 +321,10 @@ R"EXPECTEDRESULT( <img src="${root}/skin/download
|
||||
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=6a8c6fb2" defer></script>
|
||||
<script type="text/javascript" src="./skin/languages.js?cacheid=96f2cf73" defer></script>
|
||||
<script type="text/javascript" src="./skin/viewer.js?cacheid=201653b8" defer></script>
|
||||
<script type="text/javascript" src="./skin/polyfills.js?cacheid=a0e0343d"></script>
|
||||
<script type="module" src="./skin/i18n.js?cacheid=071abc9a" defer></script>
|
||||
<script type="text/javascript" src="./skin/languages.js?cacheid=5be77f5c" 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";
|
||||
<label for="kiwix_button_show_toggle"><img src="./skin/caret.png?cacheid=22b942b4" alt=""></label>
|
||||
@@ -337,6 +341,7 @@ R"EXPECTEDRESULT( <link type="text/css" href="./skin/kiwix.css?cacheid=2158fa
|
||||
// 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"
|
||||
},
|
||||
};
|
||||
@@ -535,6 +540,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;
|
||||
@@ -544,6 +550,7 @@ enum ExpectedResponseDataType
|
||||
{
|
||||
expected_page_title,
|
||||
expected_css_url,
|
||||
expected_kiwix_response_data,
|
||||
book_name,
|
||||
book_title,
|
||||
expected_body
|
||||
@@ -556,11 +563,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{};
|
||||
}
|
||||
}
|
||||
@@ -579,6 +588,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)
|
||||
@@ -607,19 +617,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"
|
||||
@@ -630,8 +650,10 @@ std::string TestContentIn404HtmlResponse::expectedResponse() const
|
||||
+ frag[1]
|
||||
+ pageCssLink()
|
||||
+ frag[2]
|
||||
+ expectedKiwixResponseData
|
||||
+ frag[3]
|
||||
+ expectedBody
|
||||
+ frag[3];
|
||||
+ frag[4];
|
||||
}
|
||||
|
||||
std::string TestContentIn404HtmlResponse::pageTitle() const
|
||||
@@ -648,7 +670,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
|
||||
@@ -676,6 +699,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>
|
||||
@@ -685,6 +709,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>
|
||||
@@ -693,6 +718,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>
|
||||
@@ -701,6 +727,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>
|
||||
@@ -710,6 +737,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>
|
||||
@@ -718,6 +746,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>
|
||||
@@ -727,6 +756,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>
|
||||
@@ -735,6 +765,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>
|
||||
@@ -748,6 +779,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>
|
||||
@@ -759,6 +791,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>
|
||||
@@ -772,6 +805,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>
|
||||
@@ -782,10 +816,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>
|
||||
@@ -797,6 +848,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>
|
||||
@@ -808,6 +860,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>
|
||||
@@ -819,6 +872,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>
|
||||
@@ -830,6 +884,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>
|
||||
@@ -845,6 +900,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>
|
||||
@@ -866,6 +922,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>
|
||||
@@ -876,6 +933,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>
|
||||
@@ -886,6 +944,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>
|
||||
@@ -896,6 +955,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>
|
||||
@@ -908,6 +968,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>
|
||||
@@ -918,6 +979,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>
|
||||
@@ -927,6 +989,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 ) {
|
||||
@@ -1023,7 +1099,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>
|
||||
@@ -1041,6 +1120,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,106 +1130,194 @@ 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": 14
|
||||
},
|
||||
{
|
||||
"Čeština": "cs"
|
||||
"iso_code": "br",
|
||||
"self_name": "brezhoneg",
|
||||
"translation_count": 35
|
||||
},
|
||||
{
|
||||
"Deutsch": "de"
|
||||
"iso_code": "cs",
|
||||
"self_name": "Čeština",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"English": "en"
|
||||
"iso_code": "dag",
|
||||
"self_name": "Silimiinsili",
|
||||
"translation_count": 24
|
||||
},
|
||||
{
|
||||
"español": "es"
|
||||
"iso_code": "de",
|
||||
"self_name": "Deutsch",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"suomi": "fi"
|
||||
"iso_code": "en",
|
||||
"self_name": "English",
|
||||
"translation_count": 58
|
||||
},
|
||||
{
|
||||
"Français": "fr"
|
||||
"iso_code": "es",
|
||||
"self_name": "español",
|
||||
"translation_count": 48
|
||||
},
|
||||
{
|
||||
"עברית": "he"
|
||||
"iso_code": "fi",
|
||||
"self_name": "suomi",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"हिन्दी": "hi"
|
||||
"iso_code": "fr",
|
||||
"self_name": "Français",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Հայերեն": "hy"
|
||||
"iso_code": "ha",
|
||||
"self_name": "Turanci",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"interlingua": "ia"
|
||||
"iso_code": "he",
|
||||
"self_name": "עברית",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"italiano": "it"
|
||||
"iso_code": "hi",
|
||||
"self_name": "हिन्दी",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"日本語": "ja"
|
||||
"iso_code": "hy",
|
||||
"self_name": "Հայերեն",
|
||||
"translation_count": 15
|
||||
},
|
||||
{
|
||||
"한국어": "ko"
|
||||
"iso_code": "ia",
|
||||
"self_name": "interlingua",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"kurdî": "ku-latn"
|
||||
"iso_code": "ig",
|
||||
"self_name": "Bekee",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Lëtzebuergesch": "lb"
|
||||
"iso_code": "it",
|
||||
"self_name": "italiano",
|
||||
"translation_count": 34
|
||||
},
|
||||
{
|
||||
"македонски": "mk"
|
||||
"iso_code": "ja",
|
||||
"self_name": "日本語",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"Bahasa Melayu": "ms"
|
||||
"iso_code": "ko",
|
||||
"self_name": "한국어",
|
||||
"translation_count": 13
|
||||
},
|
||||
{
|
||||
"Nederlands": "nl"
|
||||
"iso_code": "ku-latn",
|
||||
"self_name": "kurdî",
|
||||
"translation_count": 26
|
||||
},
|
||||
{
|
||||
"ߒߞߏ": "nqo"
|
||||
"iso_code": "lb",
|
||||
"self_name": "Lëtzebuergesch",
|
||||
"translation_count": 22
|
||||
},
|
||||
{
|
||||
"ଓଡ଼ିଆ": "or"
|
||||
"iso_code": "mk",
|
||||
"self_name": "македонски",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Polski": "pl"
|
||||
"iso_code": "ms",
|
||||
"self_name": "Bahasa Melayu",
|
||||
"translation_count": 14
|
||||
},
|
||||
{
|
||||
"русский": "ru"
|
||||
"iso_code": "nl",
|
||||
"self_name": "Nederlands",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"Sardu": "sc"
|
||||
"iso_code": "nqo",
|
||||
"self_name": "ߒߞߏ",
|
||||
"translation_count": 43
|
||||
},
|
||||
{
|
||||
"slovenčina": "sk"
|
||||
"iso_code": "or",
|
||||
"self_name": "ଓଡ଼ିଆ",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"سرائیکی": "skr-arab"
|
||||
"iso_code": "pl",
|
||||
"self_name": "Polski",
|
||||
"translation_count": 31
|
||||
},
|
||||
{
|
||||
"slovenščina": "sl"
|
||||
"iso_code": "ru",
|
||||
"self_name": "русский",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"Shqip": "sq"
|
||||
"iso_code": "sc",
|
||||
"self_name": "Sardu",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"Svenska": "sv"
|
||||
"iso_code": "sk",
|
||||
"self_name": "slovenčina",
|
||||
"translation_count": 25
|
||||
},
|
||||
{
|
||||
"ఇంగ్లీషు": "te"
|
||||
"iso_code": "skr-arab",
|
||||
"self_name": "سرائیکی",
|
||||
"translation_count": 20
|
||||
},
|
||||
{
|
||||
"Türkçe": "tr"
|
||||
"iso_code": "sl",
|
||||
"self_name": "slovenščina",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"英语": "zh-hans"
|
||||
"iso_code": "sq",
|
||||
"self_name": "Shqip",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"繁體中文": "zh-hant"
|
||||
"iso_code": "sv",
|
||||
"self_name": "Svenska",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "te",
|
||||
"self_name": "ఇంగ్లీషు",
|
||||
"translation_count": 49
|
||||
},
|
||||
{
|
||||
"iso_code": "tr",
|
||||
"self_name": "Türkçe",
|
||||
"translation_count": 57
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hans",
|
||||
"self_name": "英语",
|
||||
"translation_count": 54
|
||||
},
|
||||
{
|
||||
"iso_code": "zh-hant",
|
||||
"self_name": "繁體中文",
|
||||
"translation_count": 57
|
||||
}
|
||||
])EXPECTEDRESPONSE");
|
||||
}
|
||||
@@ -1161,7 +1329,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 std::string expectedH1;
|
||||
|
||||
operator TestContext() const
|
||||
@@ -1172,64 +1339,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,
|
||||
/* 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,
|
||||
/* 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,
|
||||
/* 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,
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"Accept-Language: header is respected",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "test",
|
||||
/*Request Cookie:*/ NO_COOKIE,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
"userlang cookie is ignored",
|
||||
/*url*/ "/ROOT%23%3F/content/zimfile/invalid-article",
|
||||
/*Accept-Language:*/ "",
|
||||
/*Request Cookie:*/ "userlang=test",
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
"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,
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
{
|
||||
@@ -1238,7 +1386,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,
|
||||
/* expected <h1> */ "[I18N TESTING] Content not found, but at least the server is alive"
|
||||
},
|
||||
{
|
||||
@@ -1247,7 +1394,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,
|
||||
/* expected <h1> */ "Not Found"
|
||||
},
|
||||
};
|
||||
@@ -1259,9 +1405,6 @@ 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);
|
||||
EXPECT_FALSE(r->has_header("Set-Cookie"));
|
||||
std::regex_search(r->body, h1Match, h1Regex);
|
||||
@@ -1958,8 +2101,7 @@ TEST_F(ServerTest, viewerSettings)
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: false,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: false,
|
||||
defaultUserLanguage: "en"
|
||||
libraryButtonEnabled: false
|
||||
}
|
||||
)");
|
||||
}
|
||||
@@ -1970,8 +2112,7 @@ R"(const viewerSettings = {
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: false,
|
||||
linkBlockingEnabled: true,
|
||||
libraryButtonEnabled: false,
|
||||
defaultUserLanguage: "en"
|
||||
libraryButtonEnabled: false
|
||||
}
|
||||
)");
|
||||
}
|
||||
@@ -1982,8 +2123,7 @@ R"(const viewerSettings = {
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: true,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: false,
|
||||
defaultUserLanguage: "en"
|
||||
libraryButtonEnabled: false
|
||||
}
|
||||
)");
|
||||
}
|
||||
@@ -1994,47 +2134,7 @@ R"(const viewerSettings = {
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: true,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: true,
|
||||
defaultUserLanguage: "en"
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
{
|
||||
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
|
||||
const Headers headers{ {"Accept-Language", "fr"} };
|
||||
ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js", headers)->body,
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: true,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: true,
|
||||
defaultUserLanguage: "fr"
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
{
|
||||
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
|
||||
const Headers headers{ {"Accept-Language", "test;q=0.2, en;q=0.9"} };
|
||||
ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js", headers)->body,
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: true,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: true,
|
||||
defaultUserLanguage: "en"
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
{
|
||||
resetServer(ZimFileServer::WITH_TASKBAR_AND_LIBRARY_BUTTON);
|
||||
const Headers headers{ {"Accept-Language", "test;q=0.9, en;q=0.2"} };
|
||||
ASSERT_EQ(zfs1_->GET("/ROOT%23%3F/viewer_settings.js", headers)->body,
|
||||
R"(const viewerSettings = {
|
||||
toolbarEnabled: true,
|
||||
linkBlockingEnabled: false,
|
||||
libraryButtonEnabled: true,
|
||||
defaultUserLanguage: "test"
|
||||
libraryButtonEnabled: true
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -190,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