diff --git a/scripts/kiwix-resources b/scripts/kiwix-resources new file mode 100755 index 000000000..481b03f06 --- /dev/null +++ b/scripts/kiwix-resources @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +''' +Copyright 2022 Veloman Yunkan + +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. +''' + +import argparse +import hashlib +import os.path +import re + +def read_resource_file(resource_file_path): + with open(resource_file_path, 'r') as f: + return [line.strip() for line in f] + +def list_resources(resource_file_path): + for resource_path in read_resource_file(resource_file_path): + print(resource_path) + +def compute_resource_revision(resource_path): + with open(os.path.join(OUT_DIR, resource_path), 'rb') as f: + return hashlib.sha1(f.read()).hexdigest()[:8] + +resource_revisions = {} + +def get_resource_revision(res): + if not res in resource_revisions: + preprocess_resource(res) + resource_revisions[res] = compute_resource_revision(res) + return resource_revisions[res] + +RESOURCE_WITH_CACHEID_URL_PATTERN=r'(?P
.*/(?Pskin/[^"?]+)\?)KIWIXCACHEID(?P[^"]*)'
+
+def set_cacheid(resource_matchobj):
+    pre = resource_matchobj.group('pre')
+    resource = resource_matchobj.group('resource')
+    post = resource_matchobj.group('post')
+    cacheid = 'cacheid=' + get_resource_revision(resource)
+    return pre + cacheid + post
+
+def preprocess_text(s):
+    if 'KIWIXCACHEID' in s:
+        s = re.sub(RESOURCE_WITH_CACHEID_URL_PATTERN, set_cacheid, s)
+        assert not 'KIWIXCACHEID' in s
+    return s
+
+def get_preprocessed_resource(srcpath):
+    """Get the transformed content of a resource
+
+    If the resource at srcpath is modified by preprocessing then this function
+    returns the transformed content of the resource. Otherwise it returns None.
+    """
+    try:
+        with open(srcpath, 'r') as resource_file:
+            content = resource_file.read()
+            preprocessed_content = preprocess_text(content)
+        return preprocessed_content if preprocessed_content != content else None
+    except UnicodeDecodeError:
+        # It was a binary resource
+        return None
+
+
+def symlink_resource(src, resource_path):
+    if os.path.exists(resource_path):
+        if os.path.islink(resource_path) and os.readlink(resource_path) == src:
+            return
+        os.remove(resource_path)
+    os.symlink(src, resource_path)
+
+def preprocess_resource(resource_path):
+    print('Preprocessing', resource_path, '...')
+    resource_dir = os.path.dirname(resource_path)
+    if resource_dir != '':
+        os.makedirs(os.path.join(OUT_DIR, resource_dir), exist_ok=True)
+    srcpath = os.path.join(BASE_DIR, resource_path)
+    outpath = os.path.join(OUT_DIR, resource_path)
+    if os.path.exists(outpath):
+        os.remove(outpath)
+    preprocessed_content = get_preprocessed_resource(srcpath)
+    if preprocessed_content is None:
+        symlink_resource(srcpath, outpath)
+    else:
+        with open(outpath, 'w') as target:
+            print(preprocessed_content, end='', file=target)
+
+
+def copy_file(src_path, dst_path):
+    with open(src_path, 'rb') as src:
+        with open(dst_path, 'wb') as dst:
+            dst.write(src.read())
+
+def preprocess_resources(resource_file_path):
+    resource_filename = os.path.basename(resource_file_path)
+    for resource in read_resource_file(resource_file_path):
+        preprocess_resource(resource)
+    copy_file(resource_file_path, os.path.join(OUT_DIR, resource_filename))
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    commands = parser.add_mutually_exclusive_group()
+    commands.add_argument('--list-all', action='store_true')
+    commands.add_argument('--preprocess', action='store_true')
+    parser.add_argument('--outdir')
+    parser.add_argument('resource_file')
+    args = parser.parse_args()
+    BASE_DIR = os.path.dirname(os.path.realpath(args.resource_file))
+    OUT_DIR = args.outdir
+
+    if args.list_all:
+        list_resources(args.resource_file)
+    elif args.preprocess:
+        preprocess_resources(args.resource_file)
diff --git a/scripts/meson.build b/scripts/meson.build
index c4ddec873..889a53248 100644
--- a/scripts/meson.build
+++ b/scripts/meson.build
@@ -1,4 +1,5 @@
 
+res_manager = find_program('kiwix-resources')
 res_compiler = find_program('kiwix-compile-resources')
 
 install_data(res_compiler.path(), install_dir:get_option('bindir'))
diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp
index 748765615..95f999f99 100644
--- a/src/server/internalServer.cpp
+++ b/src/server/internalServer.cpp
@@ -442,6 +442,16 @@ SuggestionsList_t getSuggestions(SuggestionSearcherCache& cache, const zim::Arch
 namespace
 {
 
+std::string renderUrl(const std::string& root, const std::string& urlTemplate)
+{
+  MustacheData data;
+  data.set("root", root);
+  auto url = kainjow::mustache::mustache(urlTemplate).render(data);
+  if ( url.back() == '\n' )
+    url.pop_back();
+  return url;
+}
+
 std::string makeFulltextSearchSuggestion(const std::string& lang, const std::string& queryString)
 {
   return i18n::expandParameterizedString(lang, "suggest-full-text-search",
@@ -622,10 +632,11 @@ std::unique_ptr InternalServer::handle_search(const RequestContext& re
     } catch(std::runtime_error& e) {
       // Searcher->search will throw a runtime error if there is no valid xapian database to do the search.
       // (in case of zim file not containing a index)
+      const auto cssUrl = renderUrl(m_root, RESOURCE::templates::url_of_search_results_css);
       return HTTPErrorHtmlResponse(*this, request, MHD_HTTP_NOT_FOUND,
                                    "fulltext-search-unavailable",
                                    "404-page-heading",
-                                   m_root + "/skin/search_results.css")
+                                   cssUrl)
            + nonParameterizedMessage("no-search-results")
            + TaskbarInfo(searchInfo.bookName, archive.get());
     }
diff --git a/static/meson.build b/static/meson.build
index 6a8ce1773..965f2c11b 100644
--- a/static/meson.build
+++ b/static/meson.build
@@ -1,18 +1,27 @@
-resource_files = run_command(find_program('python3'),
-                    '-c',
-                    'import sys; f=open(sys.argv[1]); print(f.read())',
+resource_files = run_command(res_manager,
+                    '--list-all',
                     files('resources_list.txt')
                  ).stdout().strip().split('\n')
 
-lib_resources = custom_target('resources',
+preprocessed_resources = custom_target('preprocessed_resource_files',
     input: 'resources_list.txt',
+    output: ['resources_list.txt'],
+    command:[res_manager,
+             '--preprocess',
+             '--outdir', '@OUTDIR@',
+             '@INPUT@'],
+    depend_files: resource_files
+)
+
+lib_resources = custom_target('resources',
+    input: preprocessed_resources,
     output: ['kiwixlib-resources.cpp', 'kiwixlib-resources.h'],
     command:[res_compiler,
              '--cxxfile', '@OUTPUT0@',
              '--hfile', '@OUTPUT1@',
              '--source_dir', '@OUTDIR@',
              '@INPUT@'],
-    depend_files: resource_files
+    depends: preprocessed_resources
 )
 
 i18n_resource_files = run_command(find_program('python3'),
diff --git a/static/resources_list.txt b/static/resources_list.txt
index 15275ad19..bc1eb1af3 100644
--- a/static/resources_list.txt
+++ b/static/resources_list.txt
@@ -47,5 +47,6 @@ templates/catalog_v2_entries.xml
 templates/catalog_v2_entry.xml
 templates/catalog_v2_categories.xml
 templates/catalog_v2_languages.xml
+templates/url_of_search_results_css
 opensearchdescription.xml
 catalog_v2_searchdescription.xml
diff --git a/static/skin/index.js b/static/skin/index.js
index 267da4098..5c435ac6b 100644
--- a/static/skin/index.js
+++ b/static/skin/index.js
@@ -171,25 +171,25 @@