From 50ddfaf18b0b9aae7768facb39a3314d23961835 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Sun, 14 Mar 2021 15:20:59 +0300 Subject: sanitize retrieved wiktionary content (just in case) --- backend.php | 8 +- classes/sanitizer.php | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++ dist/app.min.js | 2 +- js/reader.js | 2 + 4 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 classes/sanitizer.php diff --git a/backend.php b/backend.php index 0cbd6f4..9678fab 100644 --- a/backend.php +++ b/backend.php @@ -249,7 +249,13 @@ $url = "https://en.wiktionary.org/w/api.php?titles=${query}&action=query&prop=extracts&format=json&exlimit=1"; if ($resp = file_get_contents($url)) { - print $resp; + $resp = json_decode($resp, true); + + foreach ($resp['query']['pages'] as &$page) { + $page['extract'] = Sanitizer::sanitize($page['extract']); + } + + print json_encode($resp); } break; diff --git a/classes/sanitizer.php b/classes/sanitizer.php new file mode 100644 index 0000000..cf68632 --- /dev/null +++ b/classes/sanitizer.php @@ -0,0 +1,252 @@ +loadHTML('' . $res); + $xpath = new DOMXPath($doc); + + // is it a good idea to possibly rewrite urls to our own prefix? + // $rewrite_base_url = $site_url ? $site_url : Config::get_self_url(); + $rewrite_base_url = "http://domain.invalid/"; + + $entries = $xpath->query('(//a[@href]|//img[@src]|//source[@srcset|@src])'); + + foreach ($entries as $entry) { + + if ($entry->hasAttribute('href')) { + $entry->setAttribute('href', + self::rewrite_relative($rewrite_base_url, $entry->getAttribute('href'))); + + $entry->setAttribute('rel', 'noopener noreferrer'); + $entry->setAttribute("target", "_blank"); + } + + if ($entry->hasAttribute('src')) { + $entry->setAttribute('src', + self::rewrite_relative($rewrite_base_url, $entry->getAttribute('src'))); + } + + if ($entry->nodeName == 'img') { + $entry->setAttribute('referrerpolicy', 'no-referrer'); + $entry->setAttribute('loading', 'lazy'); + } + + if ($entry->hasAttribute('srcset')) { + $entry->removeAttribute('srcset'); + } + } + + $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside', + 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', + 'caption', 'cite', 'center', 'code', 'col', 'colgroup', + 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font', + 'dt', 'em', 'footer', 'figure', 'figcaption', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i', + 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript', + 'ol', 'p', 'picture', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section', + 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary', + 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', + 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' ); + + $disallowed_attributes = array('id', 'style', 'class', 'width', 'height', 'allow'); + + $doc->removeChild($doc->firstChild); //remove doctype + $doc = self::strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes); + + $res = $doc->saveHTML(); + + /* strip everything outside of ... */ + + $res_frag = array(); + if (preg_match('/(.*)<\/body>/is', $res, $res_frag)) { + return $res_frag[1]; + } else { + return $res; + } + } + + private static function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) { + $xpath = new DOMXPath($doc); + $entries = $xpath->query('//*'); + + foreach ($entries as $entry) { + if (!in_array($entry->nodeName, $allowed_elements)) { + $entry->parentNode->removeChild($entry); + } + + if ($entry->hasAttributes()) { + $attrs_to_remove = array(); + + foreach ($entry->attributes as $attr) { + + if (strpos($attr->nodeName, 'on') === 0) { + array_push($attrs_to_remove, $attr); + } + + if (strpos($attr->nodeName, "data-") === 0) { + array_push($attrs_to_remove, $attr); + } + + if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) { + array_push($attrs_to_remove, $attr); + } + + if (in_array($attr->nodeName, $disallowed_attributes)) { + array_push($attrs_to_remove, $attr); + } + } + + foreach ($attrs_to_remove as $attr) { + $entry->removeAttributeNode($attr); + } + } + } + + return $doc; + } + + // extended filtering involves validation for safe ports and loopback + static function validate($url, $extended_filtering = false) { + + $url = trim($url); + + # fix protocol-relative URLs + if (strpos($url, "//") === 0) + $url = "https:" . $url; + + $tokens = parse_url($url); + + // this isn't really necessary because filter_var(... FILTER_VALIDATE_URL) requires host and scheme + // as per https://php.watch/versions/7.3/filter-var-flag-deprecation but it might save time + if (empty($tokens['host'])) + return false; + + if (!in_array(strtolower($tokens['scheme']), ['http', 'https'])) + return false; + + //convert IDNA hostname to punycode if possible + if (function_exists("idn_to_ascii")) { + if (mb_detect_encoding($tokens['host']) != 'ASCII') { + if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) { + $tokens['host'] = idn_to_ascii($tokens['host'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } else { + $tokens['host'] = idn_to_ascii($tokens['host']); + } + } + } + + // separate set of tokens with urlencoded 'path' because filter_var() rightfully fails on non-latin characters + // (used for validation only, we actually request the original URL, in case of urlencode breaking it) + $tokens_filter_var = $tokens; + + if ($tokens['path'] ?? false) { + $tokens_filter_var['path'] = implode("/", + array_map("rawurlencode", + array_map("rawurldecode", + explode("/", $tokens['path'])))); + } + + $url = self::build_url($tokens); + $url_filter_var = self::build_url($tokens_filter_var); + + if (filter_var($url_filter_var, FILTER_VALIDATE_URL) === false) + return false; + + if ($extended_filtering) { + if (!in_array($tokens['port'] ?? '', [80, 443, ''])) + return false; + + if (strtolower($tokens['host']) == 'localhost' || $tokens['host'] == '::1' || strpos($tokens['host'], '127.') === 0) + return false; + } + + return $url; + } + + static function build_url($parts) { + $tmp = $parts['scheme'] . "://" . $parts['host']; + + if (isset($parts['path'])) $tmp .= $parts['path']; + if (isset($parts['query'])) $tmp .= '?' . $parts['query']; + if (isset($parts['fragment'])) $tmp .= '#' . $parts['fragment']; + + return $tmp; + } + + static function resolve_redirects($url, $timeout, $nest = 0) { + + // too many redirects + if ($nest > 10) + return false; + + if (version_compare(PHP_VERSION, '7.1.0', '>=')) { + $context_options = array( + 'http' => array( + 'header' => array( + 'Connection: close' + ), + 'method' => 'HEAD', + 'timeout' => $timeout, + 'protocol_version'=> 1.1) + ); + + $context = stream_context_create($context_options); + + $headers = get_headers($url, 0, $context); + } else { + $headers = get_headers($url, 0); + } + + if (is_array($headers)) { + $headers = array_reverse($headers); // last one is the correct one + + foreach($headers as $header) { + if (stripos($header, 'Location:') === 0) { + $url = self::rewrite_relative($url, trim(substr($header, strlen('Location:')))); + + return self::resolve_redirects($url, $timeout, $nest + 1); + } + } + + return $url; + } + + // request failed? + return false; + } + +} diff --git a/dist/app.min.js b/dist/app.min.js index d454dfd..491638c 100644 --- a/dist/app.min.js +++ b/dist/app.min.js @@ -1 +1 @@ -"use strict";$.urlParam=function(e){try{const t=new RegExp("[?&]"+e+"=([^&#]*)").exec(window.location.href);return decodeURIComponent(t[1].replace(/\+/g," "))||0}catch(e){return 0}};const Cookie={set:function(e,t,o){const n=new Date;n.setTime(n.getTime()+1e3*o);const a="expires="+n.toUTCString();document.cookie=e+"="+encodeURIComponent(t)+"; "+a},get:function(e){e+="=";const t=document.cookie.split(";");for(let o=0;o>>",e)})),"undefined"!=typeof EpubeApp&&($(".navbar").hide(),$(".epube-app-filler").show(),$(".separate-search").show(),"favorites"==$.urlParam("mode")?EpubeApp.setPage("PAGE_FAVORITES"):EpubeApp.setPage("PAGE_LIBRARY")),App.initNightMode(),"serviceWorker"in navigator?navigator.serviceWorker.addEventListener("message",(function(t){"refresh-started"==t.data&&(console.log("cache refresh started"),e=0,$(".dl-progress").fadeIn().text("Loading, please wait...")),t.data&&0==t.data.indexOf("refreshed:")&&(++e,$(".dl-progress").fadeIn().text("Updated "+e+" files...")),"client-reload"==t.data&&(localforage.setItem("epube.cache-timestamp",App.last_mtime),window.location.reload())})):$(".container-main").addClass("alert alert-danger").html("Service worker support missing in browser (are you using plain HTTP?)."),App.showCovers(),App.Offline.markBooks(),App.refreshCache()},logout:function(){$.post("backend.php",{op:"logout"}).then(()=>{window.location.reload()})},showSummary:function(e){const t=e.getAttribute("data-book-id");return $.post("backend.php",{op:"getinfo",id:t},(function(e){const t=e.comment?e.comment:"No description available";$("#summary-modal .modal-title").html(e.title),$("#summary-modal .book-summary").html(t),$("#summary-modal").modal()})),!1},showCovers:function(){$("img[data-book-id]").each((e,t)=>{if((t=$(t)).attr("data-cover-link")){const e=$("").on("load",(function(){t.css("background-image","url("+t.attr("data-cover-link")+")").fadeIn(),e.attr("src",null)})).attr("src",t.attr("data-cover-link"))}else t.attr("src","holder.js/130x190?auto=yes").fadeIn()}),Holder.run()},toggleFavorite:function(e){const t=e.getAttribute("data-book-id");return("0"==e.getAttribute("data-is-fav")||confirm("Remove favorite?"))&&$.post("backend.php",{op:"togglefav",id:t},(function(o){if(o){let n="[Error]";0==o.status?n="Add to favorites":1==o.status&&(n="Remove from favorites"),$(e).html(n).attr("data-is-fav",o.status),"favorites"==App.index_mode&&0==o.status&&$("#cell-"+t).remove()}})),!1},refreshCache:function(e){"serviceWorker"in navigator?localforage.getItem("epube.cache-timestamp").then((function(t){console.log("stamp",t,"last mtime",App.last_mtime),(e||t!=App.last_mtime)&&(console.log("asking worker to refresh cache"),navigator.serviceWorker.controller?navigator.serviceWorker.controller.postMessage("refresh-cache"):localforage.getItem("epube.initial-load-done").then((function(e){console.log("initial load done",e),e?$(".dl-progress").show().addClass("alert-danger").html("Could not communicate with service worker. Try reloading the page."):localforage.setItem("epube.initial-load-done",!0).then((function(){$(".dl-progress").show().addClass("alert-info").html("Page will reload to activate service worker..."),window.setTimeout((function(){window.location.reload()}),3e3)}))})))})):$(".dl-progress").show().addClass("alert-danger").html("Could not communicate with service worker. Try reloading the page.")},isOnline:function(){return"undefined"!=typeof EpubeApp&&void 0!==EpubeApp.isOnline?EpubeApp.isOnline():navigator.onLine},appCheckOffline:function(){EpubeApp.setOffline(!App.isOnline)},initNightMode:function(){if("undefined"==typeof EpubeApp){if(window.matchMedia){const e=window.matchMedia("(prefers-color-scheme: dark)");e.addEventListener("change",()=>{App.applyNightMode(e.matches)}),App.applyNightMode(e.matches)}}else App.applyNightMode(EpubeApp.isNightMode())},applyNightMode:function(e){console.log("night mode changed to",e),$("#theme_css").attr("href","lib/bootstrap/v3/css/"+(e?"theme-dark.min.css":"bootstrap-theme.min.css"))},Offline:{init:function(){"undefined"!=typeof EpubeApp&&($(".navbar").hide(),$(".epube-app-filler").show(),EpubeApp.setPage("PAGE_OFFLINE")),App.initNightMode();const e=$.urlParam("query");e&&$(".search_query").val(e),App.Offline.populateList()},get:function(e,t){console.log("offline cache: "+e),$.post("backend.php",{op:"getinfo",id:e},(function(o){if(o){const n="epube-book."+e;localforage.setItem(n,o).then((function(o){console.log(n+" got data");const a=[];a.push(fetch("backend.php?op=download&id="+o.epub_id,{credentials:"same-origin"}).then((function(e){200==e.status&&(console.log(n+" got book"),t(),localforage.setItem(n+".book",e.blob()))}))),a.push(fetch("backend.php?op=getpagination&id="+o.epub_id,{credentials:"same-origin"}).then((function(e){200==e.status&&(console.log(n+" got pagination"),e.text().then((function(e){localforage.setItem(n+".locations",JSON.parse(e))})))}))),a.push(fetch("backend.php?op=getlastread&id="+o.epub_id,{credentials:"same-origin"}).then((function(e){200==e.status&&(console.log(n+" got lastread"),e.text().then((function(e){localforage.setItem(n+".lastread",JSON.parse(e))})))}))),o.has_cover&&a.push(fetch("backend.php?op=cover&id="+e,{credentials:"same-origin"}).then((function(e){200==e.status&&(console.log(n+" got cover"),localforage.setItem(n+".cover",e.blob()))}))),Promise.all(a).then((function(){$(".dl-progress").show().html("Finished downloading "+o.title+""),window.clearTimeout(App._dl_progress_timeout),App._dl_progress_timeout=window.setTimeout((function(){$(".dl-progress").fadeOut()}),5e3)}))}))}}))},getAll:function(){confirm("Download all books on this page?")&&$(".row > div").each((function(e,t){const o=$(t).attr("id").replace("cell-",""),n=$(t).find(".offline_dropitem")[0];if(o){const e="epube-book."+o;localforage.getItem(e).then((function(e){e||App.Offline.get(o,(function(){App.Offline.mark(n)}))}))}}))},markBooks:function(){const e=$(".offline_dropitem");$.each(e,(function(e,t){App.Offline.mark(t)}))},mark:function(e){const t=e.getAttribute("data-book-id"),o="epube-book."+t;localforage.getItem(o).then((function(o){o?(e.onclick=function(){return App.Offline.remove(t,(function(){App.Offline.mark(e)})),!1},e.innerHTML="Remove offline data"):(e.onclick=function(){return App.Offline.get(t,(function(){App.Offline.mark(e)})),!1},e.innerHTML="Make available offline")}))},removeFromList:function(e){const t=e.getAttribute("data-book-id");return App.Offline.remove(t,(function(){$("#cell-"+t).remove()}))},remove:function(e,t){if(confirm("Remove download?")){const o="epube-book."+e,n=[];console.log("offline remove: "+e),localforage.iterate((function(e,t){t.match(o)&&n.push(localforage.removeItem(t))})),Promise.all(n).then((function(){window.setTimeout((function(){t()}),500)}))}},search:function(){const e=$(".search_query").val();return localforage.setItem("epube.search-query",e).then((function(){App.Offline.populateList()})),!1},removeAll:function(){if(confirm("Remove all downloaded books?")){const e=[];localforage.iterate((function(t,o){o.match("epube-book")&&e.push(localforage.removeItem(o))})),Promise.all(e).then((function(){window.setTimeout((function(){App.Offline.populateList()}),500)}))}},showSummary:function(e){const t=e.getAttribute("data-book-id");return localforage.getItem("epube-book."+t).then((function(e){const t=e.comment?e.comment:"No description available";$("#summary-modal .modal-title").html(e.title),$("#summary-modal .book-summary").html(t),$("#summary-modal").modal()})),!1},populateList:function(){let e=$.urlParam("query");e&&(e=e.toLowerCase());const t=$("#books_container");t.html(""),localforage.iterate((function(o,n){n.match(/epube-book\.\d{1,}$/)&&Promise.all([localforage.getItem(n),localforage.getItem(n+".cover"),localforage.getItem(n+".lastread"),localforage.getItem(n+".book")]).then((function(o){if(o[0]&&o[3]){const n=o[0];if(e){if(!(n.series_name&&n.series_name.toLowerCase().match(e)||n.title&&n.title.toLowerCase().match(e)||n.author_sort&&n.author_sort.toLowerCase().match(e)))return}let a=!1;o&&o[1]&&(a=URL.createObjectURL(o[1]));let i=!1,r=!1;const l=o[2];l&&(i=l.page>0,r=l.total>0&&l.total-l.page<5);const c=r?"read":"",s=i?"in_progress":"",d=n.series_name?`
${n.series_name+" ["+n.series_index+"]"}
`:"",p=$(`
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t
${n.title}
\n\t\t\t\t\t\t\t\t
${n.author_sort}
\n\t\t\t\t\t\t\t\t${d}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
`);a?p.find("img").css("background-image","url("+a+")").fadeIn():p.find("img").attr("data-src","holder.js/130x190?auto=yes").fadeIn(),p.find(".series_link").attr("title",n.series_name+" ["+n.series_index+"]").attr("href","offline.html?query="+encodeURIComponent(n.series_name)),p.find(".author_link").attr("title",n.author_sort).attr("href","offline.html?query="+encodeURIComponent(n.author_sort)),t.append(p),Holder.run()}}))}))}}},DEFAULT_FONT_SIZE=16,DEFAULT_FONT_FAMILY="Georgia",DEFAULT_LINE_HEIGHT=140,MIN_LENGTH_TO_JUSTIFY=32,PAGE_RESET_PROGRESS=-1,Reader={csrf_token:"",init:function(){this.csrf_token=Cookie.get("epube_csrf_token"),console.log("setting prefilter for token",this.csrf_token),$.ajaxPrefilter((function(e,t){if("post"!==t.type||"post"!==e.type)return;const o=typeof t.data;"object"==o?e.data=$.param($.extend(t.data,{csrf_token:Reader.csrf_token})):"string"==o&&(e.data=t.data+"&csrf_token="+encodeURIComponent(Reader.srf_token)),console.log(">>>",e)})),$(document).on("keyup",(function(e){Reader.hotkeyHandler(e)})),$("#left").on("mouseup",(function(){Reader.Page.prev()})),$("#right").on("mouseup",(function(){Reader.Page.next()})),Reader.Loader.init()},onOfflineModeChanged:function(e){if(console.log("onOfflineModeChanged",e),!e&&window.book){const e=window.book;console.log("we're online, storing lastread");const t=e.rendition.currentLocation().start.cfi,o=parseInt(100*e.locations.percentageFromCfi(t));$.post("backend.php",{op:"storelastread",id:$.urlParam("id"),page:o,cfi:t,timestamp:(new Date).getTime()},(function(e){e.cfi&&(Reader.Page._last_position_sync=(new Date).getTime()/1e3)})).fail((function(e){e&&401==e.status&&(window.location="index.php")}))}},initSecondStage:function(){return"undefined"!=typeof EpubeApp?EpubeApp.setPage("PAGE_READER"):($(window).on("online",(function(){Reader.onOfflineModeChanged(!1)})),$(window).on("offline",(function(){Reader.onOfflineModeChanged(!0)}))),Reader.applyTheme(),localforage.getItem(Reader.cacheId()).then((function(t){if(!t)return console.log("requesting bookinfo..."),new Promise((t,o)=>{const n=$.urlParam("b");$.post("backend.php",{op:"getinfo",id:n}).success((function(e){if(e)return e.has_cover&&fetch("backend.php?op=cover&id="+n,{credentials:"same-origin"}).then((function(e){200==e.status&&localforage.setItem(Reader.cacheId("cover"),e.blob())})),localforage.setItem(Reader.cacheId(),e).then((function(){console.log("bookinfo saved"),t()}));o(new Error("unable to load book info: blank"))})).error((function(t){$(".loading_message").html("Unable to load book info.
"+t.status+""),o(new Error("unable to load book info: "+e))}))});console.log("bookinfo already stored")})).then((function(){console.log("trying to load book..."),localforage.getItem(Reader.cacheId("book")).then((function(t){if(t)return console.log("loading from local storage"),new Promise((function(o,n){const a=new FileReader;a.onload=function(){try{return e.open(this.result).then((function(){o()}))}catch(e){$(".loading_message").html("Unable to load book (local)."),console.log(e),n(new Error("Unable to load book (local):"+e))}},a.readAsArrayBuffer(t)}));if(console.log("loading from network"),App.isOnline()){const t="backend.php?op=download&id="+$.urlParam("id");return $(".loading_message").html("Downloading..."),fetch(t,{credentials:"same-origin"}).then((function(t){if(200==t.status)return t.blob().then((function(t){return new Promise((function(o,n){const a=new FileReader;a.onload=function(){e.open(this.result).then((function(){localforage.setItem(Reader.cacheId("book"),t).then((function(){o()}))})).catch(e=>{$(".loading_message").html("Unable to open book.
"+e+""),n(new Error("Unable to open book: "+e))})},a.onerror=function(e){console.log("filereader error",e),$(".loading_message").html("Unable to open book.
"+e+""),n(new Error("Unable to open book: "+e))},a.readAsArrayBuffer(t)}))})).catch(e=>{console.log("blob error",e),$(".loading_message").html("Unable to download book.
"+e+"")});$(".loading_message").html("Unable to download book: "+t.status+".")})).catch((function(e){console.warn(e),$(".loading").is(":visible")&&$(".loading_message").html("Unable to load book (remote).
"+e+"")}))}$(".loading_message").html("This book is not available offline.")}));const e=ePub();window.book=e;const t=e.renderTo("reader",{width:"100%",height:"100%",minSpreadWidth:961});function o(t){try{const o=e.spine.get(t).cfiBase,n=e.locations._locations.find((function(e){return-1!=e.indexOf(o)}));return window.book.locations.locationFromCfi(n)}catch(e){console.warn(e)}return""}localforage.getItem("epube.enable-hyphens").then((function(e){e&&(Reader.hyphenateHTML=createHyphenator(hyphenationPatternsEnUs,{html:!0})),Reader.applyStyles(!0),t.display().then((function(){console.log("book displayed")}))})),t.hooks.content.register((function(t){t.on("linkClicked",(function(t){console.log("linkClicked",t),-1==t.indexOf("://")&&($(".prev_location_btn").attr("data-location-cfi",e.rendition.currentLocation().start.cfi).show(),window.setTimeout((function(){Reader.showUI(!0)}),50))}));const o=window.location.href.match(/^.*\//)[0],n=["dist/app-libs.min.js","dist/reader_iframe.min.js"],a=t.document;for(let e=0;e").text(Reader.Loader._res_data[o+"dist/reader_iframe.min.css"])),localforage.getItem("epube.theme").then((function(e){e||(e="default"),$(t.document).find("body").attr("class","undefined"!=typeof EpubeApp?"is-epube-app":"").addClass("theme-"+e)}))})),$("#settings-modal").on("shown.bs.modal",(function(){localforage.getItem(Reader.cacheId("lastread")).then(e=>{e&&e.cfi&&$(".lastread_input").val(e.page+"%"),$.post("backend.php",{op:"getlastread",id:$.urlParam("id")},(function(e){$(".lastread_input").val(e.page+"%")}))}),localforage.getItem("epube.enable-hyphens").then((function(e){$(".enable_hyphens_checkbox").attr("checked",e).off("click").on("click",(function(e){localforage.setItem("epube.enable-hyphens",e.target.checked),confirm("Toggling hyphens requires page reload. Reload now?")&&window.location.reload()}))})),localforage.getItem("epube.keep-ui-visible").then((function(e){$(".keep_ui_checkbox").attr("checked",e).off("click").on("click",(function(e){localforage.setItem("epube.keep-ui-visible",e.target.checked)}))})),localforage.getItem("epube.cache-timestamp").then((function(e){let t="V: ";parseInt(e)?t+=new Date(1e3*e).toLocaleString("en-GB"):t+="Unknown",t+=" ("+(App.isOnline()?"Online":"Offline")+")",$(".last-mod-timestamp").text(t)})),localforage.getItem("epube.fontFamily").then((function(e){e||(e="Georgia"),$(".font_family").val(e)})),localforage.getItem("epube.theme").then((function(e){$(".theme_name").val(e)})),localforage.getItem("epube.fontSize").then((function(e){e||(e=16);const t=$(".font_size").html("");for(let e=10;e<=32;e++){const o=$("