mirror of
https://github.com/kiwix/libkiwix.git
synced 2025-09-23 12:18:47 -04:00
Merge pull request #1219 from kiwix/catalog_only_mode
Support for catalog only mode of kiwix-serve
This commit is contained in:
commit
2b4b90f8a3
@ -50,7 +50,7 @@ class HumanReadableNameMapper : public NameMapper {
|
||||
std::map<std::string, std::string> m_nameToId;
|
||||
|
||||
public:
|
||||
HumanReadableNameMapper(kiwix::Library& library, bool withAlias);
|
||||
HumanReadableNameMapper(const kiwix::Library& library, bool withAlias);
|
||||
virtual ~HumanReadableNameMapper() = default;
|
||||
virtual std::string getNameForId(const std::string& id) const;
|
||||
virtual std::string getIdForName(const std::string& name) const;
|
||||
|
@ -63,6 +63,8 @@ namespace kiwix
|
||||
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
|
||||
void setBlockExternalLinks(bool blockExternalLinks)
|
||||
{ m_blockExternalLinks = blockExternalLinks; }
|
||||
void setCatalogOnlyMode(bool enable) { m_catalogOnlyMode = enable; }
|
||||
void setContentServerUrl(std::string url) { m_contentServerUrl = url; }
|
||||
void setIpMode(IpMode mode) { m_ipMode = mode; }
|
||||
int getPort() const;
|
||||
IpAddress getAddress() const;
|
||||
@ -83,6 +85,8 @@ namespace kiwix
|
||||
bool m_blockExternalLinks = false;
|
||||
IpMode m_ipMode = IpMode::AUTO;
|
||||
int m_ipConnectionLimit = 0;
|
||||
bool m_catalogOnlyMode = false;
|
||||
std::string m_contentServerUrl;
|
||||
std::unique_ptr<InternalServer> mp_server;
|
||||
};
|
||||
}
|
||||
|
@ -130,6 +130,7 @@ std::string HTMLDumper::dumpPlainHTML(kiwix::Filter filter) const
|
||||
RESOURCE::templates::no_js_library_page_html,
|
||||
kainjow::mustache::object{
|
||||
{"root", rootLocation},
|
||||
{"contentServerUrl", onlyAsNonEmptyMustacheValue(contentServerUrl)},
|
||||
{"books", booksData },
|
||||
{"searchQuery", searchQuery},
|
||||
{"languages", languages},
|
||||
|
@ -50,6 +50,13 @@ class LibraryDumper
|
||||
*/
|
||||
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
|
||||
|
||||
/**
|
||||
* Set the URL of the content server.
|
||||
*
|
||||
* @param url the URL of the content server to use.
|
||||
*/
|
||||
void setContentServerUrl(const std::string& url) { this->contentServerUrl = url; }
|
||||
|
||||
/**
|
||||
* Set some informations about the search results.
|
||||
*
|
||||
@ -81,6 +88,7 @@ class LibraryDumper
|
||||
const kiwix::NameMapper* const nameMapper;
|
||||
std::string libraryId;
|
||||
std::string rootLocation;
|
||||
std::string contentServerUrl;
|
||||
std::string m_userLang;
|
||||
int m_totalResults;
|
||||
int m_startIndex;
|
||||
|
@ -24,8 +24,8 @@
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
HumanReadableNameMapper::HumanReadableNameMapper(kiwix::Library& library, bool withAlias) {
|
||||
for (auto& bookId: library.filter(kiwix::Filter().local(true).valid(true))) {
|
||||
HumanReadableNameMapper::HumanReadableNameMapper(const kiwix::Library& library, bool withAlias) {
|
||||
for (auto& bookId: library.filter(kiwix::Filter())) {
|
||||
auto& currentBook = library.getBookById(bookId);
|
||||
auto bookName = currentBook.getHumanReadableIdFromPath();
|
||||
m_idToName[bookId] = bookName;
|
||||
|
@ -51,24 +51,26 @@ typedef kainjow::mustache::list IllustrationInfo;
|
||||
IllustrationInfo getBookIllustrationInfo(const Book& book)
|
||||
{
|
||||
kainjow::mustache::list illustrations;
|
||||
if ( book.isPathValid() ) {
|
||||
for ( const auto& illustration : book.getIllustrations() ) {
|
||||
// For now, we are handling only sizexsize@1 illustration.
|
||||
// So we can simply pass one size to mustache.
|
||||
illustrations.push_back(kainjow::mustache::object{
|
||||
{"icon_size", to_string(illustration->width)},
|
||||
{"icon_mimetype", illustration->mimeType}
|
||||
});
|
||||
}
|
||||
for ( const auto& illustration : book.getIllustrations() ) {
|
||||
// For now, we are handling only sizexsize@1 illustration.
|
||||
// So we can simply pass one size to mustache.
|
||||
illustrations.push_back(kainjow::mustache::object{
|
||||
{"icon_size", to_string(illustration->width)},
|
||||
{"icon_mimetype", illustration->mimeType}
|
||||
});
|
||||
}
|
||||
return illustrations;
|
||||
}
|
||||
|
||||
std::string fullEntryXML(const Book& book, const std::string& rootLocation, const std::string& contentId)
|
||||
std::string fullEntryXML(const Book& book,
|
||||
const std::string& rootLocation,
|
||||
const std::string& contentServerUrl,
|
||||
const std::string& contentId)
|
||||
{
|
||||
const auto bookDate = book.getDate() + "T00:00:00Z";
|
||||
const kainjow::mustache::object data{
|
||||
{"root", rootLocation},
|
||||
{"contentServerUrl", onlyAsNonEmptyMustacheValue(contentServerUrl)},
|
||||
{"id", book.getId()},
|
||||
{"name", book.getName()},
|
||||
{"title", book.getTitle()},
|
||||
@ -105,7 +107,12 @@ std::string partialEntryXML(const Book& book, const std::string& rootLocation)
|
||||
return render_template(xmlTemplate, data);
|
||||
}
|
||||
|
||||
BooksData getBooksData(const Library* library, const NameMapper* nameMapper, const std::vector<std::string>& bookIds, const std::string& rootLocation, bool partial)
|
||||
BooksData getBooksData(const Library* library,
|
||||
const NameMapper* nameMapper,
|
||||
const std::vector<std::string>& bookIds,
|
||||
const std::string& rootLocation,
|
||||
const std::string& contentServerUrl,
|
||||
bool partial)
|
||||
{
|
||||
BooksData booksData;
|
||||
for ( const auto& bookId : bookIds ) {
|
||||
@ -114,7 +121,7 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
|
||||
const std::string contentId = nameMapper->getNameForId(bookId);
|
||||
const auto entryXML = partial
|
||||
? partialEntryXML(book, rootLocation)
|
||||
: fullEntryXML(book, rootLocation, contentId);
|
||||
: fullEntryXML(book, rootLocation, contentServerUrl, contentId);
|
||||
booksData.push_back(kainjow::mustache::object{ {"entry", entryXML} });
|
||||
} catch ( const std::out_of_range& ) {
|
||||
// the book was removed from the library since its id was obtained
|
||||
@ -129,7 +136,7 @@ BooksData getBooksData(const Library* library, const NameMapper* nameMapper, con
|
||||
|
||||
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
|
||||
{
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, false);
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentServerUrl, false);
|
||||
const kainjow::mustache::object template_data{
|
||||
{"date", gen_date_str()},
|
||||
{"root", rootLocation},
|
||||
@ -147,7 +154,7 @@ string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const s
|
||||
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query, bool partial) const
|
||||
{
|
||||
const auto endpointRoot = rootLocation + "/catalog/v2";
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, partial);
|
||||
const auto booksData = getBooksData(library, nameMapper, bookIds, rootLocation, contentServerUrl, partial);
|
||||
|
||||
const char* const endpoint = partial ? "/partial_entries" : "/entries";
|
||||
const std::string url = endpoint + (query.empty() ? "" : "?" + query);
|
||||
@ -172,7 +179,7 @@ std::string OPDSDumper::dumpOPDSCompleteEntry(const std::string& bookId) const
|
||||
const std::string contentId = nameMapper->getNameForId(bookId);
|
||||
return XML_HEADER
|
||||
+ "\n"
|
||||
+ fullEntryXML(book, rootLocation, contentId);
|
||||
+ fullEntryXML(book, rootLocation, contentServerUrl, contentId);
|
||||
}
|
||||
|
||||
std::string OPDSDumper::categoriesOPDSFeed() const
|
||||
|
@ -53,7 +53,9 @@ bool Server::start() {
|
||||
m_blockExternalLinks,
|
||||
m_ipMode,
|
||||
m_indexTemplateString,
|
||||
m_ipConnectionLimit));
|
||||
m_ipConnectionLimit,
|
||||
m_catalogOnlyMode,
|
||||
m_contentServerUrl));
|
||||
return mp_server->start();
|
||||
}
|
||||
|
||||
|
@ -125,9 +125,12 @@ std::string getSearchComponent(const RequestContext& request)
|
||||
return query.empty() ? query : "?" + query;
|
||||
}
|
||||
|
||||
Filter get_search_filter(const RequestContext& request, const std::string& prefix="")
|
||||
Filter get_search_filter(const RequestContext& request, const std::string& prefix="", bool catalogOnlyMode = false)
|
||||
{
|
||||
auto filter = kiwix::Filter().valid(true).local(true);
|
||||
auto filter = kiwix::Filter();
|
||||
if ( !catalogOnlyMode ) {
|
||||
filter.valid(true).local(true);
|
||||
}
|
||||
try {
|
||||
filter.query(request.get_argument(prefix+"q"));
|
||||
} catch (const std::out_of_range&) {}
|
||||
@ -432,7 +435,9 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
bool blockExternalLinks,
|
||||
IpMode ipMode,
|
||||
std::string indexTemplateString,
|
||||
int ipConnectionLimit) :
|
||||
int ipConnectionLimit,
|
||||
bool catalogOnlyMode,
|
||||
std::string contentServerUrl) :
|
||||
m_addr(addr),
|
||||
m_port(port),
|
||||
m_root(normalizeRootUrl(root)),
|
||||
@ -451,7 +456,9 @@ InternalServer::InternalServer(LibraryPtr library,
|
||||
mp_nameMapper(nameMapper ? nameMapper : std::shared_ptr<NameMapper>(&defaultNameMapper, NoDelete())),
|
||||
searchCache(getEnvVar<int>("KIWIX_SEARCH_CACHE_SIZE", DEFAULT_CACHE_SIZE)),
|
||||
suggestionSearcherCache(getEnvVar<int>("KIWIX_SUGGESTION_SEARCHER_CACHE_SIZE", std::max((unsigned int) (mp_library->getBookCount(true, true)*0.1), 1U))),
|
||||
m_customizedResources(new CustomizedResources)
|
||||
m_customizedResources(new CustomizedResources),
|
||||
m_catalogOnlyMode(catalogOnlyMode),
|
||||
m_contentServerUrl(contentServerUrl)
|
||||
{
|
||||
m_root = urlEncode(m_root);
|
||||
}
|
||||
@ -850,12 +857,17 @@ std::unique_ptr<Response> InternalServer::handle_no_js(const RequestContext& req
|
||||
HTMLDumper htmlDumper(mp_library.get(), mp_nameMapper.get());
|
||||
htmlDumper.setRootLocation(m_root);
|
||||
htmlDumper.setLibraryId(getLibraryId());
|
||||
if ( !m_contentServerUrl.empty() ) {
|
||||
htmlDumper.setContentServerUrl(m_contentServerUrl);
|
||||
} else if ( !m_catalogOnlyMode ) {
|
||||
htmlDumper.setContentServerUrl(m_root);
|
||||
}
|
||||
auto userLang = request.get_user_language();
|
||||
htmlDumper.setUserLanguage(userLang);
|
||||
std::string content;
|
||||
|
||||
if (urlParts.size() == 1) {
|
||||
auto filter = get_search_filter(request);
|
||||
auto filter = get_search_filter(request, "", m_catalogOnlyMode);
|
||||
try {
|
||||
if (request.get_argument("category") == "") {
|
||||
filter.clearCategory();
|
||||
@ -1103,7 +1115,7 @@ std::vector<std::string>
|
||||
InternalServer::search_catalog(const RequestContext& request,
|
||||
kiwix::OPDSDumper& opdsDumper)
|
||||
{
|
||||
const auto filter = get_search_filter(request);
|
||||
const auto filter = get_search_filter(request, "", m_catalogOnlyMode);
|
||||
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
|
||||
const auto totalResults = bookIdsToDump.size();
|
||||
const long count = request.get_optional_param("count", 10L);
|
||||
|
@ -106,7 +106,9 @@ class InternalServer {
|
||||
bool blockExternalLinks,
|
||||
IpMode ipMode,
|
||||
std::string indexTemplateString,
|
||||
int ipConnectionLimit);
|
||||
int ipConnectionLimit,
|
||||
bool catalogOnlyMode,
|
||||
std::string zimViewerURL);
|
||||
virtual ~InternalServer();
|
||||
|
||||
MHD_Result handlerCallback(struct MHD_Connection* connection,
|
||||
@ -160,6 +162,7 @@ class InternalServer {
|
||||
std::string getLibraryId() const;
|
||||
|
||||
std::string getNoJSDownloadPageHTML(const std::string& bookId, const std::string& userLang) const;
|
||||
OPDSDumper getOPDSDumper() const;
|
||||
|
||||
private: // types
|
||||
class LockableSuggestionSearcher;
|
||||
@ -192,6 +195,9 @@ class InternalServer {
|
||||
|
||||
class CustomizedResources;
|
||||
std::unique_ptr<CustomizedResources> m_customizedResources;
|
||||
|
||||
const bool m_catalogOnlyMode;
|
||||
const std::string m_contentServerUrl;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -51,6 +51,19 @@ const std::string opdsMimeType[] = {
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
OPDSDumper InternalServer::getOPDSDumper() const
|
||||
{
|
||||
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
if ( !m_contentServerUrl.empty() ) {
|
||||
opdsDumper.setContentServerUrl(m_contentServerUrl);
|
||||
} else if ( !m_catalogOnlyMode ) {
|
||||
opdsDumper.setContentServerUrl(m_root);
|
||||
}
|
||||
return opdsDumper;
|
||||
}
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& request)
|
||||
{
|
||||
if (m_verbose.load()) {
|
||||
@ -80,9 +93,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||
}
|
||||
|
||||
zim::Uuid uuid;
|
||||
kiwix::OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
std::vector<std::string> bookIdsToDump;
|
||||
if (url == "root.xml") {
|
||||
uuid = zim::Uuid::generate(host);
|
||||
@ -158,9 +169,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestCo
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request, bool partial)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
const auto bookIds = search_catalog(request, opdsDumper);
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query(), partial);
|
||||
return ContentResponse::build(
|
||||
@ -177,9 +186,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
return UrlNotFoundResponse(request);
|
||||
}
|
||||
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
const auto opdsFeed = opdsDumper.dumpOPDSCompleteEntry(entryId);
|
||||
return ContentResponse::build(
|
||||
opdsFeed,
|
||||
@ -189,9 +196,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_complete_entry(const
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
return ContentResponse::build(
|
||||
opdsDumper.categoriesOPDSFeed(),
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
@ -200,9 +205,7 @@ std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const Req
|
||||
|
||||
std::unique_ptr<Response> InternalServer::handle_catalog_v2_languages(const RequestContext& request)
|
||||
{
|
||||
OPDSDumper opdsDumper(mp_library.get(), mp_nameMapper.get());
|
||||
opdsDumper.setRootLocation(m_root);
|
||||
opdsDumper.setLibraryId(getLibraryId());
|
||||
kiwix::OPDSDumper opdsDumper = getOPDSDumper();
|
||||
return ContentResponse::build(
|
||||
opdsDumper.languagesOPDSFeed(),
|
||||
opdsMimeType[OPDS_NAVIGATION_FEED]
|
||||
|
@ -152,8 +152,21 @@
|
||||
: '';
|
||||
}
|
||||
|
||||
function addBookPreviewLink(html, bookXml) {
|
||||
const bookContentLink = bookXml.querySelector('link[type="text/html"]');
|
||||
if ( !bookContentLink )
|
||||
return html;
|
||||
|
||||
const urlComponents = bookContentLink.getAttribute('href').split('/');
|
||||
// bookContentLink URL = ROOT_URL/content/BOOK_NAME
|
||||
const bookName = urlComponents.pop();
|
||||
urlComponents.pop(); // drop 'content' component
|
||||
const viewerLink = urlComponents.join('/') + `/viewer#${bookName}`;
|
||||
|
||||
return `<a class="book__link" href="${viewerLink}" data-hover="Preview">${html}</a>`;
|
||||
}
|
||||
|
||||
function generateBookHtml(book, sort = false) {
|
||||
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
|
||||
let iconUrl;
|
||||
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
|
||||
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
|
||||
@ -183,9 +196,6 @@
|
||||
} catch {
|
||||
downloadLink = '';
|
||||
}
|
||||
const bookName = link.split('/').pop();
|
||||
const viewerLink = `${root}/viewer#${bookName}`;
|
||||
|
||||
const humanFriendlyZimSize = humanFriendlySize(zimSize);
|
||||
|
||||
const divTag = document.createElement('div');
|
||||
@ -197,9 +207,7 @@
|
||||
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
|
||||
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
|
||||
|
||||
divTag.innerHTML = `
|
||||
<div class="book__wrapper">
|
||||
<a class="book__link" href="${viewerLink}" data-hover="Preview">
|
||||
let bookLinkWrapper = `
|
||||
<div class="book__link__wrapper">
|
||||
<div class="book__icon" ${faviconAttr}></div>
|
||||
<div class="book__header">
|
||||
@ -207,7 +215,11 @@
|
||||
</div>
|
||||
<div class="book__description" title="${description}">${description}</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
divTag.innerHTML = `
|
||||
<div class="book__wrapper">
|
||||
${addBookPreviewLink(bookLinkWrapper, book)}
|
||||
<div class="book__meta">
|
||||
<div class="book__languageTag" ${languageAttr}>${getLanguageCodeToDisplay(langCode)}</div>
|
||||
<div class="book__tags"><div class="book__tags--wrapper">${tagHtml}</div></div>
|
||||
|
@ -13,7 +13,8 @@
|
||||
{{#icons}}<link rel="http://opds-spec.org/image/thumbnail"
|
||||
href="{{root}}/catalog/v2/illustration/{{{id}}}/?size={{icon_size}}"
|
||||
type="{{icon_mimetype}};width={{icon_size}};height={{icon_size}};scale=1"/>
|
||||
{{/icons}}<link type="text/html" href="{{root}}/content/{{{content_id}}}" />
|
||||
{{/icons}}{{#contentServerUrl}}<link type="text/html" href="{{contentServerUrl}}/content/{{{content_id}}}" />
|
||||
{{/contentServerUrl}}
|
||||
<author>
|
||||
<name>{{author_name}}</name>
|
||||
</author>
|
||||
|
@ -33,7 +33,6 @@
|
||||
<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>
|
||||
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
|
||||
|
@ -108,7 +108,7 @@
|
||||
<h3 class="kiwixHomeBody__results">{{translations.count-of-matching-books}}</h3>
|
||||
{{#books}}
|
||||
<div class="book__wrapper">
|
||||
<a class="book__link" href="{{root}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">
|
||||
{{#contentServerUrl}}<a class="book__link" href="{{contentServerUrl}}/content/{{id}}" title="{{translations.preview-book}}" aria-label="{{translations.preview-book}}">{{/contentServerUrl}}
|
||||
<div class="book__link__wrapper">
|
||||
<div class="book__icon" {{faviconAttr}}></div>
|
||||
<div class="book__header">
|
||||
@ -116,7 +116,7 @@
|
||||
</div>
|
||||
<div class="book__description" title="{{description}}">{{description}}</div>
|
||||
</div>
|
||||
</a>
|
||||
{{#contentServerUrl}}</a>{{/contentServerUrl}}
|
||||
<div class="book__meta">
|
||||
<div class="book__languageTag" title="{{langTag.langFullString}}" aria-label="{{langTag.langFullString}}">{{langTag.langShortString}}</div>
|
||||
<div class="book__tags"><div class="book__tags--wrapper">
|
||||
|
@ -51,4 +51,21 @@
|
||||
size="556"
|
||||
favicon="faviconMimeType_attribute_is_absent"
|
||||
></book>
|
||||
<book
|
||||
id="inaccessiblezim"
|
||||
path="./nosuchzimfile.zim"
|
||||
url="https://github.com/kiwix/libkiwix/raw/master/test/data/nosuchzimfile.zim"
|
||||
title="Catalog of all catalogs"
|
||||
description="Testing that running kiwix-serve without access to ZIM files doesn't lead to a catastrophe"
|
||||
language="cat"
|
||||
creator="Catherine of Catalonia"
|
||||
publisher="Caterpillar"
|
||||
date="2025-09-04"
|
||||
name="catalog_of_all_catalogs"
|
||||
tags="unittest;_category:cats"
|
||||
articleCount="12107"
|
||||
mediaCount="8"
|
||||
size="20250904"
|
||||
favicon="Catania Cathedral"
|
||||
></book>
|
||||
</library>
|
||||
|
@ -18,9 +18,11 @@ protected:
|
||||
const int PORT = 8002;
|
||||
|
||||
protected:
|
||||
void resetServer(ZimFileServer::Options options) {
|
||||
void resetServer(ZimFileServer::Options options, std::string contentServerUrl="") {
|
||||
ZimFileServer::Cfg cfg(options);
|
||||
cfg.contentServerUrl = contentServerUrl;
|
||||
zfs1_.reset();
|
||||
zfs1_.reset(new ZimFileServer(PORT, options, "./test/library.xml"));
|
||||
zfs1_.reset(new ZimFileServer(PORT, cfg, "./test/library.xml"));
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
@ -150,6 +152,30 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||
"125952"\
|
||||
)
|
||||
|
||||
#define INACCESSIBLEZIMFILE_CATALOG_ENTRY \
|
||||
" <entry>\n" \
|
||||
" <id>urn:uuid:inaccessiblezim</id>\n" \
|
||||
" <title>Catalog of all catalogs</title>\n" \
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||
" <summary>Testing that running kiwix-serve without access to ZIM files doesn't lead to a catastrophe</summary>\n" \
|
||||
" <language>cat</language>\n" \
|
||||
" <name>catalog_of_all_catalogs</name>\n" \
|
||||
" <flavour></flavour>\n" \
|
||||
" <category>cats</category>\n" \
|
||||
" <tags>unittest;_category:cats</tags>\n" \
|
||||
" <articleCount>12107</articleCount>\n" \
|
||||
" <mediaCount>8</mediaCount>\n" \
|
||||
" <link type=\"text/html\" href=\"/ROOT%23%3F/content/nosuchzimfile\" />\n" \
|
||||
" <author>\n" \
|
||||
" <name>Catherine of Catalonia</name>\n" \
|
||||
" </author>\n" \
|
||||
" <publisher>\n" \
|
||||
" <name>Caterpillar</name>\n" \
|
||||
" </publisher>\n" \
|
||||
" <dc:issued>2025-09-04T00:00:00Z</dc:issued>\n" \
|
||||
" <link rel=\"http://opds-spec.org/acquisition/open-access\" type=\"application/x-zim\" href=\"https://github.com/kiwix/libkiwix/raw/master/test/data/nosuchzimfile.zim\" length=\"20736925696\" />\n" \
|
||||
" </entry>\n"
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_root_xml)
|
||||
{
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/root.xml");
|
||||
@ -560,6 +586,15 @@ TEST_F(LibraryServerTest, catalog_v2_categories)
|
||||
<title>List of categories</title>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
|
||||
<entry>
|
||||
<title>cats</title>
|
||||
<link rel="subsection"
|
||||
href="/ROOT%23%3F/catalog/v2/entries?category=cats"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
<content type="text">All entries with category of 'cats'.</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>jazz</title>
|
||||
<link rel="subsection"
|
||||
@ -602,6 +637,16 @@ TEST_F(LibraryServerTest, catalog_v2_languages)
|
||||
<title>List of languages</title>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
|
||||
<entry>
|
||||
<title>català</title>
|
||||
<dc:language>cat</dc:language>
|
||||
<thr:count>1</thr:count>
|
||||
<link rel="subsection"
|
||||
href="/ROOT%23%3F/catalog/v2/entries?lang=cat"
|
||||
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>English</title>
|
||||
<dc:language>eng</dc:language>
|
||||
@ -678,6 +723,29 @@ TEST_F(LibraryServerTest, catalog_v2_entries)
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_catalog_only_mode)
|
||||
{
|
||||
const std::string contentServerUrl = "https://demo.kiwix.org";
|
||||
const auto fixContentLinks = [=](std::string s) -> std::string {
|
||||
s = replace(s, "/ROOT%23%3F/content", contentServerUrl + "/content");
|
||||
return s;
|
||||
};
|
||||
resetServer(ZimFileServer::CATALOG_ONLY_MODE, contentServerUrl);
|
||||
const auto r = zfs1_->GET("/ROOT%23%3F/catalog/v2/entries");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||
CATALOG_V2_ENTRIES_PREAMBLE("")
|
||||
" <title>All Entries</title>\n"
|
||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||
"\n"
|
||||
+ fixContentLinks(CHARLES_RAY_CATALOG_ENTRY)
|
||||
+ fixContentLinks(INACCESSIBLEZIMFILE_CATALOG_ENTRY)
|
||||
+ fixContentLinks(RAY_CHARLES_CATALOG_ENTRY)
|
||||
+ fixContentLinks(UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY) +
|
||||
"</feed>\n"
|
||||
);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range)
|
||||
{
|
||||
{
|
||||
@ -1320,6 +1388,32 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" </div>\n" \
|
||||
" </div>\n"
|
||||
|
||||
#define INACCESSIBLEZIMFILE_BOOK_HTML \
|
||||
" <div class=\"book__wrapper\">\n" \
|
||||
" <a class=\"book__link\" href=\"/ROOT%23%3F/content/nosuchzimfile\" title=\"Preview\" aria-label=\"Preview\">\n" \
|
||||
" <div class=\"book__link__wrapper\">\n" \
|
||||
" <div class=\"book__icon\" style=background-image:url(/ROOT%23%3F/catalog/v2/illustration/inaccessiblezim/?size=48)></div>\n" \
|
||||
" <div class=\"book__header\">\n" \
|
||||
" <div id=\"book__title\">Catalog of all catalogs</div>\n" \
|
||||
" </div>\n" \
|
||||
" <div class=\"book__description\" title=\"Testing that running kiwix-serve without access to ZIM files doesn't lead to a catastrophe\">Testing that running kiwix-serve without access to ZIM files doesn't lead to a catastrophe</div>\n" \
|
||||
" </div>\n" \
|
||||
" </a>\n" \
|
||||
" <div class=\"book__meta\">\n" \
|
||||
" <div class=\"book__languageTag\" title=\"català\" aria-label=\"català\">cat</div>\n" \
|
||||
" <div class=\"book__tags\"><div class=\"book__tags--wrapper\">\n" \
|
||||
" <span class=\"tag__link\" aria-label='unittest' title='unittest'>unittest</span>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
" <div>\n" \
|
||||
" <a class=\"book__download\" href=\"/ROOT%23%3F/nojs/download/nosuchzimfile\">\n" \
|
||||
" <img src=\"/ROOT%23%3F/skin/download-white.svg?cacheid=079ab989\">\n" \
|
||||
" <span>Download</span>\n" \
|
||||
" </a>\n" \
|
||||
" </div>\n" \
|
||||
" </div>\n"
|
||||
|
||||
#define FINAL_HTML_TEXT \
|
||||
" </div>\n" \
|
||||
" </div>\n" \
|
||||
@ -1332,6 +1426,7 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" <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=\"cat\">català</option>\n" \
|
||||
" <option value=\"eng\"" SELECTED_ENG ">English</option>\n" \
|
||||
" <option value=\"fra\">français</option>\n" \
|
||||
" <option value=\"rus\">русский</option>\n" \
|
||||
@ -1340,6 +1435,7 @@ TEST_F(LibraryServerTest, no_name_mapper_catalog_v2_individual_entry_access)
|
||||
" <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=\"cats\">Cats</option>\n" \
|
||||
" <option value=\"jazz\">Jazz</option>\n" \
|
||||
" <option value=\"wikipedia\">Wikipedia</option>\n" \
|
||||
" </select>\n" \
|
||||
@ -1453,4 +1549,25 @@ TEST_F(LibraryServerTest, noJS) {
|
||||
EXPECT_EQ(r->body, RAY_CHARLES_UNCTZ_DOWNLOAD);
|
||||
}
|
||||
|
||||
TEST_F(LibraryServerTest, noJS_catalogOnlyMode) {
|
||||
const std::string contentServerUrl = "https://demo.kiwix.org";
|
||||
const auto fixContentLinks = [=](std::string s) -> std::string {
|
||||
s = replace(s, "/ROOT%23%3F/content", contentServerUrl + "/content");
|
||||
return s;
|
||||
};
|
||||
resetServer(ZimFileServer::CATALOG_ONLY_MODE, contentServerUrl);
|
||||
|
||||
auto r = zfs1_->GET("/ROOT%23%3F/nojs");
|
||||
EXPECT_EQ(r->status, 200);
|
||||
EXPECT_EQ(r->body,
|
||||
HTML_PREAMBLE
|
||||
FILTERS_HTML("")
|
||||
HOME_BODY_TEXT("4")
|
||||
+ fixContentLinks(CHARLES_RAY_BOOK_HTML)
|
||||
+ fixContentLinks(INACCESSIBLEZIMFILE_BOOK_HTML)
|
||||
+ fixContentLinks(RAY_CHARLES_BOOK_HTML)
|
||||
+ fixContentLinks(RAY_CHARLES_UNCTZ_BOOK_HTML)
|
||||
+ FINAL_HTML_TEXT);
|
||||
}
|
||||
|
||||
#undef EXPECT_SEARCH_RESULTS
|
||||
|
@ -88,6 +88,7 @@ TEST(Manager, reload)
|
||||
manager.reload({ "./test/library.xml" });
|
||||
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
|
||||
"charlesray",
|
||||
"inaccessiblezim",
|
||||
"raycharles",
|
||||
"raycharles_uncategorized"
|
||||
}));
|
||||
@ -95,12 +96,14 @@ TEST(Manager, reload)
|
||||
lib->removeBookById("raycharles");
|
||||
EXPECT_EQ(lib->getBooksIds(), (kiwix::Library::BookIdCollection{
|
||||
"charlesray",
|
||||
"inaccessiblezim",
|
||||
"raycharles_uncategorized"
|
||||
}));
|
||||
|
||||
manager.reload({ "./test/library.xml" });
|
||||
EXPECT_EQ(lib->getBooksIds(), kiwix::Library::BookIdCollection({
|
||||
"charlesray",
|
||||
"inaccessiblezim",
|
||||
"raycharles",
|
||||
"raycharles_uncategorized"
|
||||
}));
|
||||
|
@ -65,7 +65,7 @@ const ResourceCollection resources200Compressible{
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.css" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.css?cacheid=ae79e41a" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/index.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=cc456f1f" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/index.js?cacheid=4e232c58" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js" },
|
||||
{ STATIC_CONTENT, "/ROOT%23%3F/skin/iso6391To3.js?cacheid=ecde2bb3" },
|
||||
{ DYNAMIC_CONTENT, "/ROOT%23%3F/skin/isotope.pkgd.min.js" },
|
||||
@ -304,7 +304,7 @@ R"EXPECTEDRESULT( href="/ROOT%23%3F/skin/kiwix.css?cacheid=b4e29e64"
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/languages.js?cacheid=08955948" 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=cc456f1f" defer></script>
|
||||
<script type="text/javascript" src="/ROOT%23%3F/skin/index.js?cacheid=4e232c58" defer></script>
|
||||
<img src="/ROOT%23%3F/skin/feed.svg?cacheid=055b333f"
|
||||
<img src="/ROOT%23%3F/skin/langSelector.svg?cacheid=00b59961"
|
||||
)EXPECTEDRESULT"
|
||||
|
@ -62,6 +62,7 @@ public: // types
|
||||
WITH_LIBRARY_BUTTON = 1 << 2,
|
||||
BLOCK_EXTERNAL_LINKS = 1 << 3,
|
||||
NO_NAME_MAPPER = 1 << 4,
|
||||
CATALOG_ONLY_MODE = 1 << 5,
|
||||
|
||||
WITH_TASKBAR_AND_LIBRARY_BUTTON = WITH_TASKBAR | WITH_LIBRARY_BUTTON,
|
||||
|
||||
@ -71,6 +72,7 @@ public: // types
|
||||
struct Cfg
|
||||
{
|
||||
std::string root = "ROOT#?";
|
||||
std::string contentServerUrl = "";
|
||||
Options options = DEFAULT_OPTIONS;
|
||||
|
||||
Cfg(Options opts = DEFAULT_OPTIONS) : options(opts) {}
|
||||
@ -149,6 +151,8 @@ void ZimFileServer::run(int serverPort, std::string indexTemplateString)
|
||||
server->setTaskbar(cfg.options & WITH_TASKBAR, cfg.options & WITH_LIBRARY_BUTTON);
|
||||
server->setBlockExternalLinks(cfg.options & BLOCK_EXTERNAL_LINKS);
|
||||
server->setMultiZimSearchLimit(3);
|
||||
server->setCatalogOnlyMode(cfg.options & CATALOG_ONLY_MODE);
|
||||
server->setContentServerUrl(cfg.contentServerUrl);
|
||||
if (!indexTemplateString.empty()) {
|
||||
server->setIndexTemplateString(indexTemplateString);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user