Merge pull request #897 from kiwix/nojs

A gift to javascript naysayers
This commit is contained in:
Kelson 2023-03-29 16:15:43 +02:00 committed by GitHub
commit e13fed8670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 995 additions and 131 deletions

50
include/html_dumper.h Normal file
View File

@ -0,0 +1,50 @@
/*
* Copyright 2023 Nikhil Tanwar <2002nikhiltanwar@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_HTML_DUMPER_H
#define KIWIX_HTML_DUMPER_H
#include <string>
#include "library_dumper.h"
namespace kiwix
{
/**
* A class to dump Library in HTML format.
*/
class HTMLDumper : public LibraryDumper
{
public:
HTMLDumper(const Library* library, const NameMapper* NameMapper);
~HTMLDumper();
/**
* Dump library in HTML
*
* @return HTML content
*/
std::string dumpPlainHTML(kiwix::Filter filter) const;
};
}
#endif // KIWIX_HTML_DUMPER_H

View File

@ -120,6 +120,8 @@ class Filter {
Filter& maxSize(size_t size); Filter& maxSize(size_t size);
Filter& query(std::string query, bool partial=true); Filter& query(std::string query, bool partial=true);
Filter& name(std::string name); Filter& name(std::string name);
Filter& clearLang();
Filter& clearCategory();
bool hasQuery() const; bool hasQuery() const;
const std::string& getQuery() const { return _query; } const std::string& getQuery() const { return _query; }

91
include/library_dumper.h Normal file
View File

@ -0,0 +1,91 @@
/*
* Copyright 2023 Nikhil Tanwar <2002nikhiltanwar@gmail.com>
* Copyright 2017 Matthieu Gautier <mgautier@kymeria.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
#ifndef KIWIX_LIBRARY_DUMPER_H
#define KIWIX_LIBRARY_DUMPER_H
#include <string>
#include "library.h"
#include "name_mapper.h"
#include <mustache.hpp>
namespace kiwix
{
/**
* A base class to dump Library in various formats.
*
*/
class LibraryDumper
{
public:
LibraryDumper(const Library* library, const NameMapper* NameMapper);
~LibraryDumper();
void setLibraryId(const std::string& id) { this->libraryId = id;}
/**
* Set the root location used when generating url.
*
* @param rootLocation the root location to use.
*/
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
/**
* Set some informations about the search results.
*
* @param totalResult the total number of results of the search.
* @param startIndex the start index of the result.
* @param count the number of result of the current set (or page).
*/
void setOpenSearchInfo(int totalResult, int startIndex, int count);
/**
* Sets user default language
*
* @param userLang the user language to be set
*/
void setUserLanguage(std::string userLang) { this->m_userLang = userLang; }
/**
* Get the data of categories
*/
kainjow::mustache::list getCategoryData() const;
/**
* Get the data of languages
*/
kainjow::mustache::list getLanguageData() const;
protected:
const kiwix::Library* const library;
const kiwix::NameMapper* const nameMapper;
std::string libraryId;
std::string rootLocation;
std::string m_userLang;
int m_totalResults;
int m_startIndex;
int m_count;
};
}
#endif // KIWIX_LIBRARY_DUMPER_H

View File

@ -28,6 +28,7 @@
#include "library.h" #include "library.h"
#include "name_mapper.h" #include "name_mapper.h"
#include "library_dumper.h"
using namespace std; using namespace std;
@ -38,11 +39,10 @@ namespace kiwix
* A tool to dump a `Library` into a opds stream. * A tool to dump a `Library` into a opds stream.
* *
*/ */
class OPDSDumper class OPDSDumper : public LibraryDumper
{ {
public: public:
OPDSDumper() = default; OPDSDumper(const Library* library, const NameMapper* NameMapper);
OPDSDumper(Library* library, NameMapper* NameMapper);
~OPDSDumper(); ~OPDSDumper();
/** /**
@ -85,38 +85,6 @@ class OPDSDumper
* @return The OPDS feed. * @return The OPDS feed.
*/ */
std::string languagesOPDSFeed() const; std::string languagesOPDSFeed() const;
/**
* Set the id of the library.
*
* @param id the id to use.
*/
void setLibraryId(const std::string& id) { this->libraryId = id;}
/**
* Set the root location used when generating url.
*
* @param rootLocation the root location to use.
*/
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
/**
* Set some informations about the search results.
*
* @param totalResult the total number of results of the search.
* @param startIndex the start index of the result.
* @param count the number of result of the current set (or page).
*/
void setOpenSearchInfo(int totalResult, int startIndex, int count);
protected:
kiwix::Library* library;
kiwix::NameMapper* nameMapper;
std::string libraryId;
std::string rootLocation;
int m_totalResults;
int m_startIndex;
int m_count;
}; };
} }

120
src/html_dumper.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "html_dumper.h"
#include "libkiwix-resources.h"
#include "tools/otherTools.h"
#include "tools.h"
#include "tools/regexTools.h"
#include "server/i18n.h"
namespace kiwix
{
/* Constructor */
HTMLDumper::HTMLDumper(const Library* library, const NameMapper* nameMapper)
: LibraryDumper(library, nameMapper)
{
}
/* Destructor */
HTMLDumper::~HTMLDumper()
{
}
namespace {
std::string humanFriendlyTitle(std::string title)
{
std::string humanFriendlyString = replaceRegex(title, "_", " ");
humanFriendlyString[0] = toupper(humanFriendlyString[0]);
return humanFriendlyString;
}
kainjow::mustache::list getTagList(std::string tags)
{
const auto tagsList = kiwix::split(tags, ";", true, false);
kainjow::mustache::list finalTagList;
for (auto tag : tagsList) {
if (tag[0] != '_')
finalTagList.push_back(kainjow::mustache::object{
{"tag", tag}
});
}
return finalTagList;
}
} // unnamed namespace
std::string HTMLDumper::dumpPlainHTML(kiwix::Filter filter) const
{
kainjow::mustache::list booksData;
const auto filteredBooks = library->filter(filter);
const auto searchQuery = filter.getQuery();
auto languages = getLanguageData();
auto categories = getCategoryData();
for (auto &category : categories) {
const auto categoryName = category.get("name")->string_value();
if (categoryName == filter.getCategory()) {
category["selected"] = true;
}
category["hf_name"] = humanFriendlyTitle(categoryName);
}
for (auto &language : languages) {
if (language.get("lang_code")->string_value() == filter.getLang()) {
language["selected"] = true;
}
}
for ( const auto& bookId : filteredBooks ) {
const auto bookObj = library->getBookById(bookId);
const auto bookTitle = bookObj.getTitle();
std::string contentId = "";
try {
contentId = urlEncode(nameMapper->getNameForId(bookId));
} catch (...) {}
const auto bookDescription = bookObj.getDescription();
const auto langCode = bookObj.getCommaSeparatedLanguages();
const auto bookIconUrl = rootLocation + "/catalog/v2/illustration/" + bookId + "/?size=48";
const auto tags = bookObj.getTags();
const auto downloadAvailable = (bookObj.getUrl() != "");
std::string faviconAttr = "style=background-image:url(" + bookIconUrl + ")";
booksData.push_back(kainjow::mustache::object{
{"id", contentId},
{"title", bookTitle},
{"description", bookDescription},
{"langCode", langCode},
{"faviconAttr", faviconAttr},
{"tagList", getTagList(tags)},
{"downloadAvailable", downloadAvailable}
});
}
auto getTranslation = i18n::GetTranslatedStringWithMsgId(m_userLang);
const auto translations = kainjow::mustache::object{
getTranslation("search"),
getTranslation("download"),
getTranslation("count-of-matching-books", {{"COUNT", to_string(filteredBooks.size())}}),
getTranslation("book-filtering-all-categories"),
getTranslation("book-filtering-all-languages"),
getTranslation("powered-by-kiwix-html"),
getTranslation("welcome-to-kiwix-server"),
getTranslation("preview-book"),
getTranslation("welcome-page-overzealous-filter", {{"URL", "?lang="}})
};
return render_template(
RESOURCE::templates::no_js_library_page_html,
kainjow::mustache::object{
{"root", rootLocation},
{"books", booksData },
{"searchQuery", searchQuery},
{"languages", languages},
{"categories", categories},
{"noResults", filteredBooks.size() == 0},
{"translations", translations}
}
);
}
} // namespace kiwix

View File

@ -878,6 +878,18 @@ Filter& Filter::name(std::string name)
return *this; return *this;
} }
Filter& Filter::clearLang()
{
activeFilters &= ~LANG;
return *this;
}
Filter& Filter::clearCategory()
{
activeFilters &= ~CATEGORY;
return *this;
}
#define ACTIVE(X) (activeFilters & (X)) #define ACTIVE(X) (activeFilters & (X))
#define FILTER(TAG, TEST) if (ACTIVE(TAG) && !(TEST)) { return false; } #define FILTER(TAG, TEST) if (ACTIVE(TAG) && !(TEST)) { return false; }
bool Filter::hasQuery() const bool Filter::hasQuery() const

120
src/library_dumper.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "library_dumper.h"
#include "tools/stringTools.h"
#include "tools/otherTools.h"
#include "tools.h"
namespace kiwix
{
/* Constructor */
LibraryDumper::LibraryDumper(const Library* library, const NameMapper* nameMapper)
: library(library),
nameMapper(nameMapper)
{
}
/* Destructor */
LibraryDumper::~LibraryDumper()
{
}
void LibraryDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
{
m_totalResults = totalResults;
m_startIndex = startIndex,
m_count = count;
}
namespace {
std::map<std::string, std::string> iso639_3 = {
{"atj", "atikamekw"},
{"azb", "آذربایجان دیلی"},
{"bcl", "central bikol"},
{"bgs", "tagabawa"},
{"bxr", "буряад хэлэн"},
{"cbk", "chavacano"},
{"cdo", "閩東語"},
{"dag", "Dagbani"},
{"diq", "dimli"},
{"dty", "डोटेली"},
{"eml", "emiliân-rumagnōl"},
{"fbs", "српскохрватски"},
{"hbs", "srpskohrvatski"},
{"ido", "ido"},
{"kbp", "kabɩ"},
{"kld", "Gamilaraay"},
{"lbe", "лакку маз"},
{"lbj", "ལ་དྭགས་སྐད་"},
{"map", "Austronesian"},
{"mhr", "марий йылме"},
{"mnw", "ဘာသာမန်"},
{"myn", "mayan"},
{"nah", "nahuatl"},
{"nai", "north American Indian"},
{"nds", "plattdütsch"},
{"nrm", "bhasa narom"},
{"olo", "livvi"},
{"pih", "Pitcairn-Norfolk"},
{"pnb", "Western Panjabi"},
{"rmr", "Caló"},
{"rmy", "romani shib"},
{"roa", "romance languages"},
{"twi", "twi"},
};
std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
const ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
}
std::string getLanguageSelfName(const std::string& lang) {
const auto itr = iso639_3.find(lang);
if (itr != iso639_3.end()) {
return itr->second;
}
return lang;
};
} // unnamed namespace
kainjow::mustache::list LibraryDumper::getCategoryData() const
{
const auto now = gen_date_str();
kainjow::mustache::list categoryData;
for ( const auto& category : library->getBooksCategories() ) {
const auto urlencodedCategoryName = urlEncode(category);
categoryData.push_back(kainjow::mustache::object{
{"name", category},
{"urlencoded_name", urlencodedCategoryName},
{"updated", now},
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
});
}
return categoryData;
}
kainjow::mustache::list LibraryDumper::getLanguageData() const
{
const auto now = gen_date_str();
kainjow::mustache::list languageData;
std::call_once(fillLanguagesFlag, fillLanguagesMap);
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
const std::string languageCode = langAndBookCount.first;
const int bookCount = langAndBookCount.second;
const auto languageSelfName = getLanguageSelfName(languageCode);
languageData.push_back(kainjow::mustache::object{
{"lang_code", languageCode},
{"lang_self_name", languageSelfName},
{"book_count", to_string(bookCount)},
{"updated", now},
{"id", gen_uuid(libraryId + "/languages/" + languageCode)}
});
}
return languageData;
}
} // namespace kiwix

View File

