Merge pull request #1219 from kiwix/catalog_only_mode

Support for catalog only mode of kiwix-serve
This commit is contained in:
Kelson 2025-09-11 13:45:32 +02:00 committed by GitHub
commit 2b4b90f8a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 257 additions and 61 deletions

View File

@ -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;

View File

@ -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;
};
}

View File

@ -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},

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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();
}

View File

@ -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);

View File

@ -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;
};
}

View File

@ -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]

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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&apos;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&apos;t lead to a catastrophe\">Testing that running kiwix-serve without access to ZIM files doesn&apos;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

View File

@ -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"
}));

View File

@ -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"

View File

@ -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);
}