diff --git a/include/server.h b/include/server.h index 52f014543..b38ad8c04 100644 --- a/include/server.h +++ b/include/server.h @@ -56,7 +56,9 @@ namespace kiwix void setNbThreads(int threads) { m_nbThreads = threads; } void setVerbose(bool verbose) { m_verbose = verbose; } void setTaskbar(bool withTaskbar, bool withLibraryButton) - { m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; } + { m_withTaskbar = withTaskbar; m_withLibraryButton = withLibraryButton; } + void setBlockExternalLinks(bool blockExternalLinks) + { m_blockExternalLinks = blockExternalLinks; } protected: Library* mp_library; @@ -68,6 +70,7 @@ namespace kiwix bool m_verbose = false; bool m_withTaskbar = true; bool m_withLibraryButton = true; + bool m_blockExternalLinks = false; std::unique_ptr mp_server; }; } diff --git a/src/server.cpp b/src/server.cpp index 4af15b92b..511a8936d 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -95,7 +95,8 @@ class InternalServer { int nbThreads, bool verbose, bool withTaskbar, - bool withLibraryButton); + bool withLibraryButton, + bool blockExternalLinks); virtual ~InternalServer() = default; int handlerCallback(struct MHD_Connection* connection, @@ -119,6 +120,7 @@ class InternalServer { Response handle_search(const RequestContext& request); Response handle_suggest(const RequestContext& request); Response handle_random(const RequestContext& request); + Response handle_captured_external(const RequestContext& request); Response handle_content(const RequestContext& request); kainjow::mustache::data get_default_data(); @@ -131,6 +133,7 @@ class InternalServer { std::atomic_bool m_verbose; bool m_withTaskbar; bool m_withLibraryButton; + bool m_blockExternalLinks; struct MHD_Daemon* mp_daemon; Library* mp_library; @@ -157,7 +160,8 @@ bool Server::start() { m_nbThreads, m_verbose, m_withTaskbar, - m_withLibraryButton)); + m_withLibraryButton, + m_blockExternalLinks)); return mp_server->start(); } @@ -186,7 +190,8 @@ InternalServer::InternalServer(Library* library, int nbThreads, bool verbose, bool withTaskbar, - bool withLibraryButton) : + bool withLibraryButton, + bool blockExternalLinks) : m_addr(addr), m_port(port), m_root(root), @@ -194,6 +199,7 @@ InternalServer::InternalServer(Library* library, m_verbose(verbose), m_withTaskbar(withTaskbar), m_withLibraryButton(withLibraryButton), + m_blockExternalLinks(blockExternalLinks), mp_daemon(nullptr), mp_library(library), mp_nameMapper(nameMapper ? nameMapper : &defaultNameMapper) @@ -340,6 +346,9 @@ Response InternalServer::handle_request(const RequestContext& request) if (request.get_url() == "/random") return handle_random(request); + if (request.get_url() == "/catch/external") + return handle_captured_external(request); + return handle_content(request); } catch (std::exception& e) { fprintf(stderr, "===== Unhandled error : %s\n", e.what()); @@ -359,7 +368,7 @@ kainjow::mustache::data InternalServer::get_default_data() Response InternalServer::get_default_response() { - return Response(m_root, m_verbose.load(), m_withTaskbar, m_withLibraryButton); + return Response(m_root, m_verbose.load(), m_withTaskbar, m_withLibraryButton, m_blockExternalLinks); } @@ -383,7 +392,7 @@ Response InternalServer::build_500(const std::string& msg) { kainjow::mustache::data data; data.set("error", msg); - Response response(m_root, true, false, false); + Response response(m_root, true, false, false, false); response.set_template(RESOURCE::templates::_500_html, data); response.set_mimeType("text/html"); response.set_code(MHD_HTTP_INTERNAL_SERVER_ERROR); @@ -711,6 +720,26 @@ Response InternalServer::handle_random(const RequestContext& request) } } +Response InternalServer::handle_captured_external(const RequestContext& request) +{ + std::string source = ""; + try { + source = kiwix::urlDecode(request.get_argument("source")); + } catch (const std::out_of_range& e) {} + + if (source.empty()) + return build_404(request, ""); + + auto data = get_default_data(); + data.set("source", source); + auto response = get_default_response(); + response.set_template(RESOURCE::templates::captured_external_html, data); + response.set_mimeType("text/html; charset=utf-8"); + response.set_compress(true); + response.set_taskbar("", ""); + return response; +} + Response InternalServer::handle_catalog(const RequestContext& request) { if (m_verbose.load()) { diff --git a/src/server/response.cpp b/src/server/response.cpp index c55fd63bb..fdf1d80f6 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -17,7 +17,7 @@ namespace kiwix { -Response::Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton) +Response::Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks) : m_verbose(verbose), m_root(root), m_content(""), @@ -25,6 +25,7 @@ Response::Response(const std::string& root, bool verbose, bool withTaskbar, bool m_returnCode(MHD_HTTP_OK), m_withTaskbar(withTaskbar), m_withLibraryButton(withLibraryButton), + m_blockExternalLinks(blockExternalLinks), m_useCache(false), m_addTaskbar(false), m_bookName(""), @@ -116,7 +117,7 @@ void Response::introduce_taskbar() auto head_content = render_template(RESOURCE::templates::head_part_html, data); m_content = appendToFirstOccurence( m_content, - "", + "\n", head_content); auto taskbar_part = render_template(RESOURCE::templates::taskbar_part_html, data); @@ -127,6 +128,17 @@ void Response::introduce_taskbar() } +void Response::inject_externallinks_blocker() +{ + kainjow::mustache::data data; + data.set("root", m_root); + auto script_tag = render_template(RESOURCE::templates::external_blocker_part_html, data); + m_content = appendToFirstOccurence( + m_content, + "\n", + script_tag); +} + int Response::send(const RequestContext& request, MHD_Connection* connection) { @@ -136,6 +148,9 @@ int Response::send(const RequestContext& request, MHD_Connection* connection) if (m_addTaskbar) { introduce_taskbar(); } + if ( m_blockExternalLinks ) { + inject_externallinks_blocker(); + } bool shouldCompress = m_compress && request.can_compress(); shouldCompress &= m_mimeType.find("text/") != string::npos diff --git a/src/server/response.h b/src/server/response.h index df372c25f..89f235072 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -42,7 +42,7 @@ class RequestContext; class Response { public: - Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton); + Response(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks); ~Response() = default; int send(const RequestContext& request, MHD_Connection* connection); @@ -64,6 +64,7 @@ class Response { int getReturnCode() { return m_returnCode; } void introduce_taskbar(); + void inject_externallinks_blocker(); private: bool m_verbose; @@ -75,6 +76,7 @@ class Response { int m_returnCode; bool m_withTaskbar; bool m_withLibraryButton; + bool m_blockExternalLinks; bool m_useCache; bool m_compress; bool m_addTaskbar; diff --git a/src/wrapper/java/kiwixserver.cpp b/src/wrapper/java/kiwixserver.cpp index 7242c4cd4..b64f61899 100644 --- a/src/wrapper/java/kiwixserver.cpp +++ b/src/wrapper/java/kiwixserver.cpp @@ -85,6 +85,12 @@ Java_org_kiwix_kiwixlib_JNIKiwixServer_setTaskbar(JNIEnv* env, jobject obj, jboo SERVER->setTaskbar(withTaskbar, withLibraryButton); } +JNIEXPORT void JNICALL +Java_org_kiwix_kiwixlib_JNIKiwixServer_setBlockExternalLinks(JNIEnv* env, jobject obj, jboolean blockExternalLinks) +{ + SERVER->setBlockExternalLinks(blockExternalLinks); +} + JNIEXPORT jboolean JNICALL Java_org_kiwix_kiwixlib_JNIKiwixServer_start(JNIEnv* env, jobject obj) { diff --git a/src/wrapper/java/org/kiwix/kiwixlib/JNIKiwixServer.java b/src/wrapper/java/org/kiwix/kiwixlib/JNIKiwixServer.java index 578e2f4de..11c5f0d70 100644 --- a/src/wrapper/java/org/kiwix/kiwixlib/JNIKiwixServer.java +++ b/src/wrapper/java/org/kiwix/kiwixlib/JNIKiwixServer.java @@ -34,6 +34,8 @@ public class JNIKiwixServer public native void setTaskbar(boolean withTaskBar, boolean witLibraryButton); + public native void setBlockExternalLinks(boolean blockExternalLinks); + public native boolean start(); public native void stop(); diff --git a/static/resources_list.txt b/static/resources_list.txt index 607f30fe9..7579c33f6 100644 --- a/static/resources_list.txt +++ b/static/resources_list.txt @@ -20,6 +20,7 @@ skin/jquery-ui/jquery-ui.min.css skin/caret.png skin/taskbar.js skin/taskbar.css +skin/block_external.js templates/search_result.html templates/no_search_result.html templates/404.html @@ -28,4 +29,6 @@ templates/index.html templates/suggestion.json templates/head_part.html templates/taskbar_part.html +templates/external_blocker_part.html +templates/captured_external.html opensearchdescription.xml diff --git a/static/skin/block_external.js b/static/skin/block_external.js new file mode 100644 index 000000000..04430670b --- /dev/null +++ b/static/skin/block_external.js @@ -0,0 +1,60 @@ +var block_path = "/catch/external"; +// called only on external links +function capture_event(e) { e.target.setAttribute("href", encodeURI(block_path + "?source=" + e.target.href)); } + +// called on all link clicks. filters external and call capture_event +function on_click_event(e) { + if ("target" in e && "href" in e.target) { + var href = e.target.href; + if (window.location.pathname.indexOf(block_path) == 0) // already in catch page + return; + if (href.indexOf(window.location.origin) == 0) + return; + if (href.substr(0, 2) == "//") + return capture_event(e); + if (href.substr(0, 5) == "http:") + return capture_event(e); + if (href.substr(0, 6) == "https:") + return capture_event(e); + return; + } +} + +// script entrypoint (called on document ready) +function run() { live('a', 'click', on_click_event); } + +// matches polyfill +this.Element && function(ElementPrototype) { + ElementPrototype.matches = ElementPrototype.matches || + ElementPrototype.matchesSelector || + ElementPrototype.webkitMatchesSelector || + ElementPrototype.msMatchesSelector || + function(selector) { + var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1; + while (nodes[++i] && nodes[i] != node); + return !!nodes[i]; + } +}(Element.prototype); + +// helper for enabling IE 8 event bindings +function addEvent(el, type, handler) { + if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler); +} + +// live binding helper using matchesSelector +function live(selector, event, callback, context) { + addEvent(context || document, event, function(e) { + var found, el = e.target || e.srcElement; + while (el && el.matches && el !== context && !(found = el.matches(selector))) el = el.parentElement; + if (found) callback.call(el, e); + }); +} + +// in case the document is already rendered +if (document.readyState!='loading') run(); +// modern browsers +else if (document.addEventListener) document.addEventListener('DOMContentLoaded', run); +// IE <= 8 +else document.attachEvent('onreadystatechange', function(){ + if (document.readyState=='complete') run(); +}); diff --git a/static/templates/captured_external.html b/static/templates/captured_external.html new file mode 100644 index 000000000..1212161b1 --- /dev/null +++ b/static/templates/captured_external.html @@ -0,0 +1,14 @@ + + + + + External link blocked + + +

External link blocked

+

This instance of Kiwix protects you from accidentaly going to external (out-of ZIM) links.

+

If you intend to go to such locations, please click the link below.

+

Go to {{ source }}

+
Powered by Kiwix
+ + diff --git a/static/templates/external_blocker_part.html b/static/templates/external_blocker_part.html new file mode 100644 index 000000000..6bb0a0c21 --- /dev/null +++ b/static/templates/external_blocker_part.html @@ -0,0 +1 @@ +