Introduce the server functionality in the kiwix-lib.
This code is mainly copied from kiwix-tools. But : - Move all the response thing in a new class Response. - This Response class is responsible to handle all the MHD_response configuration. This way the server handle a global object and do no call to MHD_response* - Server uses a lot more the templating system with mustache. There are still few regex operations (because we need to change a content already existing). - By default, the server serves the content using the id as name. - Server creates a new Searcher per request. This way, we don't have to protect the search for multi-thread and we can do several search in the same time. - search results are not cached, this will allow future improvement in the search algorithm. - the home page is not cached. - Few more verbose information (number of request served, time spend to respond to a request). TOOD: - Readd interface selection. - Do Android wrapper. - Remove KiwixServer (who use a external process). -
|
@ -25,6 +25,8 @@
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
namespace kiwix
|
namespace kiwix
|
||||||
|
|
|
@ -11,6 +11,7 @@ headers = [
|
||||||
'entry.h',
|
'entry.h',
|
||||||
'searcher.h',
|
'searcher.h',
|
||||||
'search_renderer.h',
|
'search_renderer.h',
|
||||||
|
'server.h',
|
||||||
'kiwixserve.h',
|
'kiwixserve.h',
|
||||||
'name_mapper.h'
|
'name_mapper.h'
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Matthieu Gautier <mgautier@kymeria.fr>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KIWIX_SERVER_H
|
||||||
|
#define KIWIX_SERVER_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace kiwix
|
||||||
|
{
|
||||||
|
class Library;
|
||||||
|
class NameMapper;
|
||||||
|
class InternalServer;
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* The default constructor.
|
||||||
|
*
|
||||||
|
* @param library The library to serve.
|
||||||
|
*/
|
||||||
|
Server(Library& library, NameMapper* nameMapper=nullptr);
|
||||||
|
|
||||||
|
virtual ~Server();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve the content.
|
||||||
|
*/
|
||||||
|
bool start();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the daemon.
|
||||||
|
*/
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
void set_root(const std::string& root) { m_root = root; }
|
||||||
|
void set_port(int port) { m_port = port; }
|
||||||
|
void set_nbThreads(int threads) { m_nbThreads = threads; }
|
||||||
|
void set_verbose(bool verbose) { m_verbose = verbose; }
|
||||||
|
void set_taskbar(bool withTaskbar, bool withLibraryButton)
|
||||||
|
{ m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Library& m_library;
|
||||||
|
NameMapper* mp_nameMapper;
|
||||||
|
std::string m_root = "";
|
||||||
|
int m_port = 80;
|
||||||
|
int m_nbThreads = 1;
|
||||||
|
bool m_verbose = false;
|
||||||
|
bool m_withTaskbar = true;
|
||||||
|
bool m_withLibraryButton = true;
|
||||||
|
std::unique_ptr<InternalServer> mp_server;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -17,6 +17,7 @@ libicu_dep = dependency('icu-i18n', static:static_deps)
|
||||||
libzim_dep = dependency('libzim', version : '>=5.0.0', static:static_deps)
|
libzim_dep = dependency('libzim', version : '>=5.0.0', static:static_deps)
|
||||||
pugixml_dep = dependency('pugixml', static:static_deps)
|
pugixml_dep = dependency('pugixml', static:static_deps)
|
||||||
libcurl_dep = dependency('libcurl', static:static_deps)
|
libcurl_dep = dependency('libcurl', static:static_deps)
|
||||||
|
microhttpd_dep = dependency('libmicrohttpd', static:static_deps)
|
||||||
|
|
||||||
if not compiler.has_header('mustache.hpp')
|
if not compiler.has_header('mustache.hpp')
|
||||||
error('Cannot found header mustache.hpp')
|
error('Cannot found header mustache.hpp')
|
||||||
|
@ -28,7 +29,7 @@ if target_machine.system() == 'windows' and static_deps
|
||||||
extra_cflags += '-DCURL_STATICLIB'
|
extra_cflags += '-DCURL_STATICLIB'
|
||||||
endif
|
endif
|
||||||
|
|
||||||
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep]
|
all_deps = [thread_dep, libicu_dep, libzim_dep, pugixml_dep, libcurl_dep, microhttpd_dep]
|
||||||
|
|
||||||
inc = include_directories('include')
|
inc = include_directories('include')
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ kiwix_sources = [
|
||||||
'downloader.cpp',
|
'downloader.cpp',
|
||||||
'reader.cpp',
|
'reader.cpp',
|
||||||
'entry.cpp',
|
'entry.cpp',
|
||||||
|
'server.cpp',
|
||||||
'searcher.cpp',
|
'searcher.cpp',
|
||||||
'search_renderer.cpp',
|
'search_renderer.cpp',
|
||||||
'subprocess.cpp',
|
'subprocess.cpp',
|
||||||
|
@ -19,6 +20,8 @@ kiwix_sources = [
|
||||||
'tools/networkTools.cpp',
|
'tools/networkTools.cpp',
|
||||||
'tools/otherTools.cpp',
|
'tools/otherTools.cpp',
|
||||||
'kiwixserve.cpp',
|
'kiwixserve.cpp',
|
||||||
|
'server/request_context.cpp',
|
||||||
|
'server/response.cpp'
|
||||||
]
|
]
|
||||||
kiwix_sources += lib_resources
|
kiwix_sources += lib_resources
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,865 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Matthieu Gautier <mgautier@kymeria.fr>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
|
||||||
|
#if !defined(__MINGW32__) && (_MSC_VER < 1600)
|
||||||
|
#include "stdint4win.h"
|
||||||
|
#endif
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h> // otherwise socklen_t is not a recognized type
|
||||||
|
//#include <Windows.h> // otherwise int is not a recognized type
|
||||||
|
// typedef int off_t;
|
||||||
|
// typedef SSIZE_T ssize_t;
|
||||||
|
typedef UINT64 uint64_t;
|
||||||
|
typedef UINT16 uint16_t;
|
||||||
|
extern "C" {
|
||||||
|
#include <microhttpd.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "tools/otherTools.h"
|
||||||
|
#include "tools/pathTools.h"
|
||||||
|
#include "tools/regexTools.h"
|
||||||
|
#include "tools/stringTools.h"
|
||||||
|
#include "library.h"
|
||||||
|
#include "name_mapper.h"
|
||||||
|
#include "entry.h"
|
||||||
|
#include "searcher.h"
|
||||||
|
#include "search_renderer.h"
|
||||||
|
#include "opds_dumper.h"
|
||||||
|
|
||||||
|
#include <zim/uuid.h>
|
||||||
|
|
||||||
|
#include <mustache.hpp>
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
//#include <stdio.h>
|
||||||
|
//#include <stdlib.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
#include "kiwixlib-resources.h"
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
//#include <arpa/inet.h>
|
||||||
|
//#include <ifaddrs.h>
|
||||||
|
//#include <netdb.h>
|
||||||
|
//#include <stdint.h>
|
||||||
|
//#include <sys/socket.h>
|
||||||
|
//#include <netinet/in.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "server.h"
|
||||||
|
#include "server/request_context.h"
|
||||||
|
#include "server/response.h"
|
||||||
|
|
||||||
|
#ifdef interface
|
||||||
|
#undef interface
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#define MAX_SEARCH_LEN 140
|
||||||
|
|
||||||
|
namespace kiwix {
|
||||||
|
|
||||||
|
static IdNameMapper defaultNameMapper;
|
||||||
|
|
||||||
|
static int staticHandlerCallback(void* cls,
|
||||||
|
struct MHD_Connection* connection,
|
||||||
|
const char* url,
|
||||||
|
const char* method,
|
||||||
|
const char* version,
|
||||||
|
const char* upload_data,
|
||||||
|
size_t* upload_data_size,
|
||||||
|
void** cont_cls);
|
||||||
|
|
||||||
|
|
||||||
|
class InternalServer {
|
||||||
|
public:
|
||||||
|
InternalServer(Library& library,
|
||||||
|
NameMapper* nameMapper,
|
||||||
|
int port,
|
||||||
|
std::string root,
|
||||||
|
int nbThreads,
|
||||||
|
bool verbose,
|
||||||
|
bool withTaskbar,
|
||||||
|
bool withLibraryButton);
|
||||||
|
virtual ~InternalServer() = default;
|
||||||
|
|
||||||
|
int handlerCallback(struct MHD_Connection* connection,
|
||||||
|
const char* url,
|
||||||
|
const char* method,
|
||||||
|
const char* version,
|
||||||
|
const char* upload_data,
|
||||||
|
size_t* upload_data_size,
|
||||||
|
void** cont_cls);
|
||||||
|
bool start();
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Response handle_request(const RequestContext& request);
|
||||||
|
Response build_404(const RequestContext& request, const std::string& zimName);
|
||||||
|
Response build_homepage(const RequestContext& request);
|
||||||
|
Response handle_skin(const RequestContext& request);
|
||||||
|
Response handle_catalog(const RequestContext& request);
|
||||||
|
Response handle_meta(const RequestContext& request);
|
||||||
|
Response handle_search(const RequestContext& request);
|
||||||
|
Response handle_suggest(const RequestContext& request);
|
||||||
|
Response handle_random(const RequestContext& request);
|
||||||
|
Response handle_content(const RequestContext& request);
|
||||||
|
|
||||||
|
kainjow::mustache::data get_default_data();
|
||||||
|
Response get_default_response();
|
||||||
|
|
||||||
|
int m_port;
|
||||||
|
std::string m_root;
|
||||||
|
int m_nbThreads;
|
||||||
|
std::atomic_bool m_verbose;
|
||||||
|
bool m_withTaskbar;
|
||||||
|
bool m_withLibraryButton;
|
||||||
|
struct MHD_Daemon* mp_daemon;
|
||||||
|
|
||||||
|
Library& m_library;
|
||||||
|
NameMapper* mp_nameMapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Server::Server(Library& library, NameMapper* nameMapper) :
|
||||||
|
m_library(library),
|
||||||
|
mp_nameMapper(nameMapper),
|
||||||
|
mp_server(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Server::~Server() = default;
|
||||||
|
|
||||||
|
bool Server::start() {
|
||||||
|
mp_server.reset(new InternalServer(
|
||||||
|
m_library,
|
||||||
|
mp_nameMapper,
|
||||||
|
m_port,
|
||||||
|
m_root,
|
||||||
|
m_nbThreads,
|
||||||
|
m_verbose,
|
||||||
|
m_withTaskbar,
|
||||||
|
m_withLibraryButton));
|
||||||
|
return mp_server->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::stop() {
|
||||||
|
mp_server->stop();
|
||||||
|
mp_server.reset(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
InternalServer::InternalServer(Library& library,
|
||||||
|
NameMapper* nameMapper,
|
||||||
|
int port,
|
||||||
|
std::string root,
|
||||||
|
int nbThreads,
|
||||||
|
bool verbose,
|
||||||
|
bool withTaskbar,
|
||||||
|
bool withLibraryButton) :
|
||||||
|
m_port(port),
|
||||||
|
m_root(root),
|
||||||
|
m_nbThreads(nbThreads),
|
||||||
|
m_verbose(verbose),
|
||||||
|
m_withTaskbar(withTaskbar),
|
||||||
|
m_withLibraryButton(withLibraryButton),
|
||||||
|
mp_daemon(nullptr),
|
||||||
|
m_library(library),
|
||||||
|
mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool InternalServer::start() {
|
||||||
|
// TODO Readd selection of the interface
|
||||||
|
#ifdef _WIN32
|
||||||
|
int flags = MHD_USE_SELECT_INTERNALLY;
|
||||||
|
#else
|
||||||
|
int flags = MHD_USE_POLL_INTERNALLY;
|
||||||
|
#endif
|
||||||
|
if (m_verbose.load())
|
||||||
|
flags |= MHD_USE_DEBUG;
|
||||||
|
|
||||||
|
mp_daemon = MHD_start_daemon(flags,
|
||||||
|
m_port,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
&staticHandlerCallback,
|
||||||
|
this,
|
||||||
|
MHD_OPTION_THREAD_POOL_SIZE, m_nbThreads,
|
||||||
|
MHD_OPTION_END);
|
||||||
|
if (mp_daemon == nullptr) {
|
||||||
|
std::cerr << "Unable to instantiate the HTTP daemon. The port " << m_port
|
||||||
|
<< " is maybe already occupied or need more permissions to be open. "
|
||||||
|
"Please try as root or with a port number higher or equal to 1024."
|
||||||
|
<< std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InternalServer::stop()
|
||||||
|
{
|
||||||
|
MHD_stop_daemon(mp_daemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int staticHandlerCallback(void* cls,
|
||||||
|
struct MHD_Connection* connection,
|
||||||
|
const char* url,
|
||||||
|
const char* method,
|
||||||
|
const char* version,
|
||||||
|
const char* upload_data,
|
||||||
|
size_t* upload_data_size,
|
||||||
|
void** cont_cls)
|
||||||
|
{
|
||||||
|
InternalServer* _this = static_cast<InternalServer*>(cls);
|
||||||
|
|
||||||
|
return _this->handlerCallback(connection,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
version,
|
||||||
|
upload_data,
|
||||||
|
upload_data_size,
|
||||||
|
cont_cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
int InternalServer::handlerCallback(struct MHD_Connection* connection,
|
||||||
|
const char* url,
|
||||||
|
const char* method,
|
||||||
|
const char* version,
|
||||||
|
const char* upload_data,
|
||||||
|
size_t* upload_data_size,
|
||||||
|
void** cont_cls)
|
||||||
|
{
|
||||||
|
auto start_time = std::chrono::steady_clock::now();
|
||||||
|
if (m_verbose.load() ) {
|
||||||
|
printf("======================\n");
|
||||||
|
printf("Requesting : \n");
|
||||||
|
printf("full_url : %s\n", url);
|
||||||
|
}
|
||||||
|
RequestContext request(connection, m_root, url, method, version);
|
||||||
|
|
||||||
|
if (m_verbose.load() ) {
|
||||||
|
request.print_debug_info();
|
||||||
|
}
|
||||||
|
/* Unexpected method */
|
||||||
|
if (request.get_method() != RequestMethod::GET
|
||||||
|
&& request.get_method() != RequestMethod::POST) {
|
||||||
|
printf("Reject request because of unhandled request method.\n");
|
||||||
|
printf("----------------------\n");
|
||||||
|
return MHD_NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = handle_request(request);
|
||||||
|
|
||||||
|
auto ret = response.send(request, connection);
|
||||||
|
auto end_time = std::chrono::steady_clock::now();
|
||||||
|
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("Request time : %fs\n", time_span.count());
|
||||||
|
printf("----------------------\n");
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Response InternalServer::handle_request(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (! request.is_valid_url())
|
||||||
|
return build_404(request, "");
|
||||||
|
|
||||||
|
if (kiwix::startsWith(request.get_url(), "/skin/"))
|
||||||
|
return handle_skin(request);
|
||||||
|
|
||||||
|
if (startsWith(request.get_url(), "/catalog"))
|
||||||
|
return handle_catalog(request);
|
||||||
|
|
||||||
|
if (request.get_url() == "/meta")
|
||||||
|
return handle_meta(request);
|
||||||
|
|
||||||
|
if (request.get_url() == "/search")
|
||||||
|
return handle_search(request);
|
||||||
|
|
||||||
|
if (request.get_url() == "/suggest")
|
||||||
|
return handle_suggest(request);
|
||||||
|
|
||||||
|
if (request.get_url() == "/random")
|
||||||
|
return handle_random(request);
|
||||||
|
|
||||||
|
return handle_content(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
kainjow::mustache::data InternalServer::get_default_data()
|
||||||
|
{
|
||||||
|
kainjow::mustache::data data;
|
||||||
|
data.set("root", m_root);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::get_default_response()
|
||||||
|
{
|
||||||
|
return Response(m_root, m_verbose.load(), m_withTaskbar, m_withLibraryButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Response InternalServer::build_404(const RequestContext& request,
|
||||||
|
const std::string& humanReadableBookId)
|
||||||
|
{
|
||||||
|
kainjow::mustache::data results;
|
||||||
|
results.set("url", request.get_full_url());
|
||||||
|
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_template(RESOURCE::templates::_404_html, results);
|
||||||
|
response.set_mimeType("text/html");
|
||||||
|
response.set_code(MHD_HTTP_NOT_FOUND);
|
||||||
|
response.set_compress(true);
|
||||||
|
response.set_taskbar(humanReadableBookId, "");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::build_homepage(const RequestContext& request)
|
||||||
|
{
|
||||||
|
auto data = get_default_data();
|
||||||
|
|
||||||
|
kainjow::mustache::data books{kainjow::mustache::data::type::list};
|
||||||
|
for (auto& bookId: m_library.getBooksIds()) {
|
||||||
|
auto& currentBook = m_library.getBookById(bookId);
|
||||||
|
if ( currentBook.getPath().empty()
|
||||||
|
|| m_library.getReaderById(bookId) == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
kainjow::mustache::data book;
|
||||||
|
book.set("name", currentBook.getHumanReadableIdFromPath());
|
||||||
|
book.set("title", currentBook.getTitle());
|
||||||
|
book.set("description", currentBook.getDescription());
|
||||||
|
book.set("articleCount", beautifyInteger(currentBook.getArticleCount()));
|
||||||
|
book.set("mediaCount", beautifyInteger(currentBook.getMediaCount()));
|
||||||
|
books.push_back(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.set("books", books);
|
||||||
|
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_template(RESOURCE::templates::index_html, data);
|
||||||
|
response.set_mimeType("text/html; charset=utf-8");
|
||||||
|
response.set_compress(true);
|
||||||
|
response.set_taskbar("", "");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::handle_meta(const RequestContext& request)
|
||||||
|
{
|
||||||
|
std::string humanReadableBookId;
|
||||||
|
std::string bookId;
|
||||||
|
std::string meta_name;
|
||||||
|
try {
|
||||||
|
humanReadableBookId = request.get_argument("content");
|
||||||
|
bookId = mp_nameMapper->getIdForName(humanReadableBookId);
|
||||||
|
meta_name = request.get_argument("name");
|
||||||
|
} catch (const std::out_of_range& e) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto reader = m_library.getReaderById(bookId);
|
||||||
|
if (reader == nullptr) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
std::string mimeType = "text";
|
||||||
|
|
||||||
|
if (meta_name == "title") {
|
||||||
|
content = reader->getTitle();
|
||||||
|
} else if (meta_name == "description") {
|
||||||
|
content = reader->getDescription();
|
||||||
|
} else if (meta_name == "language") {
|
||||||
|
content = reader->getLanguage();
|
||||||
|
} else if (meta_name == "name") {
|
||||||
|
content = reader->getName();
|
||||||
|
} else if (meta_name == "tags") {
|
||||||
|
content = reader->getTags();
|
||||||
|
} else if (meta_name == "date") {
|
||||||
|
content = reader->getDate();
|
||||||
|
} else if (meta_name == "creator") {
|
||||||
|
content = reader->getCreator();
|
||||||
|
} else if (meta_name == "publisher") {
|
||||||
|
content = reader->getPublisher();
|
||||||
|
} else if (meta_name == "favicon") {
|
||||||
|
reader->getFavicon(content, mimeType);
|
||||||
|
} else {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_content(content);
|
||||||
|
response.set_mimeType(mimeType);
|
||||||
|
response.set_compress(false);
|
||||||
|
response.set_cache(true);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::handle_suggest(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_suggest\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
std::string mimeType;
|
||||||
|
unsigned int maxSuggestionCount = 10;
|
||||||
|
unsigned int suggestionCount = 0;
|
||||||
|
std::string suggestion;
|
||||||
|
|
||||||
|
std::string humanReadableBookId;
|
||||||
|
std::string bookId;
|
||||||
|
std::string term;
|
||||||
|
try {
|
||||||
|
humanReadableBookId = request.get_argument("content");
|
||||||
|
bookId = mp_nameMapper->getIdForName(humanReadableBookId);
|
||||||
|
term = request.get_argument("term");
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("Searching suggestions for: \"%s\"\n", term.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto reader = m_library.getReaderById(bookId);
|
||||||
|
kainjow::mustache::data results{kainjow::mustache::data::type::list};
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
if (reader != nullptr) {
|
||||||
|
/* Get the suggestions */
|
||||||
|
reader->searchSuggestionsSmart(term, maxSuggestionCount);
|
||||||
|
while (reader->getNextSuggestion(suggestion)) {
|
||||||
|
kainjow::mustache::data result;
|
||||||
|
result.set("label", suggestion);
|
||||||
|
result.set("value", suggestion);
|
||||||
|
result.set("first", first);
|
||||||
|
first = false;
|
||||||
|
results.push_back(result);
|
||||||
|
suggestionCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Propose the fulltext search if possible */
|
||||||
|
if (reader->hasFulltextIndex()) {
|
||||||
|
kainjow::mustache::data result;
|
||||||
|
result.set("label", "containing '" + term + "'...");
|
||||||
|
result.set("value", term);
|
||||||
|
result.set("first", first);
|
||||||
|
results.push_back(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = get_default_data();
|
||||||
|
data.set("suggestions", results);
|
||||||
|
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_template(RESOURCE::templates::suggestion_json, data);
|
||||||
|
response.set_mimeType("application/json; charset=utf-8");
|
||||||
|
response.set_compress(true);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::handle_skin(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_skin\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = get_default_response();
|
||||||
|
auto resourceName = request.get_url().substr(1);
|
||||||
|
try {
|
||||||
|
response.set_content(getResource(resourceName));
|
||||||
|
} catch (const ResourceNotFound& e) {
|
||||||
|
return build_404(request, "");
|
||||||
|
}
|
||||||
|
response.set_mimeType(getMimeTypeForFile(resourceName));
|
||||||
|
response.set_compress(true);
|
||||||
|
response.set_cache(true);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::handle_search(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_search\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
std::string mimeType;
|
||||||
|
std::string httpRedirection;
|
||||||
|
|
||||||
|
std::string humanReadableBookId;
|
||||||
|
std::string patternString;
|
||||||
|
std::string bookId;
|
||||||
|
try {
|
||||||
|
humanReadableBookId = request.get_argument("content");
|
||||||
|
bookId = mp_nameMapper->getIdForName(humanReadableBookId);
|
||||||
|
} catch (const std::out_of_range&) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
patternString = request.get_argument("pattern");
|
||||||
|
} catch (const std::out_of_range&) {}
|
||||||
|
|
||||||
|
/* Retrive geo search */
|
||||||
|
bool has_geo_query = false;
|
||||||
|
float latitude = 0;
|
||||||
|
float longitude = 0;
|
||||||
|
float distance = 0;
|
||||||
|
try {
|
||||||
|
latitude = request.get_argument<float>("latitude");
|
||||||
|
longitude = request.get_argument<float>("longitude");
|
||||||
|
distance = request.get_argument<float>("distance");
|
||||||
|
has_geo_query = true;
|
||||||
|
} catch(const std::out_of_range&) {}
|
||||||
|
catch(const std::invalid_argument&) {}
|
||||||
|
|
||||||
|
std::shared_ptr<Reader> reader(nullptr);
|
||||||
|
try {
|
||||||
|
reader = m_library.getReaderById(bookId);
|
||||||
|
} catch (const std::out_of_range&) {}
|
||||||
|
|
||||||
|
/* Try first to load directly the article */
|
||||||
|
if (reader != nullptr && !patternString.empty()) {
|
||||||
|
std::string patternCorrespondingUrl;
|
||||||
|
auto variants = reader->getTitleVariants(patternString);
|
||||||
|
auto variantsItr = variants.begin();
|
||||||
|
|
||||||
|
while (patternCorrespondingUrl.empty() && variantsItr != variants.end()) {
|
||||||
|
try {
|
||||||
|
auto entry = reader->getEntryFromTitle(*variantsItr);
|
||||||
|
entry = entry.getFinalEntry();
|
||||||
|
patternCorrespondingUrl = entry.getPath();
|
||||||
|
break;
|
||||||
|
} catch(kiwix::NoEntry& e) {
|
||||||
|
variantsItr++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If article found then redirect directly to it */
|
||||||
|
if (!patternCorrespondingUrl.empty()) {
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_redirection(m_root + "/" + humanReadableBookId + "/" + patternCorrespondingUrl);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the search */
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_mimeType("text/html; charset=utf-8");
|
||||||
|
response.set_taskbar(humanReadableBookId, reader ? reader->getTitle() : "");
|
||||||
|
response.set_compress(true);
|
||||||
|
|
||||||
|
Searcher searcher;
|
||||||
|
if (reader) {
|
||||||
|
searcher.add_reader(reader.get());
|
||||||
|
} else {
|
||||||
|
if (humanReadableBookId.empty()) {
|
||||||
|
for (auto& bookId: m_library.filter(kiwix::Filter().local(true).valid(true))) {
|
||||||
|
auto currentReader = m_library.getReaderById(bookId);
|
||||||
|
if (currentReader) {
|
||||||
|
searcher.add_reader(currentReader.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.set_content("<!DOCTYPE html>\n<html><head><meta content=\"text/html;charset=UTF-8\" http-equiv=\"content-type\" /><title>Fulltext search unavailable</title></head><body><h1>Not Found</h1><p>There is no article with the title <b>\"" + kiwix::encodeDiples(patternString) + "\"</b> and the fulltext search engine is not available for this content.</p></body></html>");
|
||||||
|
response.set_code(MHD_HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patternString.empty() || has_geo_query) {
|
||||||
|
auto start = 0;
|
||||||
|
try {
|
||||||
|
start = request.get_argument<unsigned int>("start");
|
||||||
|
} catch (const std::exception&) {}
|
||||||
|
auto end = 25;
|
||||||
|
try {
|
||||||
|
end = request.get_argument<unsigned int>("end");
|
||||||
|
} catch (const std::exception&) {}
|
||||||
|
if (start>end) {
|
||||||
|
auto tmp = start;
|
||||||
|
start = end;
|
||||||
|
end = tmp;
|
||||||
|
}
|
||||||
|
if (end > start + MAX_SEARCH_LEN) {
|
||||||
|
end = start + MAX_SEARCH_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the results */
|
||||||
|
try {
|
||||||
|
if (patternString.empty()) {
|
||||||
|
searcher.geo_search(latitude, longitude, distance,
|
||||||
|
start, end, m_verbose.load());
|
||||||
|
} else {
|
||||||
|
searcher.search(patternString,
|
||||||
|
start, end, m_verbose.load());
|
||||||
|
}
|
||||||
|
SearchRenderer renderer(&searcher, mp_nameMapper);
|
||||||
|
renderer.setSearchPattern(patternString);
|
||||||
|
renderer.setSearchContent(humanReadableBookId);
|
||||||
|
renderer.setProtocolPrefix(m_root + "/");
|
||||||
|
renderer.setSearchProtocolPrefix(m_root + "/search?");
|
||||||
|
response.set_content(renderer.getHtml());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << e.what() << std::endl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.set_content("<!DOCTYPE html>\n<html><head><meta content=\"text/html;charset=UTF-8\" http-equiv=\"content-type\" /><title>Fulltext search unavailable</title></head><body><h1>Not Found</h1><p>There is no article with the title <b>\"" + kiwix::encodeDiples(patternString) + "\"</b> and the fulltext search engine is not available for this content.</p></body></html>");
|
||||||
|
response.set_code(MHD_HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::handle_random(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_random\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string humanReadableBookId;
|
||||||
|
std::string bookId;
|
||||||
|
try {
|
||||||
|
humanReadableBookId = request.get_argument("content");
|
||||||
|
bookId = mp_nameMapper->getIdForName(humanReadableBookId);
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto reader = m_library.getReaderById(bookId);
|
||||||
|
if (reader == nullptr) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
auto entry = reader->getRandomPage();
|
||||||
|
entry = entry.getFinalEntry();
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_redirection(m_root + "/" + humanReadableBookId + "/" + kiwix::urlEncode(entry.getPath()));
|
||||||
|
return response;
|
||||||
|
} catch(kiwix::NoEntry& e) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::handle_catalog(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_catalog");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string host;
|
||||||
|
std::string url;
|
||||||
|
try {
|
||||||
|
host = request.get_header("Host");
|
||||||
|
url = request.get_url_part(1);
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
return build_404(request, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response = get_default_response();
|
||||||
|
if (url == "searchdescription.xml") {
|
||||||
|
response.set_template(RESOURCE::opensearchdescription_xml, get_default_data());
|
||||||
|
response.set_mimeType("application/opensearchdescription+xml");
|
||||||
|
} else {
|
||||||
|
zim::Uuid uuid;
|
||||||
|
kiwix::OPDSDumper opdsDumper;
|
||||||
|
opdsDumper.setRootLocation(m_root);
|
||||||
|
opdsDumper.setSearchDescriptionUrl("catalog/searchdescription.xml");
|
||||||
|
opdsDumper.setId(kiwix::to_string(uuid));
|
||||||
|
opdsDumper.setLibrary(&m_library);
|
||||||
|
response.set_mimeType("application/atom+xml;profile=opds-catalog;kind=acquisition; charset=utf-8");
|
||||||
|
std::vector<std::string> bookIdsToDump;
|
||||||
|
if (url == "root.xml") {
|
||||||
|
opdsDumper.setTitle("All zims");
|
||||||
|
uuid = zim::Uuid::generate(host);
|
||||||
|
|
||||||
|
bookIdsToDump = m_library.filter(kiwix::Filter().valid(true).local(true).remote(true));
|
||||||
|
} else if (url == "search") {
|
||||||
|
std::string query;
|
||||||
|
std::string language;
|
||||||
|
std::vector<std::string> tags;
|
||||||
|
std::vector<std::string> noTags;
|
||||||
|
size_t count(10);
|
||||||
|
size_t startIndex(0);
|
||||||
|
try {
|
||||||
|
query = request.get_argument("q");
|
||||||
|
} catch (const std::out_of_range&) {}
|
||||||
|
try {
|
||||||
|
language = request.get_argument("lang");
|
||||||
|
} 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 {
|
||||||
|
tags = kiwix::split(request.get_argument("notag"), ";");
|
||||||
|
} catch (...) {}
|
||||||
|
try {
|
||||||
|
noTags = kiwix::split(request.get_argument("notag"), ";");
|
||||||
|
} catch (...) {}
|
||||||
|
opdsDumper.setTitle("Search result for " + query);
|
||||||
|
uuid = zim::Uuid::generate();
|
||||||
|
bookIdsToDump = m_library.filter(
|
||||||
|
kiwix::Filter().valid(true).local(true).remote(true)
|
||||||
|
.query(query)
|
||||||
|
.lang(language)
|
||||||
|
.acceptTags(tags)
|
||||||
|
.rejectTags(noTags)
|
||||||
|
);
|
||||||
|
auto totalResults = bookIdsToDump.size();
|
||||||
|
bookIdsToDump.erase(bookIdsToDump.begin(), bookIdsToDump.begin()+startIndex);
|
||||||
|
if (count>0 && bookIdsToDump.size() > count) {
|
||||||
|
bookIdsToDump.resize(count);
|
||||||
|
}
|
||||||
|
opdsDumper.setOpenSearchInfo(totalResults, startIndex, bookIdsToDump.size());
|
||||||
|
} else {
|
||||||
|
return build_404(request, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.set_content(opdsDumper.dumpOPDSFeed(bookIdsToDump));
|
||||||
|
}
|
||||||
|
|
||||||
|
response.set_compress(true);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response InternalServer::handle_content(const RequestContext& request)
|
||||||
|
{
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("** running handle_content\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string baseUrl;
|
||||||
|
std::string content;
|
||||||
|
std::string mimeType;
|
||||||
|
|
||||||
|
kiwix::Entry entry;
|
||||||
|
|
||||||
|
std::string humanReadableBookId;
|
||||||
|
try {
|
||||||
|
humanReadableBookId = request.get_url_part(0);
|
||||||
|
} catch (const std::out_of_range& e) {
|
||||||
|
return build_homepage(request);
|
||||||
|
}
|
||||||
|
if (humanReadableBookId.size() == 0)
|
||||||
|
return build_homepage(request);
|
||||||
|
|
||||||
|
std::string bookId;
|
||||||
|
try {
|
||||||
|
bookId = mp_nameMapper->getIdForName(humanReadableBookId);
|
||||||
|
} catch (const std::out_of_range& e) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto reader = m_library.getReaderById(bookId);
|
||||||
|
if (reader == nullptr) {
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto urlStr = request.get_url().substr(humanReadableBookId.size()+1);
|
||||||
|
if (urlStr[0] == '/') {
|
||||||
|
urlStr = urlStr.substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
entry = reader->getEntryFromPath(urlStr);
|
||||||
|
if (entry.isRedirect() || urlStr.empty()) {
|
||||||
|
// If urlStr is empty, we want to mainPage.
|
||||||
|
// We must do a redirection to the real page.
|
||||||
|
entry = entry.getFinalEntry();
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_redirection(m_root + "/" + humanReadableBookId + "/" +
|
||||||
|
kiwix::urlEncode(entry.getPath()));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} catch(kiwix::NoEntry& e) {
|
||||||
|
if (m_verbose.load())
|
||||||
|
printf("Failed to find %s\n", urlStr.c_str());
|
||||||
|
|
||||||
|
return build_404(request, humanReadableBookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mimeType = entry.getMimetype();
|
||||||
|
} catch (exception& e) {
|
||||||
|
mimeType = "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_verbose.load()) {
|
||||||
|
printf("Found %s\n", urlStr.c_str());
|
||||||
|
printf("mimeType: %s\n", mimeType.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mimeType.find("text/") != string::npos
|
||||||
|
|| mimeType.find("application/javascript") != string::npos
|
||||||
|
|| mimeType.find("application/json") != string::npos) {
|
||||||
|
zim::Blob raw_content = entry.getBlob();
|
||||||
|
content = string(raw_content.data(), raw_content.size());
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_mimeType(mimeType);
|
||||||
|
|
||||||
|
/* Special rewrite URL in case of ZIM file use intern *asbolute* url like
|
||||||
|
* /A/Kiwix */
|
||||||
|
if (mimeType.find("text/html") != string::npos) {
|
||||||
|
content = replaceRegex(content,
|
||||||
|
"$1$2" + m_root + "/" + humanReadableBookId + "/$3/",
|
||||||
|
"(href|src)(=[\"|\']{0,1})/([A-Z|\\-])/");
|
||||||
|
content = replaceRegex(content,
|
||||||
|
"$1$2" + m_root + "/" + humanReadableBookId + "/$3/",
|
||||||
|
"(@import[ ]+)([\"|\']{0,1})/([A-Z|\\-])/");
|
||||||
|
response.set_taskbar(humanReadableBookId, reader->getTitle());
|
||||||
|
} else if (mimeType.find("text/css") != string::npos) {
|
||||||
|
content = replaceRegex(content,
|
||||||
|
"$1$2" + m_root + "/" + humanReadableBookId + "/$3/",
|
||||||
|
"(url|URL)(\\([\"|\']{0,1})/([A-Z|\\-])/");
|
||||||
|
}
|
||||||
|
response.set_content(content);
|
||||||
|
response.set_compress(true);
|
||||||
|
response.set_cache(true);
|
||||||
|
return response;
|
||||||
|
} else {
|
||||||
|
int range_len;
|
||||||
|
if (request.get_range().second == -1) {
|
||||||
|
range_len = entry.getSize() - request.get_range().first;
|
||||||
|
} else {
|
||||||
|
range_len = request.get_range().second - request.get_range().first;
|
||||||
|
}
|
||||||
|
auto response = get_default_response();
|
||||||
|
response.set_entry(entry);
|
||||||
|
response.set_mimeType(mimeType);
|
||||||
|
response.set_range_first(request.get_range().first);
|
||||||
|
response.set_range_len(range_len);
|
||||||
|
response.set_cache(true);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2009-2016 Emmanuel Engelhart <kelson@kiwix.org>
|
||||||
|
* Copyright 2017 Matthieu Gautier<mgautier@kymeria.fr>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "request_context.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <sstream>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
namespace kiwix {
|
||||||
|
|
||||||
|
static std::atomic_ullong s_requestIndex(0);
|
||||||
|
|
||||||
|
RequestContext::RequestContext(struct MHD_Connection* connection,
|
||||||
|
std::string rootLocation,
|
||||||
|
const std::string& _url,
|
||||||
|
const std::string& method,
|
||||||
|
const std::string& version) :
|
||||||
|
full_url(_url),
|
||||||
|
url(_url),
|
||||||
|
valid_url(true),
|
||||||
|
version(version),
|
||||||
|
requestIndex(s_requestIndex++),
|
||||||
|
acceptEncodingDeflate(false),
|
||||||
|
accept_range(false),
|
||||||
|
range_pair(0, -1)
|
||||||
|
{
|
||||||
|
if (method == "GET") {
|
||||||
|
this->method = RequestMethod::GET;
|
||||||
|
} else if (method == "HEAD") {
|
||||||
|
this->method = RequestMethod::HEAD;
|
||||||
|
} else if (method == "POST") {
|
||||||
|
this->method = RequestMethod::POST;
|
||||||
|
} else if (method == "PUT") {
|
||||||
|
this->method = RequestMethod::PUT;
|
||||||
|
} else if (method == "DELETE") {
|
||||||
|
this->method = RequestMethod::DELETE_;
|
||||||
|
} else if (method == "CONNECT") {
|
||||||
|
this->method = RequestMethod::CONNECT;
|
||||||
|
} else if (method == "OPTIONS") {
|
||||||
|
this->method = RequestMethod::OPTIONS;
|
||||||
|
} else if (method == "TRACE") {
|
||||||
|
this->method = RequestMethod::TRACE;
|
||||||
|
} else if (method == "PATCH") {
|
||||||
|
this->method = RequestMethod::PATCH;
|
||||||
|
} else {
|
||||||
|
this->method = RequestMethod::OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
MHD_get_connection_values(connection, MHD_HEADER_KIND, &RequestContext::fill_header, this);
|
||||||
|
MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &RequestContext::fill_argument, this);
|
||||||
|
|
||||||
|
valid_url = true;
|
||||||
|
if (rootLocation.empty()) {
|
||||||
|
// nothing special to handle.
|
||||||
|
url = full_url;
|
||||||
|
} else {
|
||||||
|
if (full_url == rootLocation) {
|
||||||
|
url = "/";
|
||||||
|
} else if (full_url.size() > rootLocation.size() &&
|
||||||
|
full_url.substr(0, rootLocation.size()+1) == rootLocation + "/") {
|
||||||
|
url = full_url.substr(rootLocation.size());
|
||||||
|
} else {
|
||||||
|
valid_url = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
acceptEncodingDeflate =
|
||||||
|
(get_header(MHD_HTTP_HEADER_ACCEPT_ENCODING).find("deflate") != std::string::npos);
|
||||||
|
} catch (const std::out_of_range&) {}
|
||||||
|
|
||||||
|
/*Check if range is requested. */
|
||||||
|
try {
|
||||||
|
auto range = get_header(MHD_HTTP_HEADER_RANGE);
|
||||||
|
int start = 0;
|
||||||
|
int end = -1;
|
||||||
|
std::istringstream iss(range);
|
||||||
|
char c;
|
||||||
|
|
||||||
|
iss >> start >> c;
|
||||||
|
if (iss.good() && c=='-') {
|
||||||
|
iss >> end;
|
||||||
|
if (iss.fail()) {
|
||||||
|
// Something went wrong will extracting.
|
||||||
|
end = -1;
|
||||||
|
}
|
||||||
|
if (iss.eof()) {
|
||||||
|
accept_range = true;
|
||||||
|
range_pair = std::pair<int, int>(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::out_of_range&) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestContext::~RequestContext()
|
||||||
|
{}
|
||||||
|
|
||||||
|
|
||||||
|
int RequestContext::fill_header(void *__this, enum MHD_ValueKind kind,
|
||||||
|
const char *key, const char *value)
|
||||||
|
{
|
||||||
|
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||||
|
_this->headers[key] = value;
|
||||||
|
return MHD_YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
int RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind,
|
||||||
|
const char *key, const char* value)
|
||||||
|
{
|
||||||
|
RequestContext *_this = static_cast<RequestContext*>(__this);
|
||||||
|
_this->arguments[key] = value == nullptr ? "" : value;
|
||||||
|
return MHD_YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RequestContext::print_debug_info() const {
|
||||||
|
printf("method : %s (%d)\n", method==RequestMethod::GET ? "GET" :
|
||||||
|
method==RequestMethod::POST ? "POST" :
|
||||||
|
"OTHER", (int)method);
|
||||||
|
printf("version : %s\n", version.c_str());
|
||||||
|
printf("request# : %lld\n", requestIndex);
|
||||||
|
printf("headers :\n");
|
||||||
|
for (auto it=headers.begin(); it!=headers.end(); it++) {
|
||||||
|
printf(" - %s : '%s'\n", it->first.c_str(), it->second.c_str());
|
||||||
|
}
|
||||||
|
printf("arguments :\n");
|
||||||
|
for (auto it=arguments.begin(); it!=arguments.end(); it++) {
|
||||||
|
printf(" - %s : '%s'\n", it->first.c_str(), it->second.c_str());
|
||||||
|
}
|
||||||
|
printf("Parsed : \n");
|
||||||
|
printf("url : %s\n", url.c_str());
|
||||||
|
printf("acceptEncodingDeflate : %d\n", acceptEncodingDeflate);
|
||||||
|
printf("has_range : %d\n", accept_range);
|
||||||
|
printf("is_valid_url : %d\n", valid_url);
|
||||||
|
printf(".............\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RequestMethod RequestContext::get_method() const {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RequestContext::get_url() const {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RequestContext::get_url_part(int number) const {
|
||||||
|
size_t start = 1;
|
||||||
|
while(true) {
|
||||||
|
auto found = url.find('/', start);
|
||||||
|
if (number == 0) {
|
||||||
|
if (found == std::string::npos) {
|
||||||
|
return url.substr(start);
|
||||||
|
} else {
|
||||||
|
return url.substr(start, found-start);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (found == std::string::npos) {
|
||||||
|
throw std::out_of_range("No parts");
|
||||||
|
}
|
||||||
|
start = found + 1;
|
||||||
|
number -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RequestContext::get_full_url() const {
|
||||||
|
return full_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RequestContext::is_valid_url() const {
|
||||||
|
return valid_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RequestContext::has_range() const {
|
||||||
|
return accept_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, int> RequestContext::get_range() const {
|
||||||
|
return range_pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
std::string RequestContext::get_argument(const std::string& name) const {
|
||||||
|
return arguments.at(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RequestContext::get_header(const std::string& name) const {
|
||||||
|
return headers.at(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2009-2016 Emmanuel Engelhart <kelson@kiwix.org>
|
||||||
|
* Copyright 2017 Matthieu Gautier<mgautier@kymeria.fr>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef REQUEST_CONTEXT_H
|
||||||
|
#define REQUEST_CONTEXT_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <map>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <microhttpd.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace kiwix {
|
||||||
|
|
||||||
|
enum class RequestMethod {
|
||||||
|
GET,
|
||||||
|
HEAD,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE_,
|
||||||
|
CONNECT,
|
||||||
|
OPTIONS,
|
||||||
|
TRACE,
|
||||||
|
PATCH,
|
||||||
|
OTHER
|
||||||
|
};
|
||||||
|
|
||||||
|
class KeyError : public std::runtime_error {};
|
||||||
|
class IndexError: public std::runtime_error {};
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContext {
|
||||||
|
public:
|
||||||
|
RequestContext(struct MHD_Connection* connection,
|
||||||
|
std::string rootLocation,
|
||||||
|
const std::string& url,
|
||||||
|
const std::string& method,
|
||||||
|
const std::string& version);
|
||||||
|
~RequestContext();
|
||||||
|
|
||||||
|
void print_debug_info() const;
|
||||||
|
|
||||||
|
bool is_valid_url() const;
|
||||||
|
|
||||||
|
std::string get_header(const std::string& name) const;
|
||||||
|
template<typename T=std::string>
|
||||||
|
T get_argument(const std::string& name) const {
|
||||||
|
std::istringstream stream(arguments.at(name));
|
||||||
|
T v;
|
||||||
|
stream >> v;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RequestMethod get_method() const;
|
||||||
|
std::string get_url() const;
|
||||||
|
std::string get_url_part(int part) const;
|
||||||
|
std::string get_full_url() const;
|
||||||
|
|
||||||
|
bool has_range() const;
|
||||||
|
std::pair<int, int> get_range() const;
|
||||||
|
|
||||||
|
bool can_compress() const { return acceptEncodingDeflate; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string full_url;
|
||||||
|
std::string url;
|
||||||
|
bool valid_url;
|
||||||
|
RequestMethod method;
|
||||||
|
std::string version;
|
||||||
|
unsigned long long requestIndex;
|
||||||
|
|
||||||
|
bool acceptEncodingDeflate;
|
||||||
|
|
||||||
|
bool accept_range;
|
||||||
|
std::pair<int, int> range_pair;
|
||||||
|
std::map<std::string, std::string> headers;
|
||||||
|
std::map<std::string, std::string> arguments;
|
||||||
|
|
||||||
|
static int fill_header(void *, enum MHD_ValueKind, const char*, const char*);
|
||||||
|
static int fill_argument(void *, enum MHD_ValueKind, const char*, const char*);
|
||||||
|
};
|
||||||
|
|
||||||
|
template<> std::string RequestContext::get_argument(const std::string& name) const;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //REQUEST_CONTEXT_H
|
|
@ -0,0 +1,250 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#include "response.h"
|
||||||
|
#include "request_context.h"
|
||||||
|
#include "kiwixlib-resources.h"
|
||||||
|
|
||||||
|
#include "tools/regexTools.h"
|
||||||
|
#include "tools/stringTools.h"
|
||||||
|
|
||||||
|
#include "string.h"
|
||||||
|
#include <mustache.hpp>
|
||||||
|
#include <zlib.h>
|
||||||
|
|
||||||
|
|
||||||
|
#define KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE 100
|
||||||
|
|
||||||
|
namespace kiwix {
|
||||||
|
|
||||||
|
Response::Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton)
|
||||||
|
: m_verbose(verbose),
|
||||||
|
m_root(root),
|
||||||
|
m_content(""),
|
||||||
|
m_mimeType(""),
|
||||||
|
m_returnCode(MHD_HTTP_OK),
|
||||||
|
m_withTaskbar(withTaskbar),
|
||||||
|
m_withLibraryButton(withLibraryButton),
|
||||||
|
m_useCache(false),
|
||||||
|
m_addTaskbar(false),
|
||||||
|
m_bookName(""),
|
||||||
|
m_startRange(0),
|
||||||
|
m_lenRange(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int print_key_value (void *cls, enum MHD_ValueKind kind,
|
||||||
|
const char *key, const char *value)
|
||||||
|
{
|
||||||
|
printf (" - %s: '%s'\n", key, value);
|
||||||
|
return MHD_YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct RunningResponse {
|
||||||
|
kiwix::Entry entry;
|
||||||
|
int range_start;
|
||||||
|
|
||||||
|
RunningResponse(kiwix::Entry entry,
|
||||||
|
int range_start) :
|
||||||
|
entry(entry),
|
||||||
|
range_start(range_start)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
static ssize_t callback_reader_from_entry(void* cls,
|
||||||
|
uint64_t pos,
|
||||||
|
char* buf,
|
||||||
|
size_t max)
|
||||||
|
{
|
||||||
|
RunningResponse* response = static_cast<RunningResponse*>(cls);
|
||||||
|
|
||||||
|
size_t max_size_to_set = min<size_t>(
|
||||||
|
max,
|
||||||
|
response->entry.getSize() - pos - response->range_start);
|
||||||
|
|
||||||
|
if (max_size_to_set <= 0) {
|
||||||
|
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
zim::Blob blob = response->entry.getBlob(response->range_start+pos, max_size_to_set);
|
||||||
|
memcpy(buf, blob.data(), max_size_to_set);
|
||||||
|
return max_size_to_set;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void callback_free_response(void* cls)
|
||||||
|
{
|
||||||
|
RunningResponse* response = static_cast<RunningResponse*>(cls);
|
||||||
|
delete response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void print_response_info(int retCode, MHD_Response* response)
|
||||||
|
{
|
||||||
|
printf("Response :\n");
|
||||||
|
printf("httpResponseCode : %d\n", retCode);
|
||||||
|
printf("headers :\n");
|
||||||
|
MHD_get_response_headers(response, print_key_value, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Response::introduce_taskbar()
|
||||||
|
{
|
||||||
|
if (! m_withTaskbar)
|
||||||
|
// Taskbar is globally disabled.
|
||||||
|
return;
|
||||||
|
kainjow::mustache::data data;
|
||||||
|
data.set("root", m_root);
|
||||||
|
data.set("content", m_bookName);
|
||||||
|
data.set("hascontent", !m_bookName.empty());
|
||||||
|
data.set("title", m_bookTitle);
|
||||||
|
data.set("withlibrarybutton", m_withLibraryButton);
|
||||||
|
auto head_content = render_template(RESOURCE::templates::head_part_html, data);
|
||||||
|
m_content = appendToFirstOccurence(
|
||||||
|
m_content,
|
||||||
|
"<head>",
|
||||||
|
head_content);
|
||||||
|
|
||||||
|
auto taskbar_part = render_template(RESOURCE::templates::taskbar_part_html, data);
|
||||||
|
m_content = appendToFirstOccurence(
|
||||||
|
m_content,
|
||||||
|
"<body[^>]*>",
|
||||||
|
taskbar_part);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int Response::send(const RequestContext& request, MHD_Connection* connection)
|
||||||
|
{
|
||||||
|
MHD_Response* response = nullptr;
|
||||||
|
switch (m_mode) {
|
||||||
|
case ResponseMode::RAW_CONTENT : {
|
||||||
|
if (m_addTaskbar) {
|
||||||
|
introduce_taskbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldCompress = m_compress && request.can_compress();
|
||||||
|
shouldCompress &= m_mimeType.find("text/") != string::npos
|
||||||
|
|| m_mimeType.find("application/javascript") != string::npos
|
||||||
|
|| m_mimeType.find("application/json") != string::npos;
|
||||||
|
|
||||||
|
shouldCompress &= (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE);
|
||||||
|
|
||||||
|
if (shouldCompress) {
|
||||||
|
std::vector<Bytef> compr_buffer(compressBound(m_content.size()));
|
||||||
|
uLongf comprLen = compr_buffer.capacity();
|
||||||
|
int err = compress(&compr_buffer[0],
|
||||||
|
&comprLen,
|
||||||
|
(const Bytef*)(m_content.data()),
|
||||||
|
m_content.size());
|
||||||
|
if (err == Z_OK && comprLen > 2 && comprLen < (m_content.size() + 2)) {
|
||||||
|
/* /!\ Internet Explorer has a bug with deflate compression.
|
||||||
|
It can not handle the first two bytes (compression headers)
|
||||||
|
We need to chunk them off (move the content 2bytes)
|
||||||
|
It has no incidence on other browsers
|
||||||
|
See http://www.subbu.org/blog/2008/03/ie7-deflate-or-not and comments */
|
||||||
|
m_content = string((char*)&compr_buffer[2], comprLen - 2);
|
||||||
|
} else {
|
||||||
|
shouldCompress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = MHD_create_response_from_buffer(
|
||||||
|
m_content.size(), const_cast<char*>(m_content.data()), MHD_RESPMEM_MUST_COPY);
|
||||||
|
|
||||||
|
if (shouldCompress) {
|
||||||
|
MHD_add_response_header(
|
||||||
|
response, MHD_HTTP_HEADER_VARY, "Accept-Encoding");
|
||||||
|
MHD_add_response_header(
|
||||||
|
response, MHD_HTTP_HEADER_CONTENT_ENCODING, "deflate");
|
||||||
|
}
|
||||||
|
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ResponseMode::REDIRECTION : {
|
||||||
|
response = MHD_create_response_from_buffer(0, nullptr, MHD_RESPMEM_MUST_COPY);
|
||||||
|
MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION, m_content.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ResponseMode::ENTRY : {
|
||||||
|
response = MHD_create_response_from_callback(m_entry.getSize(),
|
||||||
|
16384,
|
||||||
|
callback_reader_from_entry,
|
||||||
|
new RunningResponse(m_entry, m_startRange),
|
||||||
|
callback_free_response);
|
||||||
|
MHD_add_response_header(response,
|
||||||
|
MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType.c_str());
|
||||||
|
MHD_add_response_header(response, MHD_HTTP_HEADER_ACCEPT_RANGES, "bytes");
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "bytes " << m_startRange << "-" << m_startRange + m_lenRange - 1
|
||||||
|
<< "/" << m_entry.getSize();
|
||||||
|
|
||||||
|
MHD_add_response_header(response,
|
||||||
|
MHD_HTTP_HEADER_CONTENT_RANGE, oss.str().c_str());
|
||||||
|
|
||||||
|
MHD_add_response_header(response,
|
||||||
|
MHD_HTTP_HEADER_CONTENT_LENGTH, kiwix::to_string(m_lenRange).c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MHD_add_response_header(response, "Access-Control-Allow-Origin", "*");
|
||||||
|
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
|
||||||
|
m_useCache ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
|
||||||
|
|
||||||
|
if (m_returnCode == MHD_HTTP_OK && request.has_range())
|
||||||
|
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
|
||||||
|
|
||||||
|
if (m_verbose)
|
||||||
|
print_response_info(m_returnCode, response);
|
||||||
|
|
||||||
|
auto ret = MHD_queue_response(connection, m_returnCode, response);
|
||||||
|
MHD_destroy_response(response);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Response::set_template(const std::string& template_str, kainjow::mustache::data data) {
|
||||||
|
set_content(render_template(template_str, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Response::set_content(const std::string& content) {
|
||||||
|
m_content = content;
|
||||||
|
m_mode = ResponseMode::RAW_CONTENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Response::set_redirection(const std::string& url) {
|
||||||
|
m_content = url;
|
||||||
|
m_mode = ResponseMode::REDIRECTION;
|
||||||
|
m_returnCode = MHD_HTTP_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Response::set_entry(const Entry& entry) {
|
||||||
|
m_entry = entry;
|
||||||
|
m_mode = ResponseMode::ENTRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Response::set_taskbar(const std::string& bookName, const std::string& bookTitle)
|
||||||
|
{
|
||||||
|
m_addTaskbar = true;
|
||||||
|
m_bookName = bookName;
|
||||||
|
m_bookTitle = bookTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 Matthieu Gautier<mgautier@kymeria.fr>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 3 of the License, or
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||||
|
* MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef KIWIXLIB_SERVER_RESPONSE_H
|
||||||
|
#define KIWIXLIB_SERVER_RESPONSE_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <mustache.hpp>
|
||||||
|
#include "entry.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <microhttpd.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace kiwix {
|
||||||
|
|
||||||
|
enum class ResponseMode {
|
||||||
|
RAW_CONTENT,
|
||||||
|
REDIRECTION,
|
||||||
|
ENTRY
|
||||||
|
};
|
||||||
|
|
||||||
|
class RequestContext;
|
||||||
|
|
||||||
|
class Response {
|
||||||
|
public:
|
||||||
|
Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton);
|
||||||
|
~Response() = default;
|
||||||
|
|
||||||
|
int send(const RequestContext& request, MHD_Connection* connection);
|
||||||
|
|
||||||
|
void set_template(const std::string& template_str, kainjow::mustache::data data);
|
||||||
|
void set_content(const std::string& content);
|
||||||
|
void set_redirection(const std::string& url);
|
||||||
|
void set_entry(const Entry& entry);
|
||||||
|
|
||||||
|
|
||||||
|
void set_mimeType(const std::string& mimeType) { m_mimeType = mimeType; }
|
||||||
|
void set_code(int code) { m_returnCode = code; }
|
||||||
|
void set_cache(bool cache) { m_useCache = cache; }
|
||||||
|
void set_compress(bool compress) { m_compress = compress; }
|
||||||
|
void set_taskbar(const std::string& bookName, const std::string& bookTitle);
|
||||||
|
void set_range_first(uint64_t start) { m_startRange = start; }
|
||||||
|
void set_range_len(uint64_t len) { m_lenRange = len; }
|
||||||
|
|
||||||
|
void introduce_taskbar();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_verbose;
|
||||||
|
ResponseMode m_mode;
|
||||||
|
std::string m_root;
|
||||||
|
std::string m_content;
|
||||||
|
Entry m_entry;
|
||||||
|
std::string m_mimeType;
|
||||||
|
int m_returnCode;
|
||||||
|
bool m_withTaskbar;
|
||||||
|
bool m_withLibraryButton;
|
||||||
|
bool m_useCache;
|
||||||
|
bool m_compress;
|
||||||
|
bool m_addTaskbar;
|
||||||
|
std::string m_bookName;
|
||||||
|
std::string m_bookTitle;
|
||||||
|
uint64_t m_startRange;
|
||||||
|
uint64_t m_lenRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif //KIWIXLIB_SERVER_RESPONSE_H
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<id>{{id}}</id>
|
||||||
|
<link rel="self"
|
||||||
|
href="{{self_url}}"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<link rel="start"
|
||||||
|
href="{{start_url}}"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<updated>{{updated}}</updated>
|
||||||
|
<author>
|
||||||
|
<name>{{author_name}}</name>
|
||||||
|
<uri>{{author_uri}}</uri>
|
||||||
|
</author>
|
||||||
|
|
||||||
|
{{#entries}}
|
||||||
|
<entry>
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="{{href}}"
|
||||||
|
type="application/atom+xml;profile=opds-catalog;kind=acquisition"/>
|
||||||
|
<updated>{{updated}}</updated>
|
||||||
|
<id>{{id}}</id>
|
||||||
|
</entry>
|
||||||
|
{{/entries}}
|
||||||
|
</feed>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?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"
|
||||||
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
indexOffset="0"
|
||||||
|
template="/{{root}}/catalog/search?q={searchTerms}&lang={language}&count={count}&start={startIndex}"/>
|
||||||
|
</OpenSearchDescription>
|
|
@ -1 +1,29 @@
|
||||||
|
skin/jquery-ui/jquery-ui.structure.min.css
|
||||||
|
skin/jquery-ui/jquery-ui.min.js
|
||||||
|
skin/jquery-ui/external/jquery/jquery.js
|
||||||
|
skin/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png
|
||||||
|
skin/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png
|
||||||
|
skin/jquery-ui/images/ui-icons_222222_256x240.png
|
||||||
|
skin/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png
|
||||||
|
skin/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png
|
||||||
|
skin/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png
|
||||||
|
skin/jquery-ui/images/ui-icons_2e83ff_256x240.png
|
||||||
|
skin/jquery-ui/images/ui-icons_cd0a0a_256x240.png
|
||||||
|
skin/jquery-ui/images/ui-icons_888888_256x240.png
|
||||||
|
skin/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png
|
||||||
|
skin/jquery-ui/images/animated-overlay.gif
|
||||||
|
skin/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png
|
||||||
|
skin/jquery-ui/images/ui-icons_454545_256x240.png
|
||||||
|
skin/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png
|
||||||
|
skin/jquery-ui/jquery-ui.theme.min.css
|
||||||
|
skin/jquery-ui/jquery-ui.min.css
|
||||||
|
skin/caret.png
|
||||||
|
skin/taskbar.js
|
||||||
|
skin/taskbar.css
|
||||||
templates/search_result.html
|
templates/search_result.html
|
||||||
|
templates/404.html
|
||||||
|
templates/index.html
|
||||||
|
templates/suggestion.json
|
||||||
|
opensearchdescription.xml
|
||||||
|
templates/head_part.html
|
||||||
|
templates/taskbar_part.html
|
||||||
|
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 212 B |
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 335 B |
After Width: | Height: | Size: 207 B |
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 332 B |
After Width: | Height: | Size: 280 B |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 4.4 KiB |
|
@ -0,0 +1,186 @@
|
||||||
|
#kiwixtoolbar {
|
||||||
|
position: fixed;
|
||||||
|
padding: .5em;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background-position-y: 0px;
|
||||||
|
transition: 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kiwixtoolbar>a {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kiwixfooter {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.height_separator {
|
||||||
|
height: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwixsearch {
|
||||||
|
position: relative;
|
||||||
|
height: 26px;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix_searchform {
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kiwix_serve_taskbar_home_button button {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix .kiwix_centered {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kiwix_button_show_toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kiwix_button_show_toggle:checked~label~.kiwix_button_cont,
|
||||||
|
#kiwix_button_show_toggle:checked~label~.kiwix_button_cont>a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kiwix_button_show_toggle:not(:checked)~label~.kiwix_button_cont {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for="kiwix_button_show_toggle"] {
|
||||||
|
display: inline-block;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for="kiwix_button_show_toggle"] img {
|
||||||
|
transition: 0.1s;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#kiwix_button_show_toggle:checked~label img {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for="kiwix_button_show_toggle"],
|
||||||
|
.kiwix_button_cont {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix .kiwix_searchform {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix #kiwixtoolbar button,
|
||||||
|
.kiwix #kiwixtoolbar input[type="submit"] {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
height: 26px !important;
|
||||||
|
line-height: 20px !important;
|
||||||
|
margin-right: 5px !important;
|
||||||
|
padding: 2px 6px !important;
|
||||||
|
border: 1px solid #999 !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
background-color: #ededed !important;
|
||||||
|
font-weight: normal !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix #kiwixtoolbar #kiwixsearchform input[type='text'] {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
width: 100%;
|
||||||
|
height: 26px !important;
|
||||||
|
line-height: 20px !important;
|
||||||
|
border: 1px solid #999 !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
padding: 2px 2px 2px 27px !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for=kiwixsearchbox] {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
left: 5px;
|
||||||
|
font-size: 90%;
|
||||||
|
line-height: 26px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding-top: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Try to fix buggy stuff in jquery-ui autocomplete */
|
||||||
|
#ui-id-1,
|
||||||
|
.ui-autocomplete {
|
||||||
|
background: white !important;
|
||||||
|
border: solid 1px grey !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.ui-state-focus {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(min-width:420px) {
|
||||||
|
.kiwix_button_cont {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix_button_cont>a {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for="kiwix_button_show_toggle"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 645px) {
|
||||||
|
|
||||||
|
#kiwix_button_show_toggle~label~.kiwix_button_cont.searching {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
label[for="kiwix_button_show_toggle"].searching {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix_searchform.full_width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwixsearch {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kiwix_searchform {
|
||||||
|
width: 36%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.height_separator {
|
||||||
|
height: 6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width:415px) {
|
||||||
|
.kiwix_searchform {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
if ($(window).width() < 520) {
|
||||||
|
var didScroll;
|
||||||
|
var lastScrollTop = 0;
|
||||||
|
var delta = 5;
|
||||||
|
// on scroll, let the interval function know the user has scrolled
|
||||||
|
$(window).scroll(function (event) {
|
||||||
|
didScroll = true;
|
||||||
|
});
|
||||||
|
// run hasScrolled() and reset didScroll status
|
||||||
|
setInterval(function () {
|
||||||
|
if (didScroll) {
|
||||||
|
hasScrolled();
|
||||||
|
didScroll = false;
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
function hasScrolled() {
|
||||||
|
var st = $(this).scrollTop();
|
||||||
|
|
||||||
|
// Make sure they scroll more than delta
|
||||||
|
if (Math.abs(lastScrollTop - st) <= delta)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If they scrolled down and are past the navbar, add class .nav-up.
|
||||||
|
// This is necessary so you never see what is "behind" the navbar.
|
||||||
|
if (st > lastScrollTop) {
|
||||||
|
// Scroll Down
|
||||||
|
$('#kiwixtoolbar').css({ top: '-100%' });
|
||||||
|
} else {
|
||||||
|
// Scroll Up
|
||||||
|
$('#kiwixtoolbar').css({ top: '0' });
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollTop = st;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#kiwixsearchbox').on({
|
||||||
|
focus: function () {
|
||||||
|
$('.kiwix_searchform').addClass('full_width');
|
||||||
|
$('label[for="kiwix_button_show_toggle"], .kiwix_button_cont').addClass('searching');
|
||||||
|
},
|
||||||
|
blur: function () {
|
||||||
|
$('.kiwix_searchform').removeClass('full_width');
|
||||||
|
$('label[for="kiwix_button_show_toggle"], .kiwix_button_cont').removeClass('searching');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})(jQuery);
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content="text/html;charset=UTF-8" http-equiv="content-type" />
|
||||||
|
<title>Content not found</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Not Found</h1>
|
||||||
|
<p>
|
||||||
|
The requested URL "{{url}}" was not found on this server.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.min.css" rel="Stylesheet" />
|
||||||
|
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.theme.min.css" rel="Stylesheet" />
|
||||||
|
<link type="text/css" href="{{root}}/skin/taskbar.css" rel="Stylesheet" />
|
||||||
|
<script type="text/javascript" src="{{root}}/skin/jquery-ui/external/jquery/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="{{root}}/skin/jquery-ui/jquery-ui.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var jk = jQuery.noConflict();
|
||||||
|
jk(function() {
|
||||||
|
jk( "#kiwixsearchbox" ).autocomplete({
|
||||||
|
|
||||||
|
source: "{{root}}/suggest?content={{#urlencoded}}{{{content}}}{{/urlencoded}}",
|
||||||
|
dataType: "json",
|
||||||
|
cache: false,
|
||||||
|
|
||||||
|
select: function(event, ui) {
|
||||||
|
jk( "#kiwixsearchbox" ).val(ui.item.value);
|
||||||
|
jk( "#kiwixsearchform" ).submit();
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* cybook hack */
|
||||||
|
if (navigator.userAgent.indexOf("bookeen/cybook") != -1) {
|
||||||
|
jk("html").addClass("cybook");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="{{root}}/skin/taskbar.js" async></script>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Welcome to Kiwix Server</title>
|
||||||
|
<script type="text/javascript" src="{{root}}/skin/jquery-ui/external/jquery/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="{{root}}/skin/jquery-ui/jquery-ui.min.js"></script>
|
||||||
|
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.min.css" rel="Stylesheet" />
|
||||||
|
<link type="text/css" href="{{root}}/skin/jquery-ui/jquery-ui.theme.min.css" rel="Stylesheet" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background:
|
||||||
|
radial-gradient(#EEEEEE 15%, transparent 16%) 0 0,
|
||||||
|
radial-gradient(#EEEEEE 15%, transparent 16%) 8px 8px,
|
||||||
|
radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 0 1px,
|
||||||
|
radial-gradient(rgba(255,255,255,.1) 15%, transparent 20%) 8px 9px;
|
||||||
|
background-color:#E8E8E8;
|
||||||
|
background-size:16px 16px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
.book__list { text-align: center; }
|
||||||
|
.book {
|
||||||
|
display: inline-block; vertical-align: bottom; margin: 8px; padding: 12px 15px; width: 300px;
|
||||||
|
border: 1px solid #ccc; border-radius: 8px;
|
||||||
|
text-align: left; color: #000; font-family: sans-serif; font-size: 13px;
|
||||||
|
background-color:#F1F1F1;
|
||||||
|
box-shadow: 2px 2px 5px 0px #ccc;
|
||||||
|
}
|
||||||
|
.book:hover { background-color: #F9F9F9; box-shadow: none;}
|
||||||
|
.book__background { background-repeat: no-repeat; background-size: auto; background-position: top right; }
|
||||||
|
.book__title {
|
||||||
|
padding: 0 55px 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
font-size: 18px; color: #0645ad;
|
||||||
|
}
|
||||||
|
.book__description {
|
||||||
|
padding: 5px 55px 5px 0px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.book__info { line-height: 18px; color: #777; font-weight: bold; font-size: 13px; }
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript" src="{{root}}/skin/taskbar.js" async></script>
|
||||||
|
</head>
|
||||||
|
<body class="kiwix">
|
||||||
|
|
||||||
|
<div class="kiwix">
|
||||||
|
<div class='book__list'>
|
||||||
|
{{#books}}
|
||||||
|
<a href="{{root}}/{{name}}">
|
||||||
|
<div class='book'>
|
||||||
|
<div class='book__background' style="background-image: url('{{root}}/meta?content={{#urlencoded}}{{{name}}}{{/urlencoded}}&name=favicon');">
|
||||||
|
<div class='book__title' title='{{title}}'>{{title}}</div>
|
||||||
|
<div class='book__description' title='{{description}}'>{{description}}</div>
|
||||||
|
<div class='book__info'>{{articleCount}} articles, {{mediaCount}} medias</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{{/books}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kiwixfooter">
|
||||||
|
Powered by <a href="https://kiwix.org">Kiwix</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,7 @@
|
||||||
|
[
|
||||||
|
{{#suggestions}}{{^first}},{{/first}}
|
||||||
|
{
|
||||||
|
"value" : "{{value}}",
|
||||||
|
"label" : "{{& label}}"
|
||||||
|
}{{/suggestions}}
|
||||||
|
]
|
|
@ -0,0 +1,26 @@
|
||||||
|
<span class="kiwix">
|
||||||
|
<span id="kiwixtoolbar" class="ui-widget-header">
|
||||||
|
<div class="kiwix_centered">
|
||||||
|
<div class="kiwix_searchform">
|
||||||
|
<form class="kiwixsearch" method="GET" action="{{root}}/search" id="kiwixsearchform">
|
||||||
|
{{#hascontent}}<input type="hidden" name="content" value="{{content}}" />{{/hascontent}}
|
||||||
|
<label for="kiwixsearchbox">🔍</label>
|
||||||
|
<input autocomplete="off" class="ui-autocomplete-input" id="kiwixsearchbox" name="pattern" type="text">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{#hascontent}}
|
||||||
|
<input type="checkbox" id="kiwix_button_show_toggle">
|
||||||
|
<label for="kiwix_button_show_toggle"><img src="{{root}}/skin/caret.png" alt=""></label>
|
||||||
|
<div class="kiwix_button_cont">
|
||||||
|
{{#withlibrarybutton}}
|
||||||
|
<a id="kiwix_serve_taskbar_library_button" href="{{root}}/"><button>🏠</button></a>
|
||||||
|
{{/withlibrarybutton}}
|
||||||
|
<a id="kiwix_serve_taskbar_home_button" href="{{root}}/{{content}}/"><button>{{title}}</button></a>
|
||||||
|
<a id="kiwix_serve_taskbar_random_button"
|
||||||
|
href="{{root}}/random?content={{#urlencoded}}{{{content}}}{{/urlencoded}}"><button>🎲</button></a>
|
||||||
|
</div>
|
||||||
|
{{/hascontent}}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div style="display: block; height: 5em;"></div>
|