@ -5,6 +5,8 @@ kiwix_sources = [
'manager.cpp', 'manager.cpp',
'libxml_dumper.cpp', 'libxml_dumper.cpp',
'opds_dumper.cpp', 'opds_dumper.cpp',
'html_dumper.cpp',
'library_dumper.cpp',
'downloader.cpp', 'downloader.cpp',
'server.cpp', 'server.cpp',
'search_renderer.cpp', 'search_renderer.cpp',

View File

@ -30,9 +30,8 @@ namespace kiwix
{ {
/* Constructor */ /* Constructor */
OPDSDumper::OPDSDumper(Library* library, NameMapper* nameMapper) OPDSDumper::OPDSDumper(const Library* library, const NameMapper* nameMapper)
: library(library), : LibraryDumper(library, nameMapper)
nameMapper(nameMapper)
{ {
} }
/* Destructor */ /* Destructor */
@ -40,13 +39,6 @@ OPDSDumper::~OPDSDumper()
{ {
} }
void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
{
m_totalResults = totalResults;
m_startIndex = startIndex,
m_count = count;
}
namespace namespace
{ {
@ -133,60 +125,6 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
return booksData; return booksData;
} }
std::map<std::string, std::string> iso639_3 = {
{"atj", "atikamekw"},
{"azb", "آذربایجان دیلی"},
{"bcl", "central bikol"},
{"bgs", "tagabawa"},
{"bxr", "буряад хэлэн"},
{"cbk", "chavacano"},
{"cdo", "閩東語"},
{"dag", "Dagbani"},
{"diq", "dimli"},
{"dty", "डोटेली"},
{"eml", "emiliân-rumagnōl"},
{"fbs", "српскохрватски"},
{"hbs", "srpskohrvatski"},
{"ido", "ido"},
{"kbp", "kabɩ"},
{"kld", "Gamilaraay"},
{"lbe", "лакку маз"},
{"lbj", "ལ་དྭགས་སྐད་"},
{"map", "Austronesian"},
{"mhr", "марий йылме"},
{"mnw", "ဘာသာမန်"},
{"myn", "mayan"},
{"nah", "nahuatl"},
{"nai", "north American Indian"},
{"nds", "plattdütsch"},
{"nrm", "bhasa narom"},
{"olo", "livvi"},
{"pih", "Pitcairn-Norfolk"},
{"pnb", "Western Panjabi"},
{"rmr", "Caló"},
{"rmy", "romani shib"},
{"roa", "romance languages"},
{"twi", "twi"},
};
std::once_flag fillLanguagesFlag;
void fillLanguagesMap()
{
for (auto icuLangPtr = icu::Locale::getISOLanguages(); *icuLangPtr != NULL; ++icuLangPtr) {
const ICULanguageInfo lang(*icuLangPtr);
iso639_3.insert({lang.iso3Code(), lang.selfName()});
}
}
std::string getLanguageSelfName(const std::string& lang) {
const auto itr = iso639_3.find(lang);
if (itr != iso639_3.end()) {
return itr->second;
}
return lang;
};
} // unnamed namespace } // unnamed namespace
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
@ -240,17 +178,7 @@ std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
std::string OPDSDumper::categoriesOPDSFeed() const std::string OPDSDumper::categoriesOPDSFeed() const
{ {
const auto now = gen_date_str(); const auto now = gen_date_str();
kainjow::mustache::list categoryData; kainjow::mustache::list categoryData = getCategoryData();
for ( const auto& category : library->getBooksCategories() ) {
const auto urlencodedCategoryName = urlEncode(category);
categoryData.push_back(kainjow::mustache::object{
{"name", category},
{"urlencoded_name", urlencodedCategoryName},
{"updated", now},
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
});
}
return render_template( return render_template(
RESOURCE::templates::catalog_v2_categories_xml, RESOURCE::templates::catalog_v2_categories_xml,
kainjow::mustache::object{ kainjow::mustache::object{
@ -265,21 +193,7 @@ std::string OPDSDumper::categoriesOPDSFeed() const
std::string OPDSDumper::languagesOPDSFeed() const std::string OPDSDumper::languagesOPDSFeed() const
{ {
const auto now = gen_date_str(); const auto now = gen_date_str();
kainjow::mustache::list languageData; kainjow::mustache::list languageData = getLanguageData();
std::call_once(fillLanguagesFlag, fillLanguagesMap);
for ( const auto& langAndBookCount : library->getBooksLanguagesWithCounts() ) {
const std::string languageCode = langAndBookCount.first;
const int bookCount = langAndBookCount.second;
const auto languageSelfName = getLanguageSelfName(languageCode);
languageData.push_back(kainjow::mustache::object{
{"lang_code", languageCode},
{"lang_self_name", languageSelfName},
{"book_count", to_string(bookCount)},
{"updated", now},
{"id", gen_uuid(libraryId + "/languages/" + languageCode)}
});
}
return render_template( return render_template(
RESOURCE::templates::catalog_v2_languages_xml, RESOURCE::templates::catalog_v2_languages_xml,
kainjow::mustache::object{ kainjow::mustache::object{

View File

@ -69,6 +69,28 @@ private:
const std::string m_lang; const std::string m_lang;
}; };
class GetTranslatedStringWithMsgId
{
typedef kainjow::mustache::basic_data<std::string> MustacheString;
typedef std::pair<std::string, MustacheString> MsgIdAndTranslation;
public:
explicit GetTranslatedStringWithMsgId(const std::string& lang) : m_lang(lang) {}
MsgIdAndTranslation operator()(const std::string& key) const
{
return {key, getTranslatedString(m_lang, key)};
}
MsgIdAndTranslation operator()(const std::string& key, const Parameters& params) const
{
return {key, expandParameterizedString(m_lang, key, params)};
}
private:
const std::string m_lang;
};
} // namespace i18n } // namespace i18n
struct ParameterizedMessage struct ParameterizedMessage

View File

@ -53,6 +53,7 @@ extern "C" {
#include "name_mapper.h" #include "name_mapper.h"
#include "search_renderer.h" #include "search_renderer.h"
#include "opds_dumper.h" #include "opds_dumper.h"
#include "html_dumper.h"
#include "i18n.h" #include "i18n.h"
#include <zim/uuid.h> #include <zim/uuid.h>
@ -627,6 +628,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
if (isEndpointUrl(url, "search")) if (isEndpointUrl(url, "search"))
return handle_search(request); return handle_search(request);
if (isEndpointUrl(url, "nojs"))
return handle_no_js(request);
if (isEndpointUrl(url, "suggest")) if (isEndpointUrl(url, "suggest"))
return handle_suggest(request); return handle_suggest(request);
@ -755,6 +759,73 @@ std::unique_ptr<Response> InternalServer::handle_viewer_settings(const RequestCo
return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8"); return ContentResponse::build(*this, RESOURCE::templates::viewer_settings_js, data, "application/javascript; charset=utf-8");
} }
std::string InternalServer::getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const
{
const auto book = mp_library->getBookById(bookId);
auto bookUrl = kiwix::stripSuffix(book.getUrl(), ".meta4");
auto getTranslation = i18n::GetTranslatedStringWithMsgId(userLang);
const auto translations = kainjow::mustache::object{
getTranslation("download-links-heading", {{"BOOK_TITLE", book.getTitle()}}),
getTranslation("download-links-title"),
getTranslation("direct-download-link-text"),
getTranslation("hash-download-link-text"),
getTranslation("magnet-link-text"),
getTranslation("torrent-download-link-text")
};
return render_template(
RESOURCE::templates::no_js_download_html,
kainjow::mustache::object{
{"url", bookUrl},
{"translations", translations}
}
);
}
std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& request)
{
const auto url = request.get_url();
const auto urlParts = kiwix::split(url, "/", true, false);
HTMLDumper htmlDumper(mp_library, mp_nameMapper);
htmlDumper.setRootLocation(m_root);
htmlDumper.setLibraryId(getLibraryId());
auto userLang = request.get_user_language();
htmlDumper.setUserLanguage(userLang);
std::string content;
if (urlParts.size() == 1) {
auto filter = get_search_filter(request);
try {
if (request.get_argument("category") == "") {
filter.clearCategory();
}
} catch (...) {}
try {
if (request.get_argument("lang") == "") {
filter.clearLang();
}
} catch (...) {}
content = htmlDumper.dumpPlainHTML(filter);
} else if ((urlParts.size() == 3) && (urlParts[1] == "download")) {
try {
const auto bookId = mp_nameMapper->getIdForName(urlParts[2]);
content = getNoJSDownloadPageHTML(bookId, userLang);
} catch (const std::out_of_range&) {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
} else {
return HTTP404Response(*this, request)
+ urlNotFoundMsg;
}
return ContentResponse::build(
*this,
content,
"text/html; charset=utf-8"
);
}
namespace namespace
{ {

View File

@ -131,6 +131,7 @@ class InternalServer {
std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request, bool partial); std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request, bool partial);
std::unique_ptr<Response> handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId); std::unique_ptr<Response> handle_catalog_v2_complete_entry(const RequestContext& request, const std::string& entryId);
std::unique_ptr<Response> handle_catalog_v2_categories(const RequestContext& request); std::unique_ptr<Response> handle_catalog_v2_categories(const RequestContext& request);
std::unique_ptr<Response> handle_no_js(const RequestContext& request);
std::unique_ptr<Response> handle_catalog_v2_languages(const RequestContext& request); std::unique_ptr<Response> handle_catalog_v2_languages(const RequestContext& request);
std::unique_ptr<Response> handle_catalog_v2_illustration(const RequestContext& request); std::unique_ptr<Response> handle_catalog_v2_illustration(const RequestContext& request);
std::unique_ptr<Response> handle_search(const RequestContext& request); std::unique_ptr<Response> handle_search(const RequestContext& request);
@ -155,6 +156,8 @@ class InternalServer {
std::string getLibraryId() const; std::string getLibraryId() const;
std::string getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const;
private: // types private: // types
class LockableSuggestionSearcher; class LockableSuggestionSearcher;
typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache; typedef ConcurrentCache<SearchInfo, std::shared_ptr<zim::Search>> SearchCache;

View File

@ -415,6 +415,17 @@ bool kiwix::startsWith(const std::string& base, const std::string& start)
&& std::equal(start.begin(), start.end(), base.begin()); && std::equal(start.begin(), start.end(), base.begin());
} }
std::string kiwix::stripSuffix(const std::string& str, const std::string& suffix)
{
if (str.size() > suffix.size()) {
const auto subStr = str.substr(str.size() - suffix.size(), str.size());
if (subStr == suffix) {
return str.substr(0, str.size() - suffix.size());
}
}
return str;
}
std::vector<std::string> kiwix::getTitleVariants(const std::string& title) { std::vector<std::string> kiwix::getTitleVariants(const std::string& title) {
std::vector<std::string> variants; std::vector<std::string> variants;
variants.push_back(title); variants.push_back(title);

View File

@ -93,6 +93,8 @@ std::string extractFromString(const std::string& str);
bool startsWith(const std::string& base, const std::string& start); bool startsWith(const std::string& base, const std::string& start);
std::string stripSuffix(const std::string& str, const std::string& suffix);
std::vector<std::string> getTitleVariants(const std::string& title); std::vector<std::string> getTitleVariants(const std::string& title);
} //namespace kiwix } //namespace kiwix
#endif #endif

View File

@ -37,6 +37,8 @@ templates/catalog_v2_categories.xml
templates/catalog_v2_languages.xml templates/catalog_v2_languages.xml
templates/url_of_search_results_css templates/url_of_search_results_css
templates/viewer_settings.js templates/viewer_settings.js
templates/no_js_library_page.html
templates/no_js_download.html
opensearchdescription.xml opensearchdescription.xml
ft_opensearchdescription.xml ft_opensearchdescription.xml
catalog_v2_searchdescription.xml catalog_v2_searchdescription.xml

View File

@ -28,7 +28,7 @@
, "random-page-button-text": "Go to a randomly selected page" , "random-page-button-text": "Go to a randomly selected page"
, "searchbox-tooltip": "Search '{{BOOK_TITLE}}'" , "searchbox-tooltip": "Search '{{BOOK_TITLE}}'"
, "confusion-of-tongues": "Two or more books in different languages would participate in search, which may lead to confusing results." , "confusion-of-tongues": "Two or more books in different languages would participate in search, which may lead to confusing results."
, "welcome-page-overzealous-filter": "No result. Would you like to <a href=\"#lang=\">reset filter</a>?" , "welcome-page-overzealous-filter": "No result. Would you like to <a href=\"{{URL}}\">reset filter</a>?"
, "powered-by-kiwix-html": "Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>" , "powered-by-kiwix-html": "Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a>"
, "search": "Search" , "search": "Search"
, "book-filtering-all-categories": "All categories" , "book-filtering-all-categories": "All categories"
@ -47,4 +47,8 @@
, "filter-by-tag": "Filter by tag \"{{TAG}}\"" , "filter-by-tag": "Filter by tag \"{{TAG}}\""
, "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\"" , "stop-filtering-by-tag": "Stop filtering by tag \"{{TAG}}\""
, "library-opds-feed-parameterised": "Library OPDS Feed - entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}" , "library-opds-feed-parameterised": "Library OPDS Feed - entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}"
, "welcome-to-kiwix-server": "Welcome to Kiwix Server"
, "download-links-heading": "Download links for <b><i>{{BOOK_TITLE}}</i></b>"
, "download-links-title": "Download book"
, "preview-book": "Preview"
} }

View File

@ -48,5 +48,9 @@
"filter-by-tag": "Hint for a link that would load results filtered by a single tag", "filter-by-tag": "Hint for a link that would load results filtered by a single tag",
"stop-filtering-by-tag": "Tooltip for the button that cancels filtering by tag", "stop-filtering-by-tag": "Tooltip for the button that cancels filtering by tag",
"library-opds-feed-all-entries": "Hint for the library OPDS feed for all entries", "library-opds-feed-all-entries": "Hint for the library OPDS feed for all entries",
"library-opds-feed-parameterised": "Hint for the library OPDS feed for filtered entries" "library-opds-feed-parameterised": "Hint for the library OPDS feed for filtered entries",
"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"
} }

View File

@ -17,7 +17,7 @@
, "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'" , "home-button-text": "[I18N TESTING] Jump to the main page of '{{BOOK_TITLE}}'"
, "random-page-button-text": "[I18N TESTING] I am tired of determinism" , "random-page-button-text": "[I18N TESTING] I am tired of determinism"
, "searchbox-tooltip": "[I18N TESTING] Let's search in '{{BOOK_TITLE}}'" , "searchbox-tooltip": "[I18N TESTING] Let's search in '{{BOOK_TITLE}}'"
, "welcome-page-overzealous-filter": "[I18N TESTING] Nothing found. <a href=\"?lang=\">Reset filter</a>" , "welcome-page-overzealous-filter": "[I18N TESTING] Nothing found. <a href=\"{{URL}}\">Reset filter</a>"
, "powered-by-kiwix-html": "[I18N TESTING] Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a> (nominal power: 1.23 kW)" , "powered-by-kiwix-html": "[I18N TESTING] Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a> (nominal power: 1.23 kW)"
, "search": "[I18N Search TESTING]" , "search": "[I18N Search TESTING]"
, "book-filtering-all-categories": "All [I18N TESTING] categories" , "book-filtering-all-categories": "All [I18N TESTING] categories"
@ -36,4 +36,8 @@
, "filter-by-tag": "Filter [I18N] by [TESTING] tag \"{{TAG}}\"" , "filter-by-tag": "Filter [I18N] by [TESTING] tag \"{{TAG}}\""
, "stop-filtering-by-tag": "[I18N] Stop filtering [TESTING] by tag \"{{TAG}}\"" , "stop-filtering-by-tag": "[I18N] Stop filtering [TESTING] by tag \"{{TAG}}\""
, "library-opds-feed-parameterised": "[I18N] Library OPDS Feed - [TESTING] entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}" , "library-opds-feed-parameterised": "[I18N] Library OPDS Feed - [TESTING] entries matching {{#LANG}}\nLanguage: {{LANG}} {{/LANG}}{{#CATEGORY}}\nCategory: {{CATEGORY}} {{/CATEGORY}}{{#TAG}}\nTag: {{TAG}} {{/TAG}}{{#Q}}\nQuery: {{Q}} {{/Q}}"
, "welcome-to-kiwix-server": "[I18N] Welcome to Kiwix Server [TESTING]"
, "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]"
} }

View File

@ -315,7 +315,7 @@
const kiwixHomeBody = document.querySelector('.kiwixHomeBody'); const kiwixHomeBody = document.querySelector('.kiwixHomeBody');
const divTag = document.createElement('div'); const divTag = document.createElement('div');
divTag.setAttribute('class', 'noResults'); divTag.setAttribute('class', 'noResults');
divTag.innerHTML = $t("welcome-page-overzealous-filter"); divTag.innerHTML = $t("welcome-page-overzealous-filter", {URL: '#lang='});
kiwixHomeBody.append(divTag); kiwixHomeBody.append(divTag);
kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center'); kiwixHomeBody.setAttribute('style', 'display: flex; justify-content: center; align-items: center');
loader.setAttribute('style', 'position: absolute; top: 50%'); loader.setAttribute('style', 'position: absolute; top: 50%');

View File

@ -44,6 +44,29 @@
<script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script> <script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script>
</head> </head>
<body> <body>
<noscript>
<style>
.kiwixNav, .kiwixHomeBody, #feedLink, .kiwixfooter {
display: none;
}
html, body {
height: 100%;
}
.noScriptLinkContainer {
position: absolute;
top: 50%;
left: 50%;
-moz-transform: translateX(-50%) translateY(-50%);
-webkit-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
font-size: 16px;
font-family: roboto;
}
</style>
<div class="noScriptLinkContainer">
<span id="noScriptLinkText">This page cannot be accessed if JavaScript is not enabled. Please head over to <a href="{{root}}/nojs">nojs endpoint.</a></span>
</div>
</noscript>
<div class='kiwixNav'> <div class='kiwixNav'>
<a href="{{root}}/catalog/v2/entries" id="feedLink"> <a href="{{root}}/catalog/v2/entries" id="feedLink">
<img src="{{root}}/skin/feed.svg?KIWIXCACHEID" <img src="{{root}}/skin/feed.svg?KIWIXCACHEID"

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{translations.download-links-title}}</title>
</head>
<style>
.downloadLinksTitle {
text-align: center;
font-size: 32px;
margin-bottom: 8px;
}
</style>
<body>
<div class="downloadLinksTitle">
{{{translations.download-links-heading}}}
</div>
<a href="{{url}}" download>
<div>{{translations.direct-download-link-text}}</div>
</a>
<a href="{{url}}.sha256" download>
<div>{{translations.hash-download-link-text}}</div>
</a>
<a href="{{url}}.magnet" target="_blank">
<div>{{translations.magnet-link-text}}</div>
</a>
<a href="{{url}}.torrent" download>
<div>{{translations.torrent-download-link-text}}</div>
</a>
</body>
</html>

View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link type="root" href="{{root}}">
<title>{{translations.welcome-to-kiwix-server}}</title>
<link
type="text/css"
href="{{root}}/skin/index.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<link rel="apple-touch-icon" sizes="180x180" href="{{root}}/skin/favicon/apple-touch-icon.png?KIWIXCACHEID">
<link rel="icon" type="image/png" sizes="32x32" href="{{root}}/skin/favicon/favicon-32x32.png?KIWIXCACHEID">
<link rel="icon" type="image/png" sizes="16x16" href="{{root}}/skin/favicon/favicon-16x16.png?KIWIXCACHEID">
<link rel="manifest" href="{{root}}/skin/favicon/site.webmanifest?KIWIXCACHEID">
<link rel="mask-icon" href="{{root}}/skin/favicon/safari-pinned-tab.svg?KIWIXCACHEID" color="#5bbad5">
<link rel="shortcut icon" href="{{root}}/skin/favicon/favicon.ico?KIWIXCACHEID">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{{root}}/skin/favicon/browserconfig.xml?KIWIXCACHEID">
<meta name="theme-color" content="#ffffff">
<style>
@font-face {
font-family: "poppins";
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
}
@font-face {
font-family: "roboto";
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
}
.book__list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.book__wrapper:hover {
transform: scale(1.0);
}
.tag__link {
pointer-events: none;
}
.book__link__wrapper {
grid-column: 1 / 3;
grid-row: 1 / 3;
}
.book__link {
grid-row: 2 / 3;
}
.kiwixHomeBody__results {
flex-basis: 100%;
}
#book__title>a, .book__download a {
text-decoration: none;
all: unset;
}
</style>
</head>
<body>
<div class='kiwixNav'>
<div class="kiwixNav__filters">
<div class="kiwixNav__select">
<select name="lang" id="languageFilter" class='kiwixNav__kiwixFilter filter' form="kiwixSearchForm">
<option value="" selected>{{translations.book-filtering-all-languages}}</option>
{{#languages}}
<option value="{{lang_code}}"{{#selected}} selected {{/selected}}>{{lang_self_name}}</option>
{{/languages}}
</select>
</div>
<div class="kiwixNav__select">
<select name="category" id="categoryFilter" class='kiwixNav__kiwixFilter filter' form="kiwixSearchForm">
<option value="">{{translations.book-filtering-all-categories}}</option>
{{#categories}}
<option value="{{name}}"{{#selected}} selected {{/selected}}>{{hf_name}}</option>
{{/categories}}
</select>
</div>
</div>
<form id='kiwixSearchForm' class='kiwixNav__SearchForm' action="{{root}}/nojs">
<input type="text" name="q" placeholder="{{translations.search}}" id="searchFilter" class='kiwixSearch filter' value="{{searchQuery}}">
<input type="submit" class="kiwixButton kiwixButtonHover" value="{{translations.search}}"/>
</form>
</div>
<div class="kiwixHomeBody">
{{#noResults}}
<style>
.book__list {
display: none;
}
.kiwixHomeBody {
justify-content: center;
}
.noResults {
font-size: 16px;
font-family: roboto;
}
</style>
<div class="noResults">
{{{translations.welcome-page-overzealous-filter}}}
</div>
</style>
{{/noResults}}
<div class="book__list">
<h3 class="kiwixHomeBody__results">{{translations.count-of-matching-books}}</h3>
{{#books}}
<div class="book__wrapper">
<div class="book__link__wrapper">
<div class="book__icon" {{faviconAttr}}></div>
<div class="book__header">
<div id="book__title"><a href="{{root}}/content/{{id}}">{{title}}</a></div>
{{#downloadAvailable}}
<div class="book__download"><span><a href="{{root}}/nojs/download/{{id}}">{{translations.download}}</a></span></div>
{{/downloadAvailable}}
</div>
<a class="book__link" href="{{root}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">
<div class="book__description" title="{{description}}">{{description}}</div>
</a>
</div>
<div class="book__languageTag" {{languageAttr}}>{{langCode}}</div>
<div class="book__tags"><div class="book__tags--wrapper">
{{#tagList}}
<span class="tag__link" aria-label='{{tag}}' title='{{tag}}'>{{tag}}</span>
{{/tagList}}
</div>
</div>
</div>
{{/books}}
</div>
</div>
<div id="kiwixfooter" class="kiwixfooter">{{{translations.powered-by-kiwix-html}}}</div>
</body>
</html>

View File

@ -945,6 +945,259 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
EXPECT_EQ(r1->status, 404); EXPECT_EQ(r1->status, 404);
} }
#define HTML_PREAMBLE \
"<!DOCTYPE html>\n" \
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" \
" <head>\n" \
" <meta charset=\"UTF-8\" />\n" \
" <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n" \
" <link type=\"root\" href=\"/ROOT%23%3F\">\n" \
" <title>Welcome to Kiwix Server</title>\n" \
" <link\n" \
" type=\"text/css\"\n" \
" href=\"/ROOT%23%3F/skin/index.css?cacheid=e4d76d16\"\n" \
" rel=\"Stylesheet\"\n" \
" />\n" \
" <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/ROOT%23%3F/skin/favicon/apple-touch-icon.png?cacheid=f86f8df3\">\n" \
" <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/ROOT%23%3F/skin/favicon/favicon-32x32.png?cacheid=79ded625\">\n" \
" <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/ROOT%23%3F/skin/favicon/favicon-16x16.png?cacheid=a986fedc\">\n" \
" <link rel=\"manifest\" href=\"/ROOT%23%3F/skin/favicon/site.webmanifest?cacheid=bc396efb\">\n" \
" <link rel=\"mask-icon\" href=\"/ROOT%23%3F/skin/favicon/safari-pinned-tab.svg?cacheid=8d487e95\" color=\"#5bbad5\">\n" \
" <link rel=\"shortcut icon\" href=\"/ROOT%23%3F/skin/favicon/favicon.ico?cacheid=92663314\">\n" \
" <meta name=\"msapplication-TileColor\" content=\"#da532c\">\n" \
" <meta name=\"msapplication-config\" content=\"/ROOT%23%3F/skin/favicon/browserconfig.xml?cacheid=f29a7c4a\">\n" \
" <meta name=\"theme-color\" content=\"#ffffff\">\n" \
" <style>\n" \
" @font-face {\n" \
" font-family: \"poppins\";\n" \
" src: url(\"/ROOT%23%3F/skin/fonts/Poppins.ttf?cacheid=af705837\") format(\"truetype\");\n" \
" }\n\n" \
" @font-face {\n" \
" font-family: \"roboto\";\n" \
" src: url(\"/ROOT%23%3F/skin/fonts/Roboto.ttf?cacheid=84d10248\") format(\"truetype\");\n" \
" }\n\n" \
" .book__list {\n" \
" display: flex;\n" \
" flex-direction: row;\n" \
" flex-wrap: wrap;\n" \
" align-items: center;\n" \
" }\n\n" \
" .book__wrapper:hover {\n" \
" transform: scale(1.0);\n" \
" }\n\n" \
" .tag__link {\n" \
" pointer-events: none;\n" \
" }\n\n" \
" .book__link__wrapper {\n" \
" grid-column: 1 / 3;\n" \
" grid-row: 1 / 3;\n" \
" }\n\n" \
" .book__link {\n" \
" grid-row: 2 / 3;\n" \
" }\n\n" \
" .kiwixHomeBody__results {\n" \
" flex-basis: 100%;\n" \
" }\n\n" \
" #book__title>a, .book__download a {\n" \
" text-decoration: none;\n" \
" all: unset;\n" \
" }\n" \
" </style>\n" \
" </head>\n" \
" <body>\n" \
" <div class='kiwixNav'>\n"
#define CHARLES_RAY_BOOK_HTML \
" <div class=\"book__wrapper\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/charlesray/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile%26other\">Charles, Ray</a></div>\n" \
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile%26other\">Download</a></span></div>\n" \
" </div>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile%26other\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles\">Wikipedia articles about Ray Charles</div>\n" \
" </a>\n" \
" </div>\n" \
" <div class=\"book__languageTag\" >fra</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='unittest' title='unittest'>unittest</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n"
#define RAY_CHARLES_BOOK_HTML \
" <div class=\"book__wrapper\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile\">Ray Charles</a></div>\n" \
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile\">Download</a></span></div>\n" \
" </div>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__description\" title=\"Wikipedia articles about Ray Charles\">Wikipedia articles about Ray Charles</div>\n" \
" </a>\n" \
" </div>\n" \
" <div class=\"book__languageTag\" >eng</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='public_tag_without_a_value' title='public_tag_without_a_value'>public_tag_without_a_value</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n"
#define RAY_CHARLES_UNCTZ_BOOK_HTML \
" <div class=\"book__wrapper\">\n" \
" <div class=\"book__link__wrapper\">\n" \
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/raycharles_uncategorized/?size=48)></div>\n" \
" <div class=\"book__header\">\n" \
" <div id=\"book__title\"><a href=\"/ROOT%23%3F/content/zimfile\">Ray (uncategorized) Charles</a></div>\n" \
" <div class=\"book__download\"><span><a href=\"/ROOT%23%3F/nojs/download/zimfile\">Download</a></span></div>\n" \
" </div>\n" \
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/zimfile\" title=\"Preview\" aria-label=\"Preview\">\n" \
" <div class=\"book__description\" title=\"No category is assigned to this library entry.\">No category is assigned to this library entry.</div>\n" \
" </a>\n" \
" </div>\n" \
" <div class=\"book__languageTag\" >rus,eng</div>\n" \
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
" <span class=\"tag__link\" aria-label='public_tag_with_a_value:value_of_a_public_tag' title='public_tag_with_a_value:value_of_a_public_tag'>public_tag_with_a_value:value_of_a_public_tag</span>\n" \
" <span class=\"tag__link\" aria-label='wikipedia' title='wikipedia'>wikipedia</span>\n" \
" </div>\n" \
" </div>\n" \
" </div>\n"
#define FINAL_HTML_TEXT \
" </div>\n" \
" </div>\n" \
" <div id=\"kiwixfooter\" class=\"kiwixfooter\">Powered by&nbsp;<a href=\"https://kiwix.org\">Kiwix</a></div>\n" \
" </body>\n" \
"</html>"
#define FILTERS_HTML(SELECTED_ENG) \
" <div class=\"kiwixNav__filters\">\n" \
" <div class=\"kiwixNav__select\">\n" \
" <select name=\"lang\" id=\"languageFilter\" class='kiwixNav__kiwixFilter filter' form=\"kiwixSearchForm\">\n" \
" <option value=\"\" selected>All languages</option>\n" \
" <option value=\"eng\"" SELECTED_ENG ">English</option>\n" \
" <option value=\"fra\">français</option>\n" \
" <option value=\"rus\">русский</option>\n" \
" </select>\n" \
" </div>\n" \
" <div class=\"kiwixNav__select\">\n" \
" <select name=\"category\" id=\"categoryFilter\" class='kiwixNav__kiwixFilter filter' form=\"kiwixSearchForm\">\n" \
" <option value=\"\">All categories</option>\n" \
" <option value=\"jazz\">Jazz</option>\n" \
" <option value=\"wikipedia\">Wikipedia</option>\n" \
" </select>\n" \
" </div>\n" \
" </div>\n" \
" <form id='kiwixSearchForm' class='kiwixNav__SearchForm' action=\"/ROOT%23%3F/nojs\">\n" \
" <input type=\"text\" name=\"q\" placeholder=\"Search\" id=\"searchFilter\" class='kiwixSearch filter' value=\"\">\n" \
" <input type=\"submit\" class=\"kiwixButton kiwixButtonHover\" value=\"Search\"/>\n" \
" </form>\n" \
" </div>\n"
#define HOME_BODY_TEXT(X) \
" <div class=\"kiwixHomeBody\">\n" \
" \n" \
" <div class=\"book__list\">\n" \
" <h3 class=\"kiwixHomeBody__results\">" X " book(s)</h3>\n"
#define HOME_BODY_0_RESULTS \
" <div class=\"kiwixHomeBody\">\n" \
" <style>\n" \
" .book__list {\n" \
" display: none;\n" \
" }\n" \
" .kiwixHomeBody {\n" \
" justify-content: center;\n" \
" }\n" \
" .noResults {\n" \
" font-size: 16px;\n" \
" font-family: roboto;\n" \
" }\n" \
" </style>\n" \
" <div class=\"noResults\">\n" \
" No result. Would you like to <a href=\"?lang=\">reset filter</a>?\n" \
" </div>\n" \
" </style>\n" \
" <div class=\"book__list\">\n" \
" <h3 class=\"kiwixHomeBody__results\">0 book(s)</h3>\n" \
" \n"
#define RAY_CHARLES_UNCTZ_DOWNLOAD \
"<!DOCTYPE html>\n" \
"<html lang=\"en\">\n" \
"<head>\n" \
" <meta charset=\"UTF-8\">\n" \
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n" \
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" \
" <title>Download book</title>\n" \
"</head>\n" \
"<style>\n" \
" .downloadLinksTitle {\n" \
" text-align: center;\n" \
" font-size: 32px;\n" \
" margin-bottom: 8px;\n" \
" }\n" \
"</style>\n" \
"<body>\n" \
" <div class=\"downloadLinksTitle\">\n" \
" Download links for <b><i>Ray (uncategorized) Charles</i></b>\n" \
" </div>\n" \
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim\" download>\n" \
" <div>Direct</div>\n" \
" </a>\n" \
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim.sha256\" download>\n" \
" <div>Sha256 hash</div>\n" \
" </a>\n" \
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim.magnet\" target=\"_blank\">\n" \
" <div>Magnet link</div>\n" \
" </a>\n" \
" <a href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/zimfile.zim.torrent\" download>\n" \
" <div>Torrent file</div>\n" \
" </a>\n" \
"</body>\n" \
"</html>"
TEST_F(LibraryServerTest, noJS) {
// no_js_default
auto r = zfs1_->GET("/ROOT%23%3F/nojs");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body,
HTML_PREAMBLE
FILTERS_HTML("")
HOME_BODY_TEXT("3")
CHARLES_RAY_BOOK_HTML
RAY_CHARLES_BOOK_HTML
RAY_CHARLES_UNCTZ_BOOK_HTML
FINAL_HTML_TEXT);
// no_js_eng_lang
r = zfs1_->GET("/ROOT%23%3F/nojs?lang=eng");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body,
HTML_PREAMBLE
FILTERS_HTML(" selected ")
HOME_BODY_TEXT("2")
RAY_CHARLES_UNCTZ_BOOK_HTML
RAY_CHARLES_BOOK_HTML
FINAL_HTML_TEXT);
// no_js_no_books
r = zfs1_->GET("/ROOT%23%3F/nojs?lang=fas");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body,
HTML_PREAMBLE
FILTERS_HTML("")
HOME_BODY_0_RESULTS
FINAL_HTML_TEXT);
// no_js_download
r = zfs1_->GET("/ROOT%23%3F/nojs/download/zimfile");
EXPECT_EQ(r->status, 200);
EXPECT_EQ(r->body, RAY_CHARLES_UNCTZ_DOWNLOAD);
}
#undef EXPECT_SEARCH_RESULTS #undef EXPECT_SEARCH_RESULTS

View File

@ -63,7 +63,7 @@ const ResourceCollection resources200Compressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=e4d76d16" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=e4d76d16" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=78cfd6a2" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=d38d9ef1" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" },
{ STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" }, { STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" },
@ -97,6 +97,8 @@ const ResourceCollection resources200Compressible{
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/content/A/index" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/content/A/index" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/content/A/Ray_Charles" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/content/A/Ray_Charles" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/nojs"},
}; };
const ResourceCollection resources200Uncompressible{ const ResourceCollection resources200Uncompressible{
@ -179,6 +181,8 @@ const ResourceCollection resources200Uncompressible{
{ DYNAMIC_CONTENT, "/ROOT%23%3F/suggest?content=zimfile" }, { DYNAMIC_CONTENT, "/ROOT%23%3F/suggest?content=zimfile" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Creator" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Creator" },
{ ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" }, { ZIM_CONTENT, "/ROOT%23%3F/raw/zimfile/meta/Title" },
{ DYNAMIC_CONTENT, "/ROOT%23%3F/nojs/download/zimfile"},
}; };
ResourceCollection all200Resources() ResourceCollection all200Resources()
@ -284,7 +288,7 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/index.css?cacheid=e4d76d16"
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=b00b12db" defer></script> <script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=b00b12db" defer></script>
<script src="/ROOT%23%3F/skin/isotope.pkgd.min.js?cacheid=2e48d392" 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 src="/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=78cfd6a2" defer></script> <script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=d38d9ef1" defer></script>
<img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f" <img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f"
<img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961" <img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961"
)EXPECTEDRESULT" )EXPECTEDRESULT"
@ -395,6 +399,8 @@ const char* urls404[] = {
// zimfile has no Favicon nor Illustration_48x48@1 meta item // zimfile has no Favicon nor Illustration_48x48@1 meta item
"/ROOT%23%3F/raw/zimfile/meta/Favicon", "/ROOT%23%3F/raw/zimfile/meta/Favicon",
"/ROOT%23%3F/raw/zimfile/meta/Illustration_48x48@1", "/ROOT%23%3F/raw/zimfile/meta/Illustration_48x48@1",
"/ROOT%23%3F/nojs/download/thiszimdoesntexist"
}; };
TEST_F(ServerTest, 404) TEST_F(ServerTest, 404)

View File

@ -163,4 +163,11 @@ TEST(stringTools, urlDecode)
EXPECT_EQ(urlDecode(encodedUriDelimSymbols, false), encodedUriDelimSymbols); EXPECT_EQ(urlDecode(encodedUriDelimSymbols, false), encodedUriDelimSymbols);
} }
TEST(stringTools, stripSuffix)
{
EXPECT_EQ(stripSuffix("abc123", "123"), "abc");
EXPECT_EQ(stripSuffix("abc123", "123456789"), "abc123");
EXPECT_EQ(stripSuffix("abc123", "987"), "abc123");
}
}; };