mirror of https://github.com/kiwix/libkiwix.git
Merge pull request #492 from kiwix/opds_categories_feed
This commit is contained in:
commit
6d237ff1d5
|
@ -233,12 +233,19 @@ class Library
|
||||||
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;
|
unsigned int getBookCount(const bool localBooks, const bool remoteBooks) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all langagues of the books in the library.
|
* Get all languagues of the books in the library.
|
||||||
*
|
*
|
||||||
* @return A list of languages.
|
* @return A list of languages.
|
||||||
*/
|
*/
|
||||||
std::vector<std::string> getBooksLanguages() const;
|
std::vector<std::string> getBooksLanguages() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all categories of the books in the library.
|
||||||
|
*
|
||||||
|
* @return A list of categories.
|
||||||
|
*/
|
||||||
|
std::vector<std::string> getBooksCategories() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all book creators of the books in the library.
|
* Get all book creators of the books in the library.
|
||||||
*
|
*
|
||||||
|
|
|
@ -51,24 +51,35 @@ class OPDSDumper
|
||||||
/**
|
/**
|
||||||
* Dump the OPDS feed.
|
* Dump the OPDS feed.
|
||||||
*
|
*
|
||||||
* @param id The id of the library.
|
* @param bookIds the ids of the books to include in the feed
|
||||||
|
* @param query the query used to obtain the list of book ids
|
||||||
* @return The OPDS feed.
|
* @return The OPDS feed.
|
||||||
*/
|
*/
|
||||||
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds);
|
std::string dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the id of the opds stream.
|
* Dump the OPDS feed.
|
||||||
|
*
|
||||||
|
* @param bookIds the ids of the books to include in the feed
|
||||||
|
* @param query the query used to obtain the list of book ids
|
||||||
|
* @return The OPDS feed.
|
||||||
|
*/
|
||||||
|
std::string dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump the categories OPDS feed.
|
||||||
|
*
|
||||||
|
* @param categories list of category names
|
||||||
|
* @return The OPDS feed.
|
||||||
|
*/
|
||||||
|
std::string categoriesOPDSFeed(const std::vector<std::string>& categories) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the id of the library.
|
||||||
*
|
*
|
||||||
* @param id the id to use.
|
* @param id the id to use.
|
||||||
*/
|
*/
|
||||||
void setId(const std::string& id) { this->id = id;}
|
void setLibraryId(const std::string& id) { this->libraryId = id;}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the title oft the opds stream.
|
|
||||||
*
|
|
||||||
* @param title the title to use.
|
|
||||||
*/
|
|
||||||
void setTitle(const std::string& title) { this->title = title; }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the root location used when generating url.
|
* Set the root location used when generating url.
|
||||||
|
@ -77,13 +88,6 @@ class OPDSDumper
|
||||||
*/
|
*/
|
||||||
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
|
void setRootLocation(const std::string& rootLocation) { this->rootLocation = rootLocation; }
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the search url.
|
|
||||||
*
|
|
||||||
* @param searchUrl the search url to use.
|
|
||||||
*/
|
|
||||||
void setSearchDescriptionUrl(const std::string& searchDescriptionUrl) { this->searchDescriptionUrl = searchDescriptionUrl; }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set some informations about the search results.
|
* Set some informations about the search results.
|
||||||
*
|
*
|
||||||
|
@ -93,27 +97,13 @@ class OPDSDumper
|
||||||
*/
|
*/
|
||||||
void setOpenSearchInfo(int totalResult, int startIndex, int count);
|
void setOpenSearchInfo(int totalResult, int startIndex, int count);
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the library to dump.
|
|
||||||
*
|
|
||||||
* @param library The library to dump.
|
|
||||||
*/
|
|
||||||
void setLibrary(Library* library) { this->library = library; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
kiwix::Library* library;
|
kiwix::Library* library;
|
||||||
std::string id;
|
std::string libraryId;
|
||||||
std::string title;
|
|
||||||
std::string date;
|
|
||||||
std::string rootLocation;
|
std::string rootLocation;
|
||||||
std::string searchDescriptionUrl;
|
|
||||||
int m_totalResults;
|
int m_totalResults;
|
||||||
int m_startIndex;
|
int m_startIndex;
|
||||||
int m_count;
|
int m_count;
|
||||||
bool m_isSearchResult = false;
|
|
||||||
|
|
||||||
private:
|
|
||||||
pugi::xml_node handleBook(Book book, pugi::xml_node root_node);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <zim/zim.h>
|
#include <zim/zim.h>
|
||||||
|
#include <mustache.hpp>
|
||||||
|
|
||||||
namespace pugi {
|
namespace pugi {
|
||||||
class xml_node;
|
class xml_node;
|
||||||
|
@ -45,6 +46,11 @@ namespace kiwix
|
||||||
|
|
||||||
using MimeCounterType = std::map<const std::string, zim::entry_index_type>;
|
using MimeCounterType = std::map<const std::string, zim::entry_index_type>;
|
||||||
MimeCounterType parseMimetypeCounter(const std::string& counterData);
|
MimeCounterType parseMimetypeCounter(const std::string& counterData);
|
||||||
|
|
||||||
|
std::string gen_date_str();
|
||||||
|
std::string gen_uuid(const std::string& s);
|
||||||
|
|
||||||
|
std::string render_template(const std::string& template_str, kainjow::mustache::data data);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -201,6 +201,21 @@ std::vector<std::string> Library::getBooksLanguages() const
|
||||||
return booksLanguages;
|
return booksLanguages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> Library::getBooksCategories() const
|
||||||
|
{
|
||||||
|
std::set<std::string> categories;
|
||||||
|
|
||||||
|
for (const auto& pair: m_books) {
|
||||||
|
const auto& book = pair.second;
|
||||||
|
const auto& c = book.getCategory();
|
||||||
|
if ( !c.empty() ) {
|
||||||
|
categories.insert(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::vector<std::string>(categories.begin(), categories.end());
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> Library::getBooksCreators() const
|
std::vector<std::string> Library::getBooksCreators() const
|
||||||
{
|
{
|
||||||
std::vector<std::string> booksCreators;
|
std::vector<std::string> booksCreators;
|
||||||
|
|
|
@ -25,7 +25,8 @@ kiwix_sources = [
|
||||||
'server/etag.cpp',
|
'server/etag.cpp',
|
||||||
'server/request_context.cpp',
|
'server/request_context.cpp',
|
||||||
'server/response.cpp',
|
'server/response.cpp',
|
||||||
'server/internalServer.cpp'
|
'server/internalServer.cpp',
|
||||||
|
'server/internalServer_catalog_v2.cpp'
|
||||||
]
|
]
|
||||||
kiwix_sources += lib_resources
|
kiwix_sources += lib_resources
|
||||||
|
|
||||||
|
|
|
@ -21,10 +21,13 @@
|
||||||
#include "book.h"
|
#include "book.h"
|
||||||
|
|
||||||
#include "tools/otherTools.h"
|
#include "tools/otherTools.h"
|
||||||
#include <iomanip>
|
|
||||||
|
#include "kiwixlib-resources.h"
|
||||||
|
#include <mustache.hpp>
|
||||||
|
|
||||||
namespace kiwix
|
namespace kiwix
|
||||||
{
|
{
|
||||||
|
|
||||||
/* Constructor */
|
/* Constructor */
|
||||||
OPDSDumper::OPDSDumper(Library* library)
|
OPDSDumper::OPDSDumper(Library* library)
|
||||||
: library(library)
|
: library(library)
|
||||||
|
@ -35,121 +38,111 @@ OPDSDumper::~OPDSDumper()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string gen_date_str()
|
|
||||||
{
|
|
||||||
auto now = time(0);
|
|
||||||
auto tm = localtime(&now);
|
|
||||||
|
|
||||||
std::stringstream is;
|
|
||||||
is << std::setw(2) << std::setfill('0')
|
|
||||||
<< 1900+tm->tm_year << "-"
|
|
||||||
<< std::setw(2) << std::setfill('0') << tm->tm_mon+1 << "-"
|
|
||||||
<< std::setw(2) << std::setfill('0') << tm->tm_mday << "T"
|
|
||||||
<< std::setw(2) << std::setfill('0') << tm->tm_hour << ":"
|
|
||||||
<< std::setw(2) << std::setfill('0') << tm->tm_min << ":"
|
|
||||||
<< std::setw(2) << std::setfill('0') << tm->tm_sec << "Z";
|
|
||||||
return is.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string gen_date_from_yyyy_mm_dd(const std::string& date)
|
|
||||||
{
|
|
||||||
std::stringstream is;
|
|
||||||
is << date << "T00:00::00Z";
|
|
||||||
return is.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
|
void OPDSDumper::setOpenSearchInfo(int totalResults, int startIndex, int count)
|
||||||
{
|
{
|
||||||
m_totalResults = totalResults;
|
m_totalResults = totalResults;
|
||||||
m_startIndex = startIndex,
|
m_startIndex = startIndex,
|
||||||
m_count = count;
|
m_count = count;
|
||||||
m_isSearchResult = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#define ADD_TEXT_ENTRY(node, child, value) (node).append_child((child)).append_child(pugi::node_pcdata).set_value((value).c_str())
|
namespace
|
||||||
|
|
||||||
pugi::xml_node OPDSDumper::handleBook(Book book, pugi::xml_node root_node) {
|
|
||||||
auto entry_node = root_node.append_child("entry");
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "id", "urn:uuid:"+book.getId());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "title", book.getTitle());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "summary", book.getDescription());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "language", book.getLanguage());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "updated", gen_date_from_yyyy_mm_dd(book.getDate()));
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "name", book.getName());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "flavour", book.getFlavour());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "category", book.getCategory());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "tags", book.getTags());
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "articleCount", to_string(book.getArticleCount()));
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "mediaCount", to_string(book.getMediaCount()));
|
|
||||||
ADD_TEXT_ENTRY(entry_node, "icon", rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath());
|
|
||||||
|
|
||||||
auto content_node = entry_node.append_child("link");
|
|
||||||
content_node.append_attribute("type") = "text/html";
|
|
||||||
content_node.append_attribute("href") = (rootLocation + "/" + book.getHumanReadableIdFromPath()).c_str();
|
|
||||||
|
|
||||||
auto author_node = entry_node.append_child("author");
|
|
||||||
ADD_TEXT_ENTRY(author_node, "name", book.getCreator());
|
|
||||||
|
|
||||||
auto publisher_node = entry_node.append_child("publisher");
|
|
||||||
ADD_TEXT_ENTRY(publisher_node, "name", book.getPublisher());
|
|
||||||
|
|
||||||
if (! book.getUrl().empty()) {
|
|
||||||
auto acquisition_link = entry_node.append_child("link");
|
|
||||||
acquisition_link.append_attribute("rel") = "http://opds-spec.org/acquisition/open-access";
|
|
||||||
acquisition_link.append_attribute("type") = "application/x-zim";
|
|
||||||
acquisition_link.append_attribute("href") = book.getUrl().c_str();
|
|
||||||
acquisition_link.append_attribute("length") = to_string(book.getSize()).c_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! book.getFaviconMimeType().empty() ) {
|
|
||||||
auto image_link = entry_node.append_child("link");
|
|
||||||
image_link.append_attribute("rel") = "http://opds-spec.org/image/thumbnail";
|
|
||||||
image_link.append_attribute("type") = book.getFaviconMimeType().c_str();
|
|
||||||
image_link.append_attribute("href") = (rootLocation + "/meta?name=favicon&content=" + book.getHumanReadableIdFromPath()).c_str();
|
|
||||||
}
|
|
||||||
return entry_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds)
|
|
||||||
{
|
{
|
||||||
date = gen_date_str();
|
|
||||||
pugi::xml_document doc;
|
|
||||||
|
|
||||||
auto root_node = doc.append_child("feed");
|
typedef kainjow::mustache::data MustacheData;
|
||||||
root_node.append_attribute("xmlns") = "http://www.w3.org/2005/Atom";
|
typedef kainjow::mustache::list BookData;
|
||||||
root_node.append_attribute("xmlns:opds") = "http://opds-spec.org/2010/catalog";
|
|
||||||
|
|
||||||
ADD_TEXT_ENTRY(root_node, "id", id);
|
BookData getBookData(const Library* library, const std::vector<std::string>& bookIds)
|
||||||
|
{
|
||||||
ADD_TEXT_ENTRY(root_node, "title", title);
|
BookData bookData;
|
||||||
ADD_TEXT_ENTRY(root_node, "updated", date);
|
for ( const auto& bookId : bookIds ) {
|
||||||
|
const Book& book = library->getBookById(bookId);
|
||||||
if (m_isSearchResult) {
|
const MustacheData bookUrl = book.getUrl().empty()
|
||||||
ADD_TEXT_ENTRY(root_node, "totalResults", to_string(m_totalResults));
|
? MustacheData(false)
|
||||||
ADD_TEXT_ENTRY(root_node, "startIndex", to_string(m_startIndex));
|
: MustacheData(book.getUrl());
|
||||||
ADD_TEXT_ENTRY(root_node, "itemsPerPage", to_string(m_count));
|
bookData.push_back(kainjow::mustache::object{
|
||||||
|
{"id", "urn:uuid:"+book.getId()},
|
||||||
|
{"name", book.getName()},
|
||||||
|
{"title", book.getTitle()},
|
||||||
|
{"description", book.getDescription()},
|
||||||
|
{"language", book.getLanguage()},
|
||||||
|
{"content_id", book.getHumanReadableIdFromPath()},
|
||||||
|
{"updated", book.getDate() + "T00:00:00Z"},
|
||||||
|
{"category", book.getCategory()},
|
||||||
|
{"flavour", book.getFlavour()},
|
||||||
|
{"tags", book.getTags()},
|
||||||
|
{"article_count", to_string(book.getArticleCount())},
|
||||||
|
{"media_count", to_string(book.getMediaCount())},
|
||||||
|
{"author_name", book.getCreator()},
|
||||||
|
{"publisher_name", book.getPublisher()},
|
||||||
|
{"url", bookUrl},
|
||||||
|
{"size", to_string(book.getSize())},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto self_link_node = root_node.append_child("link");
|
return bookData;
|
||||||
self_link_node.append_attribute("rel") = "self";
|
}
|
||||||
self_link_node.append_attribute("href") = "";
|
|
||||||
self_link_node.append_attribute("type") = "application/atom+xml";
|
|
||||||
|
|
||||||
|
} // unnamed namespace
|
||||||
|
|
||||||
if (!searchDescriptionUrl.empty() ) {
|
string OPDSDumper::dumpOPDSFeed(const std::vector<std::string>& bookIds, const std::string& query) const
|
||||||
auto search_link = root_node.append_child("link");
|
{
|
||||||
search_link.append_attribute("rel") = "search";
|
const auto bookData = getBookData(library, bookIds);
|
||||||
search_link.append_attribute("type") = "application/opensearchdescription+xml";
|
const kainjow::mustache::object template_data{
|
||||||
search_link.append_attribute("href") = searchDescriptionUrl.c_str();
|
{"date", gen_date_str()},
|
||||||
|
{"root", rootLocation},
|
||||||
|
{"feed_id", gen_uuid(libraryId + "/catalog/search?"+query)},
|
||||||
|
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
|
||||||
|
{"totalResults", to_string(m_totalResults)},
|
||||||
|
{"startIndex", to_string(m_startIndex)},
|
||||||
|
{"itemsPerPage", to_string(m_count)},
|
||||||
|
{"books", bookData }
|
||||||
|
};
|
||||||
|
|
||||||
|
return render_template(RESOURCE::templates::catalog_entries_xml, template_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
string OPDSDumper::dumpOPDSFeedV2(const std::vector<std::string>& bookIds, const std::string& query) const
|
||||||
|
{
|
||||||
|
const auto bookData = getBookData(library, bookIds);
|
||||||
|
|
||||||
|
const kainjow::mustache::object template_data{
|
||||||
|
{"date", gen_date_str()},
|
||||||
|
{"endpoint_root", rootLocation + "/catalog/v2"},
|
||||||
|
{"feed_id", gen_uuid(libraryId + "/entries?"+query)},
|
||||||
|
{"filter", query.empty() ? MustacheData(false) : MustacheData(query)},
|
||||||
|
{"query", query.empty() ? "" : "?" + urlEncode(query)},
|
||||||
|
{"totalResults", to_string(m_totalResults)},
|
||||||
|
{"startIndex", to_string(m_startIndex)},
|
||||||
|
{"itemsPerPage", to_string(m_count)},
|
||||||
|
{"books", bookData }
|
||||||
|
};
|
||||||
|
|
||||||
|
return render_template(RESOURCE::templates::catalog_v2_entries_xml, template_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string OPDSDumper::categoriesOPDSFeed(const std::vector<std::string>& categories) const
|
||||||
|
{
|
||||||
|
const auto now = gen_date_str();
|
||||||
|
kainjow::mustache::list categoryData;
|
||||||
|
for ( const auto& category : categories ) {
|
||||||
|
const auto urlencodedCategoryName = urlEncode(category);
|
||||||
|
categoryData.push_back(kainjow::mustache::object{
|
||||||
|
{"name", category},
|
||||||
|
{"urlencoded_name", urlencodedCategoryName},
|
||||||
|
{"updated", now},
|
||||||
|
{"id", gen_uuid(libraryId + "/categories/" + urlencodedCategoryName)}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (library) {
|
return render_template(
|
||||||
for (auto& bookId: bookIds) {
|
RESOURCE::templates::catalog_v2_categories_xml,
|
||||||
handleBook(library->getBookById(bookId), root_node);
|
kainjow::mustache::object{
|
||||||
|
{"date", now},
|
||||||
|
{"endpoint_root", rootLocation + "/catalog/v2"},
|
||||||
|
{"feed_id", gen_uuid(libraryId + "/categories")},
|
||||||
|
{"categories", categoryData }
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
return nodeToString(root_node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,21 @@ extern "C" {
|
||||||
|
|
||||||
namespace kiwix {
|
namespace kiwix {
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
inline std::string normalizeRootUrl(std::string rootUrl)
|
||||||
|
{
|
||||||
|
while ( !rootUrl.empty() && rootUrl.back() == '/' )
|
||||||
|
rootUrl.pop_back();
|
||||||
|
|
||||||
|
while ( !rootUrl.empty() && rootUrl.front() == '/' )
|
||||||
|
rootUrl = rootUrl.substr(1);
|
||||||
|
return rootUrl.empty() ? rootUrl : "/" + rootUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // unnamed namespace
|
||||||
|
|
||||||
static IdNameMapper defaultNameMapper;
|
static IdNameMapper defaultNameMapper;
|
||||||
|
|
||||||
static MHD_Result staticHandlerCallback(void* cls,
|
static MHD_Result staticHandlerCallback(void* cls,
|
||||||
|
@ -100,7 +115,7 @@ InternalServer::InternalServer(Library* library,
|
||||||
bool blockExternalLinks) :
|
bool blockExternalLinks) :
|
||||||
m_addr(addr),
|
m_addr(addr),
|
||||||
m_port(port),
|
m_port(port),
|
||||||
m_root(root),
|
m_root(normalizeRootUrl(root)),
|
||||||
m_nbThreads(nbThreads),
|
m_nbThreads(nbThreads),
|
||||||
m_verbose(verbose),
|
m_verbose(verbose),
|
||||||
m_withTaskbar(withTaskbar),
|
m_withTaskbar(withTaskbar),
|
||||||
|
@ -153,6 +168,7 @@ bool InternalServer::start() {
|
||||||
}
|
}
|
||||||
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
|
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
|
||||||
m_server_id = kiwix::to_string(server_start_time.count());
|
m_server_id = kiwix::to_string(server_start_time.count());
|
||||||
|
m_library_id = m_server_id;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,10 +259,10 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
|
||||||
if ( etag )
|
if ( etag )
|
||||||
return Response::build_304(*this, etag);
|
return Response::build_304(*this, etag);
|
||||||
|
|
||||||
if (kiwix::startsWith(request.get_url(), "/skin/"))
|
if (startsWith(request.get_url(), "/skin/"))
|
||||||
return handle_skin(request);
|
return handle_skin(request);
|
||||||
|
|
||||||
if (startsWith(request.get_url(), "/catalog"))
|
if (startsWith(request.get_url(), "/catalog/"))
|
||||||
return handle_catalog(request);
|
return handle_catalog(request);
|
||||||
|
|
||||||
if (request.get_url() == "/meta")
|
if (request.get_url() == "/meta")
|
||||||
|
@ -606,6 +622,10 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||||
return Response::build_404(*this, request, "");
|
return Response::build_404(*this, request, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url == "v2") {
|
||||||
|
return handle_catalog_v2(request);
|
||||||
|
}
|
||||||
|
|
||||||
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
if (url != "searchdescription.xml" && url != "root.xml" && url != "search") {
|
||||||
return Response::build_404(*this, request, "");
|
return Response::build_404(*this, request, "");
|
||||||
}
|
}
|
||||||
|
@ -616,13 +636,11 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||||
}
|
}
|
||||||
|
|
||||||
zim::Uuid uuid;
|
zim::Uuid uuid;
|
||||||
kiwix::OPDSDumper opdsDumper;
|
kiwix::OPDSDumper opdsDumper(mp_library);
|
||||||
opdsDumper.setRootLocation(m_root);
|
opdsDumper.setRootLocation(m_root);
|
||||||
opdsDumper.setSearchDescriptionUrl("catalog/searchdescription.xml");
|
opdsDumper.setLibraryId(m_library_id);
|
||||||
opdsDumper.setLibrary(mp_library);
|
|
||||||
std::vector<std::string> bookIdsToDump;
|
std::vector<std::string> bookIdsToDump;
|
||||||
if (url == "root.xml") {
|
if (url == "root.xml") {
|
||||||
opdsDumper.setTitle("All zims");
|
|
||||||
uuid = zim::Uuid::generate(host);
|
uuid = zim::Uuid::generate(host);
|
||||||
bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
|
bookIdsToDump = mp_library->filter(kiwix::Filter().valid(true).local(true).remote(true));
|
||||||
} else if (url == "search") {
|
} else if (url == "search") {
|
||||||
|
@ -630,28 +648,24 @@ std::unique_ptr<Response> InternalServer::handle_catalog(const RequestContext& r
|
||||||
uuid = zim::Uuid::generate();
|
uuid = zim::Uuid::generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
opdsDumper.setId(kiwix::to_string(uuid));
|
|
||||||
auto response = ContentResponse::build(
|
auto response = ContentResponse::build(
|
||||||
*this,
|
*this,
|
||||||
opdsDumper.dumpOPDSFeed(bookIdsToDump),
|
opdsDumper.dumpOPDSFeed(bookIdsToDump, request.get_query()),
|
||||||
"application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8");
|
"application/atom+xml; profile=opds-catalog; kind=acquisition; charset=utf-8");
|
||||||
return std::move(response);
|
return std::move(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string>
|
namespace
|
||||||
InternalServer::search_catalog(const RequestContext& request,
|
{
|
||||||
kiwix::OPDSDumper& opdsDumper)
|
|
||||||
|
Filter get_search_filter(const RequestContext& request)
|
||||||
{
|
{
|
||||||
auto filter = kiwix::Filter().valid(true).local(true);
|
auto filter = kiwix::Filter().valid(true).local(true);
|
||||||
string query("<Empty query>");
|
|
||||||
size_t count(10);
|
|
||||||
size_t startIndex(0);
|
|
||||||
try {
|
try {
|
||||||
query = request.get_argument("q");
|
filter.query(request.get_argument("q"));
|
||||||
filter.query(query);
|
|
||||||
} catch (const std::out_of_range&) {}
|
} catch (const std::out_of_range&) {}
|
||||||
try {
|
try {
|
||||||
filter.maxSize(extractFromString<unsigned long>(request.get_argument("maxsize")));
|
filter.maxSize(request.get_argument<unsigned long>("maxsize"));
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
try {
|
try {
|
||||||
filter.name(request.get_argument("name"));
|
filter.name(request.get_argument("name"));
|
||||||
|
@ -662,26 +676,37 @@ InternalServer::search_catalog(const RequestContext& request,
|
||||||
try {
|
try {
|
||||||
filter.lang(request.get_argument("lang"));
|
filter.lang(request.get_argument("lang"));
|
||||||
} catch (const std::out_of_range&) {}
|
} catch (const std::out_of_range&) {}
|
||||||
try {
|
|
||||||
count = extractFromString<unsigned long>(request.get_argument("count"));
|
|
||||||
} catch (...) {}
|
|
||||||
try {
|
|
||||||
startIndex = extractFromString<unsigned long>(request.get_argument("start"));
|
|
||||||
} catch (...) {}
|
|
||||||
try {
|
try {
|
||||||
filter.acceptTags(kiwix::split(request.get_argument("tag"), ";"));
|
filter.acceptTags(kiwix::split(request.get_argument("tag"), ";"));
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
try {
|
try {
|
||||||
filter.rejectTags(kiwix::split(request.get_argument("notag"), ";"));
|
filter.rejectTags(kiwix::split(request.get_argument("notag"), ";"));
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
opdsDumper.setTitle("Search result for " + query);
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
std::vector<T> subrange(const std::vector<T>& v, size_t s, size_t n)
|
||||||
|
{
|
||||||
|
const size_t e = std::min(v.size(), s+n);
|
||||||
|
return std::vector<T>(v.begin()+std::min(v.size(), s), v.begin()+e);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // unnamed namespace
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
InternalServer::search_catalog(const RequestContext& request,
|
||||||
|
kiwix::OPDSDumper& opdsDumper)
|
||||||
|
{
|
||||||
|
const auto filter = get_search_filter(request);
|
||||||
|
const std::string q = filter.hasQuery()
|
||||||
|
? filter.getQuery()
|
||||||
|
: "<Empty query>";
|
||||||
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
|
std::vector<std::string> bookIdsToDump = mp_library->filter(filter);
|
||||||
const auto totalResults = bookIdsToDump.size();
|
const auto totalResults = bookIdsToDump.size();
|
||||||
const auto s = std::min(startIndex, totalResults);
|
const size_t count = request.get_optional_param("count", 10UL);
|
||||||
bookIdsToDump.erase(bookIdsToDump.begin(), bookIdsToDump.begin()+s);
|
const size_t startIndex = request.get_optional_param("start", 0UL);
|
||||||
if (count>0 && bookIdsToDump.size() > count) {
|
bookIdsToDump = subrange(bookIdsToDump, startIndex, count);
|
||||||
bookIdsToDump.resize(count);
|
|
||||||
}
|
|
||||||
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
|
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
|
||||||
return bookIdsToDump;
|
return bookIdsToDump;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,10 @@ class InternalServer {
|
||||||
std::unique_ptr<Response> build_homepage(const RequestContext& request);
|
std::unique_ptr<Response> build_homepage(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_skin(const RequestContext& request);
|
std::unique_ptr<Response> handle_skin(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
|
std::unique_ptr<Response> handle_catalog(const RequestContext& request);
|
||||||
|
std::unique_ptr<Response> handle_catalog_v2(const RequestContext& request);
|
||||||
|
std::unique_ptr<Response> handle_catalog_v2_root(const RequestContext& request);
|
||||||
|
std::unique_ptr<Response> handle_catalog_v2_entries(const RequestContext& request);
|
||||||
|
std::unique_ptr<Response> handle_catalog_v2_categories(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_meta(const RequestContext& request);
|
std::unique_ptr<Response> handle_meta(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_search(const RequestContext& request);
|
std::unique_ptr<Response> handle_search(const RequestContext& request);
|
||||||
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
|
std::unique_ptr<Response> handle_suggest(const RequestContext& request);
|
||||||
|
@ -104,6 +108,7 @@ class InternalServer {
|
||||||
NameMapper* mp_nameMapper;
|
NameMapper* mp_nameMapper;
|
||||||
|
|
||||||
std::string m_server_id;
|
std::string m_server_id;
|
||||||
|
std::string m_library_id;
|
||||||
|
|
||||||
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
friend std::unique_ptr<Response> Response::build(const InternalServer& server);
|
||||||
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage);
|
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage);
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Veloman Yunkan <veloman.yunkan@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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "internalServer.h"
|
||||||
|
|
||||||
|
#include "library.h"
|
||||||
|
#include "opds_dumper.h"
|
||||||
|
#include "request_context.h"
|
||||||
|
#include "response.h"
|
||||||
|
#include "tools/otherTools.h"
|
||||||
|
#include "kiwixlib-resources.h"
|
||||||
|
|
||||||
|
#include <mustache.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kiwix {
|
||||||
|
|
||||||
|
std::unique_ptr<Response> InternalServer::handle_catalog_v2(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_catalog_v2");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string url;
|
||||||
|
try {
|
||||||
|
url = request.get_url_part(2);
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
return Response::build_404(*this, request, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url == "root.xml") {
|
||||||
|
return handle_catalog_v2_root(request);
|
||||||
|
} else if (url == "searchdescription.xml") {
|
||||||
|
const std::string endpoint_root = m_root + "/catalog/v2";
|
||||||
|
return ContentResponse::build(*this,
|
||||||
|
RESOURCE::catalog_v2_searchdescription_xml,
|
||||||
|
kainjow::mustache::object({{"endpoint_root", endpoint_root}}),
|
||||||
|
"application/opensearchdescription+xml"
|
||||||
|
);
|
||||||
|
} else if (url == "entries") {
|
||||||
|
return handle_catalog_v2_entries(request);
|
||||||
|
} else if (url == "categories") {
|
||||||
|
return handle_catalog_v2_categories(request);
|
||||||
|
} else {
|
||||||
|
return Response::build_404(*this, request, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Response> InternalServer::handle_catalog_v2_root(const RequestContext& request)
|
||||||
|
{
|
||||||
|
return ContentResponse::build(
|
||||||
|
*this,
|
||||||
|
RESOURCE::templates::catalog_v2_root_xml,
|
||||||
|
kainjow::mustache::object{
|
||||||
|
{"date", gen_date_str()},
|
||||||
|
{"endpoint_root", m_root + "/catalog/v2"},
|
||||||
|
{"feed_id", gen_uuid(m_library_id)},
|
||||||
|
{"all_entries_feed_id", gen_uuid(m_library_id + "/entries")},
|
||||||
|
{"category_list_feed_id", gen_uuid(m_library_id + "/categories")}
|
||||||
|
},
|
||||||
|
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Response> InternalServer::handle_catalog_v2_entries(const RequestContext& request)
|
||||||
|
{
|
||||||
|
OPDSDumper opdsDumper(mp_library);
|
||||||
|
opdsDumper.setRootLocation(m_root);
|
||||||
|
opdsDumper.setLibraryId(m_library_id);
|
||||||
|
const auto bookIds = search_catalog(request, opdsDumper);
|
||||||
|
const auto opdsFeed = opdsDumper.dumpOPDSFeedV2(bookIds, request.get_query());
|
||||||
|
return ContentResponse::build(
|
||||||
|
*this,
|
||||||
|
opdsFeed,
|
||||||
|
"application/atom+xml;profile=opds-catalog;kind=acquisition"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Response> InternalServer::handle_catalog_v2_categories(const RequestContext& request)
|
||||||
|
{
|
||||||
|
OPDSDumper opdsDumper(mp_library);
|
||||||
|
opdsDumper.setRootLocation(m_root);
|
||||||
|
opdsDumper.setLibraryId(m_library_id);
|
||||||
|
return ContentResponse::build(
|
||||||
|
*this,
|
||||||
|
opdsDumper.categoriesOPDSFeed(mp_library->getBooksCategories()),
|
||||||
|
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kiwix
|
|
@ -183,4 +183,14 @@ std::string RequestContext::get_header(const std::string& name) const {
|
||||||
return headers.at(lcAll(name));
|
return headers.at(lcAll(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string RequestContext::get_query() const {
|
||||||
|
std::string q;
|
||||||
|
const char* sep = "";
|
||||||
|
for ( const auto& a : arguments ) {
|
||||||
|
q += sep + a.first + '=' + a.second;
|
||||||
|
sep = "&";
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,11 +74,21 @@ class RequestContext {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
T get_optional_param(const std::string& name, T default_value) const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return get_argument<T>(name);
|
||||||
|
} catch (...) {}
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
RequestMethod get_method() const;
|
RequestMethod get_method() const;
|
||||||
std::string get_url() const;
|
std::string get_url() const;
|
||||||
std::string get_url_part(int part) const;
|
std::string get_url_part(int part) const;
|
||||||
std::string get_full_url() const;
|
std::string get_full_url() const;
|
||||||
|
std::string get_query() const;
|
||||||
|
|
||||||
ByteRange get_range() const;
|
ByteRange get_range() const;
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
#include "tools/regexTools.h"
|
#include "tools/regexTools.h"
|
||||||
#include "tools/stringTools.h"
|
#include "tools/stringTools.h"
|
||||||
|
#include "tools/otherTools.h"
|
||||||
|
|
||||||
#include "string.h"
|
#include "string.h"
|
||||||
#include <mustache.hpp>
|
#include <mustache.hpp>
|
||||||
|
@ -38,17 +39,6 @@ namespace
|
||||||
{
|
{
|
||||||
// some utilities
|
// some utilities
|
||||||
|
|
||||||
std::string render_template(const std::string& template_str, kainjow::mustache::data data)
|
|
||||||
{
|
|
||||||
kainjow::mustache::mustache tmpl(template_str);
|
|
||||||
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
|
|
||||||
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
|
|
||||||
data.set("urlencoded", urlencode);
|
|
||||||
std::stringstream ss;
|
|
||||||
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
|
|
||||||
return ss.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string get_mime_type(const zim::Item& item)
|
std::string get_mime_type(const zim::Item& item)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
#include "tools/otherTools.h"
|
#include "tools/otherTools.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <iomanip>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
@ -32,6 +33,8 @@
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <pugixml.hpp>
|
#include <pugixml.hpp>
|
||||||
|
|
||||||
|
#include <zim/uuid.h>
|
||||||
|
|
||||||
|
|
||||||
static std::map<std::string, std::string> codeisomapping {
|
static std::map<std::string, std::string> codeisomapping {
|
||||||
{ "aa", "aar" },
|
{ "aa", "aar" },
|
||||||
|
@ -341,3 +344,35 @@ kiwix::MimeCounterType kiwix::parseMimetypeCounter(const std::string& counterDat
|
||||||
|
|
||||||
return counters;
|
return counters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string kiwix::gen_date_str()
|
||||||
|
{
|
||||||
|
auto now = std::time(0);
|
||||||
|
auto tm = std::localtime(&now);
|
||||||
|
|
||||||
|
std::stringstream is;
|
||||||
|
is << std::setw(2) << std::setfill('0')
|
||||||
|
<< 1900+tm->tm_year << "-"
|
||||||
|
<< std::setw(2) << std::setfill('0') << tm->tm_mon+1 << "-"
|
||||||
|
<< std::setw(2) << std::setfill('0') << tm->tm_mday << "T"
|
||||||
|
<< std::setw(2) << std::setfill('0') << tm->tm_hour << ":"
|
||||||
|
<< std::setw(2) << std::setfill('0') << tm->tm_min << ":"
|
||||||
|
<< std::setw(2) << std::setfill('0') << tm->tm_sec << "Z";
|
||||||
|
return is.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string kiwix::gen_uuid(const std::string& s)
|
||||||
|
{
|
||||||
|
return kiwix::to_string(zim::Uuid::generate(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data)
|
||||||
|
{
|
||||||
|
kainjow::mustache::mustache tmpl(template_str);
|
||||||
|
kainjow::mustache::data urlencode{kainjow::mustache::lambda2{
|
||||||
|
[](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}};
|
||||||
|
data.set("urlencoded", urlencode);
|
||||||
|
std::stringstream ss;
|
||||||
|
tmpl.render(data, [&ss](const std::string& str) { ss << str; });
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
<ShortName>Zim catalog search</ShortName>
|
||||||
|
<Description>Search zim files in the catalog.</Description>
|
||||||
|
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:k="http://kiwix.org/opensearchextension/1.0"
|
||||||
|
indexOffset="0"
|
||||||
|
template="{{endpoint_root}}/entries?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}"/>
|
||||||
|
</OpenSearchDescription>
|
|
@ -6,5 +6,5 @@
|
||||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
xmlns:k="http://kiwix.org/opensearchextension/1.0"
|
xmlns:k="http://kiwix.org/opensearchextension/1.0"
|
||||||
indexOffset="0"
|
indexOffset="0"
|
||||||
template="/{{root}}/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}¬ag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}"/>
|
template="{{root}}/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}¬ag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}"/>
|
||||||
</OpenSearchDescription>
|
</OpenSearchDescription>
|
||||||
|
|
|
@ -36,4 +36,9 @@ templates/head_taskbar.html
|
||||||
templates/taskbar_part.html
|
templates/taskbar_part.html
|
||||||
templates/external_blocker_part.html
|
templates/external_blocker_part.html
|
||||||
templates/captured_external.html
|
templates/captured_external.html
|
||||||
|
templates/catalog_entries.xml
|
||||||
|
templates/catalog_v2_root.xml
|
||||||
|
templates/catalog_v2_entries.xml
|
||||||
|
templates/catalog_v2_categories.xml
|
||||||
opensearchdescription.xml
|
opensearchdescription.xml
|
||||||
|
catalog_v2_searchdescription.xml
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opds="http://opds-spec.org/2010/catalog">
|
||||||
|
<id>{{feed_id}}</id>
|
||||||
|
<title>{{^filter}}All zims{{/filter}}{{#filter}}Filtered zims ({{filter}}){{/filter}}</title>
|
||||||
|
<updated>{{date}}</updated>
|
||||||
|
{{#filter}}
|
||||||
|
<totalResults>{{totalResults}}</totalResults>
|
||||||
|
<startIndex>{{startIndex}}</startIndex>
|
||||||
|
<itemsPerPage>{{itemsPerPage}}</itemsPerPage>
|
||||||
|
{{/filter}}
|
||||||
|
<link rel="self" href="" type="application/atom+xml" />
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" href="{{root}}/catalog/searchdescription.xml" />
|
||||||
|
{{#books}}
|
||||||
|
<entry>
|
||||||
|
<id>{{id}}</id>
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<summary>{{description}}</summary>
|
||||||
|
<language>{{language}}</language>
|
||||||
|
<updated>{{updated}}</updated>
|
||||||
|
<name>{{name}}</name>
|
||||||
|
<flavour>{{flavour}}</flavour>
|
||||||
|
<category>{{category}}</category>
|
||||||
|
<tags>{{tags}}</tags>
|
||||||
|
<articleCount>{{article_count}}</articleCount>
|
||||||
|
<mediaCount>{{media_count}}</mediaCount>
|
||||||
|
<icon>/meta?name=favicon&content={{{content_id}}}</icon>
|
||||||
|
<link type="text/html" href="/{{{content_id}}}" />
|
||||||
|
<author>
|
||||||
|
<name>{{author_name}}</name>
|
||||||
|
</author>
|
||||||
|
<publisher>
|
||||||
|
<name>{{publisher_name}}</name>
|
||||||
|
</publisher>
|
||||||
|
{{#url}}
|
||||||
|
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
|
||||||
|
{{/url}}
|
||||||
|
</entry>
|
||||||
|
{{/books}}
|
||||||
|
</feed>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:opds="https://specs.opds.io/opds-1.2">
|
||||||
|
<id>{{feed_id}}</id>
|
||||||
|
<link rel="self"
|
||||||
|
href="{{endpoint_root}}/categories"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="start"
|
||||||
|
href="{{endpoint_root}}/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<title>List of categories</title>
|
||||||
|
<updated>{{date}}</updated>
|
||||||
|
|
||||||
|
{{#categories}}
|
||||||
|
<entry>
|
||||||
|
<title>{{name}}</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="{{endpoint_root}}/entries?category={{{urlencoded_name}}}"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||||
|
<updated>{{updated}}</updated>
|
||||||
|
<id>{{id}}</id>
|
||||||
|
<content type="text">All entries with category of '{{name}}'.</content>
|
||||||
|
</entry>
|
||||||
|
{{/categories}}
|
||||||
|
</feed>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:opds="https://specs.opds.io/opds-1.2"
|
||||||
|
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
<id>{{feed_id}}</id>
|
||||||
|
|
||||||
|
<link rel="self"
|
||||||
|
href="{{endpoint_root}}/entries{{{query}}}"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||||
|
<link rel="start"
|
||||||
|
href="{{endpoint_root}}/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="up"
|
||||||
|
href="{{endpoint_root}}/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
|
||||||
|
<title>{{^filter}}All Entries{{/filter}}{{#filter}}Filtered Entries ({{filter}}){{/filter}}</title>
|
||||||
|
<updated>{{date}}</updated>
|
||||||
|
{{#filter}}
|
||||||
|
<totalResults>{{totalResults}}</totalResults>
|
||||||
|
<startIndex>{{startIndex}}</startIndex>
|
||||||
|
<itemsPerPage>{{itemsPerPage}}</itemsPerPage>
|
||||||
|
{{/filter}}
|
||||||
|
{{#books}}
|
||||||
|
<entry>
|
||||||
|
<id>{{id}}</id>
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<summary>{{description}}</summary>
|
||||||
|
<language>{{language}}</language>
|
||||||
|
<updated>{{updated}}</updated>
|
||||||
|
<name>{{name}}</name>
|
||||||
|
<flavour>{{flavour}}</flavour>
|
||||||
|
<category>{{category}}</category>
|
||||||
|
<tags>{{tags}}</tags>
|
||||||
|
<articleCount>{{article_count}}</articleCount>
|
||||||
|
<mediaCount>{{media_count}}</mediaCount>
|
||||||
|
<icon>/meta?name=favicon&content={{{content_id}}}</icon>
|
||||||
|
<link type="text/html" href="/{{{content_id}}}" />
|
||||||
|
<author>
|
||||||
|
<name>{{author_name}}</name>
|
||||||
|
</author>
|
||||||
|
<publisher>
|
||||||
|
<name>{{publisher_name}}</name>
|
||||||
|
</publisher>
|
||||||
|
{{#url}}
|
||||||
|
<link rel="http://opds-spec.org/acquisition/open-access" type="application/x-zim" href="{{{url}}}" length="{{{size}}}" />
|
||||||
|
{{/url}}
|
||||||
|
</entry>
|
||||||
|
{{/books}}
|
||||||
|
</feed>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:opds="https://specs.opds.io/opds-1.2">
|
||||||
|
<id>{{feed_id}}</id>
|
||||||
|
<link rel="self"
|
||||||
|
href="{{endpoint_root}}/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="start"
|
||||||
|
href="{{endpoint_root}}/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="search"
|
||||||
|
href="{{endpoint_root}}/searchdescription.xml"
|
||||||
|
type="application/opensearchdescription+xml"/>
|
||||||
|
<title>OPDS Catalog Root</title>
|
||||||
|
<updated>{{date}}</updated>
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<title>All entries</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="{{endpoint_root}}/entries"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||||
|
<updated>{{date}}</updated>
|
||||||
|
<id>{{all_entries_feed_id}}</id>
|
||||||
|
<content type="text">All entries from this catalog.</content>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>List of categories</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="{{endpoint_root}}/categories"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<updated>{{date}}</updated>
|
||||||
|
<id>{{category_list_feed_id}}</id>
|
||||||
|
<content type="text">List of all categories in this catalog.</content>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
253
test/server.cpp
253
test/server.cpp
|
@ -240,6 +240,7 @@ const char* urls404[] = {
|
||||||
"/skin/non-existent-skin-resource",
|
"/skin/non-existent-skin-resource",
|
||||||
"/catalog",
|
"/catalog",
|
||||||
"/catalog/non-existent-item",
|
"/catalog/non-existent-item",
|
||||||
|
"/catalogBLABLABLA/root.xml",
|
||||||
"/meta",
|
"/meta",
|
||||||
"/meta?content=zimfile",
|
"/meta?content=zimfile",
|
||||||
"/meta?content=zimfile&name=non-existent-item",
|
"/meta?content=zimfile&name=non-existent-item",
|
||||||
|
@ -570,18 +571,21 @@ protected:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a copy of 'text' with every line that fully matches 'pattern'
|
// Returns a copy of 'text' where every line that fully matches 'pattern'
|
||||||
// replaced with the fixed string 'replacement'
|
// preceded by optional whitespace is replaced with the fixed string
|
||||||
|
// 'replacement' preserving the leading whitespace
|
||||||
std::string replaceLines(const std::string& text,
|
std::string replaceLines(const std::string& text,
|
||||||
const std::string& pattern,
|
const std::string& pattern,
|
||||||
const std::string& replacement)
|
const std::string& replacement)
|
||||||
{
|
{
|
||||||
std::regex regex("^" + pattern + "$");
|
std::regex regex("^ *" + pattern + "$");
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
std::istringstream iss(text);
|
std::istringstream iss(text);
|
||||||
std::string line;
|
std::string line;
|
||||||
while ( std::getline(iss, line) ) {
|
while ( std::getline(iss, line) ) {
|
||||||
if ( std::regex_match(line, regex) ) {
|
if ( std::regex_match(line, regex) ) {
|
||||||
|
for ( size_t i = 0; i < line.size() && line[i] == ' '; ++i )
|
||||||
|
oss << ' ';
|
||||||
oss << replacement << "\n";
|
oss << replacement << "\n";
|
||||||
} else {
|
} else {
|
||||||
oss << line << "\n";
|
oss << line << "\n";
|
||||||
|
@ -592,10 +596,10 @@ std::string replaceLines(const std::string& text,
|
||||||
|
|
||||||
std::string maskVariableOPDSFeedData(std::string s)
|
std::string maskVariableOPDSFeedData(std::string s)
|
||||||
{
|
{
|
||||||
s = replaceLines(s, " <updated>.+</updated>",
|
s = replaceLines(s, R"(<updated>\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ</updated>)",
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>");
|
"<updated>YYYY-MM-DDThh:mm:ssZ</updated>");
|
||||||
s = replaceLines(s, " <id>.+</id>",
|
s = replaceLines(s, "<id>[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}</id>",
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>");
|
"<id>12345678-90ab-cdef-1234-567890abcdef</id>");
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -607,7 +611,7 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||||
" <link rel=\"self\" href=\"\" type=\"application/atom+xml\" />\n" \
|
" <link rel=\"self\" href=\"\" type=\"application/atom+xml\" />\n" \
|
||||||
" <link rel=\"search\"" \
|
" <link rel=\"search\"" \
|
||||||
" type=\"application/opensearchdescription+xml\"" \
|
" type=\"application/opensearchdescription+xml\"" \
|
||||||
" href=\"catalog/searchdescription.xml\" />\n"
|
" href=\"/catalog/searchdescription.xml\" />\n"
|
||||||
|
|
||||||
#define CHARLES_RAY_CATALOG_ENTRY \
|
#define CHARLES_RAY_CATALOG_ENTRY \
|
||||||
" <entry>\n" \
|
" <entry>\n" \
|
||||||
|
@ -615,7 +619,7 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||||
" <title>Charles, Ray</title>\n" \
|
" <title>Charles, Ray</title>\n" \
|
||||||
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
||||||
" <language>eng</language>\n" \
|
" <language>eng</language>\n" \
|
||||||
" <updated>2020-03-31T00:00::00Z</updated>\n" \
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||||
" <name>wikipedia_en_ray_charles</name>\n" \
|
" <name>wikipedia_en_ray_charles</name>\n" \
|
||||||
" <flavour></flavour>\n" \
|
" <flavour></flavour>\n" \
|
||||||
" <category>jazz</category>\n" \
|
" <category>jazz</category>\n" \
|
||||||
|
@ -639,7 +643,7 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||||
" <title>Ray Charles</title>\n" \
|
" <title>Ray Charles</title>\n" \
|
||||||
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
" <summary>Wikipedia articles about Ray Charles</summary>\n" \
|
||||||
" <language>eng</language>\n" \
|
" <language>eng</language>\n" \
|
||||||
" <updated>2020-03-31T00:00::00Z</updated>\n" \
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||||
" <name>wikipedia_en_ray_charles</name>\n" \
|
" <name>wikipedia_en_ray_charles</name>\n" \
|
||||||
" <flavour></flavour>\n" \
|
" <flavour></flavour>\n" \
|
||||||
" <category>wikipedia</category>\n" \
|
" <category>wikipedia</category>\n" \
|
||||||
|
@ -663,7 +667,7 @@ std::string maskVariableOPDSFeedData(std::string s)
|
||||||
" <title>Ray (uncategorized) Charles</title>\n" \
|
" <title>Ray (uncategorized) Charles</title>\n" \
|
||||||
" <summary>No category is assigned to this library entry.</summary>\n" \
|
" <summary>No category is assigned to this library entry.</summary>\n" \
|
||||||
" <language>eng</language>\n" \
|
" <language>eng</language>\n" \
|
||||||
" <updated>2020-03-31T00:00::00Z</updated>\n" \
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n" \
|
||||||
" <name>wikipedia_en_ray_charles</name>\n" \
|
" <name>wikipedia_en_ray_charles</name>\n" \
|
||||||
" <flavour></flavour>\n" \
|
" <flavour></flavour>\n" \
|
||||||
" <category></category>\n" \
|
" <category></category>\n" \
|
||||||
|
@ -690,6 +694,7 @@ TEST_F(LibraryServerTest, catalog_root_xml)
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>All zims</title>\n"
|
" <title>All zims</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
|
"\n"
|
||||||
CATALOG_LINK_TAGS
|
CATALOG_LINK_TAGS
|
||||||
CHARLES_RAY_CATALOG_ENTRY
|
CHARLES_RAY_CATALOG_ENTRY
|
||||||
RAY_CHARLES_CATALOG_ENTRY
|
RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
@ -711,7 +716,7 @@ TEST_F(LibraryServerTest, catalog_searchdescription_xml)
|
||||||
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
|
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
|
||||||
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
|
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
|
||||||
" indexOffset=\"0\"\n"
|
" indexOffset=\"0\"\n"
|
||||||
" template=\"//catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}¬ag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
|
" template=\"/catalog/search?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}¬ag={k:notag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
|
||||||
"</OpenSearchDescription>\n"
|
"</OpenSearchDescription>\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -723,7 +728,7 @@ TEST_F(LibraryServerTest, catalog_search_by_phrase)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for \"ray charles\"</title>\n"
|
" <title>Filtered zims (q="ray charles")</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>2</totalResults>\n"
|
" <totalResults>2</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -742,7 +747,7 @@ TEST_F(LibraryServerTest, catalog_search_by_words)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for ray charles</title>\n"
|
" <title>Filtered zims (q=ray charles)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>3</totalResults>\n"
|
" <totalResults>3</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -763,7 +768,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for description:ray description:charles</title>\n"
|
" <title>Filtered zims (q=description:ray description:charles)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>2</totalResults>\n"
|
" <totalResults>2</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -780,7 +785,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for title:\"ray charles\"</title>\n"
|
" <title>Filtered zims (q=title:"ray charles")</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>1</totalResults>\n"
|
" <totalResults>1</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -799,7 +804,7 @@ TEST_F(LibraryServerTest, catalog_search_with_word_exclusion)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for ray -uncategorized</title>\n"
|
" <title>Filtered zims (q=ray -uncategorized)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>2</totalResults>\n"
|
" <totalResults>2</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -818,7 +823,7 @@ TEST_F(LibraryServerTest, catalog_search_by_tag)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for <Empty query></title>\n"
|
" <title>Filtered zims (tag=_category:jazz)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>1</totalResults>\n"
|
" <totalResults>1</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -836,7 +841,7 @@ TEST_F(LibraryServerTest, catalog_search_by_category)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for <Empty query></title>\n"
|
" <title>Filtered zims (category=jazz)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>1</totalResults>\n"
|
" <totalResults>1</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -855,7 +860,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for <Empty query></title>\n"
|
" <title>Filtered zims (count=1)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>3</totalResults>\n"
|
" <totalResults>3</totalResults>\n"
|
||||||
" <startIndex>0</startIndex>\n"
|
" <startIndex>0</startIndex>\n"
|
||||||
|
@ -871,7 +876,7 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for <Empty query></title>\n"
|
" <title>Filtered zims (count=1&start=1)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>3</totalResults>\n"
|
" <totalResults>3</totalResults>\n"
|
||||||
" <startIndex>1</startIndex>\n"
|
" <startIndex>1</startIndex>\n"
|
||||||
|
@ -887,13 +892,217 @@ TEST_F(LibraryServerTest, catalog_search_results_pagination)
|
||||||
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
OPDS_FEED_TAG
|
OPDS_FEED_TAG
|
||||||
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n"
|
||||||
" <title>Search result for <Empty query></title>\n"
|
" <title>Filtered zims (count=10&start=100)</title>\n"
|
||||||
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
" <totalResults>3</totalResults>\n"
|
" <totalResults>3</totalResults>\n"
|
||||||
" <startIndex>100</startIndex>\n"
|
" <startIndex>100</startIndex>\n"
|
||||||
" <itemsPerPage>0</itemsPerPage>\n"
|
" <itemsPerPage>0</itemsPerPage>\n"
|
||||||
CATALOG_LINK_TAGS
|
CATALOG_LINK_TAGS
|
||||||
|
" \n"
|
||||||
"</feed>\n"
|
"</feed>\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(LibraryServerTest, catalog_v2_root)
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/catalog/v2/root.xml");
|
||||||
|
EXPECT_EQ(r->status, 200);
|
||||||
|
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:opds="https://specs.opds.io/opds-1.2">
|
||||||
|
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||||
|
<link rel="self"
|
||||||
|
href="/catalog/v2/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="start"
|
||||||
|
href="/catalog/v2/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="search"
|
||||||
|
href="/catalog/v2/searchdescription.xml"
|
||||||
|
type="application/opensearchdescription+xml"/>
|
||||||
|
<title>OPDS Catalog Root</title>
|
||||||
|
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<title>All entries</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="/catalog/v2/entries"
|
||||||
|
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 from this catalog.</content>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>List of categories</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="/catalog/v2/categories"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||||
|
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||||
|
<content type="text">List of all categories in this catalog.</content>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
)";
|
||||||
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LibraryServerTest, catalog_v2_searchdescription_xml)
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/catalog/v2/searchdescription.xml");
|
||||||
|
EXPECT_EQ(r->status, 200);
|
||||||
|
EXPECT_EQ(r->body,
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||||
|
"<OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\">\n"
|
||||||
|
" <ShortName>Zim catalog search</ShortName>\n"
|
||||||
|
" <Description>Search zim files in the catalog.</Description>\n"
|
||||||
|
" <Url type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"\n"
|
||||||
|
" xmlns:atom=\"http://www.w3.org/2005/Atom\"\n"
|
||||||
|
" xmlns:k=\"http://kiwix.org/opensearchextension/1.0\"\n"
|
||||||
|
" indexOffset=\"0\"\n"
|
||||||
|
" template=\"/catalog/v2/entries?q={searchTerms?}&lang={language?}&name={k:name?}&tag={k:tag?}&maxsize={k:maxsize?}&count={count?}&start={startIndex?}\"/>\n"
|
||||||
|
"</OpenSearchDescription>\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LibraryServerTest, catalog_v2_categories)
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/catalog/v2/categories");
|
||||||
|
EXPECT_EQ(r->status, 200);
|
||||||
|
const char expected_output[] = R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:opds="https://specs.opds.io/opds-1.2">
|
||||||
|
<id>12345678-90ab-cdef-1234-567890abcdef</id>
|
||||||
|
<link rel="self"
|
||||||
|
href="/catalog/v2/categories"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="start"
|
||||||
|
href="/catalog/v2/root.xml"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<title>List of categories</title>
|
||||||
|
<updated>YYYY-MM-DDThh:mm:ssZ</updated>
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<title>jazz</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="/catalog/v2/entries?category=jazz"
|
||||||
|
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 'jazz'.</content>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>wikipedia</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="/catalog/v2/entries?category=wikipedia"
|
||||||
|
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 'wikipedia'.</content>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
)";
|
||||||
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body), expected_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define CATALOG_V2_ENTRIES_PREAMBLE(q) \
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
|
||||||
|
"<feed xmlns=\"http://www.w3.org/2005/Atom\"\n" \
|
||||||
|
" xmlns:opds=\"https://specs.opds.io/opds-1.2\"\n" \
|
||||||
|
" xmlns:opensearch=\"http://a9.com/-/spec/opensearch/1.1/\">\n" \
|
||||||
|
" <id>12345678-90ab-cdef-1234-567890abcdef</id>\n" \
|
||||||
|
"\n" \
|
||||||
|
" <link rel=\"self\"\n" \
|
||||||
|
" href=\"/catalog/v2/entries" q "\"\n" \
|
||||||
|
" type=\"application/atom+xml;profile=opds-catalog;kind=acquisition\"/>\n" \
|
||||||
|
" <link rel=\"start\"\n" \
|
||||||
|
" href=\"/catalog/v2/root.xml\"\n" \
|
||||||
|
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\n" \
|
||||||
|
" <link rel=\"up\"\n" \
|
||||||
|
" href=\"/catalog/v2/root.xml\"\n" \
|
||||||
|
" type=\"application/atom+xml;profile=opds-catalog;kind=navigation\"/>\n" \
|
||||||
|
"\n" \
|
||||||
|
|
||||||
|
|
||||||
|
TEST_F(LibraryServerTest, catalog_v2_entries)
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/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"
|
||||||
|
CHARLES_RAY_CATALOG_ENTRY
|
||||||
|
RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
"</feed>\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_range)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/catalog/v2/entries?start=1");
|
||||||
|
EXPECT_EQ(r->status, 200);
|
||||||
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
|
CATALOG_V2_ENTRIES_PREAMBLE("?start=1")
|
||||||
|
" <title>Filtered Entries (start=1)</title>\n"
|
||||||
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
|
" <totalResults>3</totalResults>\n"
|
||||||
|
" <startIndex>1</startIndex>\n"
|
||||||
|
" <itemsPerPage>2</itemsPerPage>\n"
|
||||||
|
RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
UNCATEGORIZED_RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
"</feed>\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/catalog/v2/entries?count=2");
|
||||||
|
EXPECT_EQ(r->status, 200);
|
||||||
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
|
CATALOG_V2_ENTRIES_PREAMBLE("?count=2")
|
||||||
|
" <title>Filtered Entries (count=2)</title>\n"
|
||||||
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
|
" <totalResults>3</totalResults>\n"
|
||||||
|
" <startIndex>0</startIndex>\n"
|
||||||
|
" <itemsPerPage>2</itemsPerPage>\n"
|
||||||
|
CHARLES_RAY_CATALOG_ENTRY
|
||||||
|
RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
"</feed>\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/catalog/v2/entries?start=1&count=1");
|
||||||
|
EXPECT_EQ(r->status, 200);
|
||||||
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
|
CATALOG_V2_ENTRIES_PREAMBLE("?count=1&start=1")
|
||||||
|
" <title>Filtered Entries (count=1&start=1)</title>\n"
|
||||||
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
|
" <totalResults>3</totalResults>\n"
|
||||||
|
" <startIndex>1</startIndex>\n"
|
||||||
|
" <itemsPerPage>1</itemsPerPage>\n"
|
||||||
|
RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
"</feed>\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms)
|
||||||
|
{
|
||||||
|
const auto r = zfs1_->GET("/catalog/v2/entries?q=\"ray%20charles\"");
|
||||||
|
EXPECT_EQ(r->status, 200);
|
||||||
|
EXPECT_EQ(maskVariableOPDSFeedData(r->body),
|
||||||
|
CATALOG_V2_ENTRIES_PREAMBLE("?q=%22ray%20charles%22")
|
||||||
|
" <title>Filtered Entries (q="ray charles")</title>\n"
|
||||||
|
" <updated>YYYY-MM-DDThh:mm:ssZ</updated>\n"
|
||||||
|
" <totalResults>2</totalResults>\n"
|
||||||
|
" <startIndex>0</startIndex>\n"
|
||||||
|
" <itemsPerPage>2</itemsPerPage>\n"
|
||||||
|
RAY_CHARLES_CATALOG_ENTRY
|
||||||
|
CHARLES_RAY_CATALOG_ENTRY
|
||||||
|
"</feed>\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue