From 3b8d237474e43eec499b12ebfa1a0127096d0379 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sat, 22 Jan 2022 23:41:26 +0000 Subject: [PATCH 01/20] Initial fixes - need testing --- service-worker.js | 7 ++++--- www/js/app.js | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/service-worker.js b/service-worker.js index ccdb049ff..e9165ece2 100644 --- a/service-worker.js +++ b/service-worker.js @@ -286,8 +286,9 @@ function fetchRequestFromZIM(fetchEvent) { nameSpace = regexpResult[2]; title = regexpResult[3]; - // We need to remove the potential parameters in the URL - title = removeUrlParameters(decodeURIComponent(title)); + // We need to remove the potential parameters in the URL. Note that titles may contain question marks or hashes, so we test the + // encoded URI before decoding it. Be sure that you haven't encoded any querystring along with the URL, e.g. for clicked links. + title = decodeURIComponent(removeUrlParameters(title)); titleWithNameSpace = nameSpace + '/' + title; @@ -339,7 +340,7 @@ function fetchRequestFromZIM(fetchEvent) { * @returns {String} The same URL without its parameters and anchors */ function removeUrlParameters(url) { - return url.replace(/([^?#]+)[?#].*$/, '$1'); + return url.replace(/[?#].*$/, ''); } /** diff --git a/www/js/app.js b/www/js/app.js index 1272cebe8..1f2238a4e 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -248,7 +248,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys if (/Enter/.test(e.key)) { if (activeElement.classList.contains('hover')) { var dirEntryId = activeElement.getAttribute('dirEntryId'); - findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); + findDirEntryFromDirEntryIdAndLaunchArticleRead(decodeURIComponent(dirEntryId)); return; } } @@ -1285,7 +1285,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys var listLength = dirEntryArray.length < params.maxSearchResultsSize ? dirEntryArray.length : params.maxSearchResultsSize; for (var i = 0; i < listLength; i++) { var dirEntry = dirEntryArray[i]; - var dirEntryStringId = uiUtil.htmlEscapeChars(dirEntry.toStringId()); + // NB We use encodeURIComponent here because we know that any question marks in the title are not querystrings, and should be encoded + var dirEntryStringId = encodeURIComponent(dirEntry.toStringId()); articleListDivHtml += '' + dirEntry.getTitleOrUrl() + ''; } @@ -1308,7 +1309,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys * @returns {Boolean} */ function handleTitleClick(event) { - var dirEntryId = event.target.getAttribute("dirEntryId"); + var dirEntryId = decodeURIComponent(event.target.getAttribute("dirEntryId")); findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); return false; } From 7eed74f03d25f745f298017b3766762e3fbeef07 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 23 Jan 2022 08:39:46 +0000 Subject: [PATCH 02/20] Comments and cleanup --- www/js/app.js | 13 ++++++++----- www/js/lib/uiUtil.js | 8 +++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 1f2238a4e..175250744 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1285,7 +1285,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys var listLength = dirEntryArray.length < params.maxSearchResultsSize ? dirEntryArray.length : params.maxSearchResultsSize; for (var i = 0; i < listLength; i++) { var dirEntry = dirEntryArray[i]; - // NB We use encodeURIComponent here because we know that any question marks in the title are not querystrings, and should be encoded + // NB We use encodeURIComponent rather than encodeURI here because we know that any question marks in the title are not querystrings, + // and should be encoded [kiwix-js #806]. DEV: be very careful if you edit the dirEntryId attribute below, because the contents must be + // inside double quotes (in the final HTML string), given that dirEntryStringId may contain bare apostrophes + // Info: encodeURIComponent encodes all characters except A-Z a-z 0-9 - _ . ! ~ * ' ( ) var dirEntryStringId = encodeURIComponent(dirEntry.toStringId()); articleListDivHtml += '' + dirEntry.getTitleOrUrl() + ''; @@ -1305,11 +1308,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys /** * Handles the click on the title of an article in search results - * @param {Event} event - * @returns {Boolean} + * @param {Event} event The click event to handle + * @returns {Boolean} Always returns false for JQuery event handling */ function handleTitleClick(event) { - var dirEntryId = decodeURIComponent(event.target.getAttribute("dirEntryId")); + var dirEntryId = decodeURIComponent(event.target.getAttribute('dirEntryId')); findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); return false; } @@ -1318,7 +1321,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys /** * Creates an instance of DirEntry from given dirEntryId (including resolving redirects), * and call the function to read the corresponding article - * @param {String} dirEntryId + * @param {String} dirEntryId The stringified Directory Entry to parse and launch */ function findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId) { if (selectedArchive.isReady()) { diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 5b6a9c8fa..6f89bbd6a 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -307,10 +307,16 @@ define(rqDef, function(settingsStore) { } /** + * DEV: This function is no longer used in the project and could be removed unless it is of historical interest. + * It has been superseded by encodeURIComponent which encodes all characters except A-Z a-z 0-9 - _ . ! ~ * ' ( ) + * The only character from below that is not encoded is apostrophe ('), but this does not need to be encoded to + * show correctly in our UI, given that it is an allowed character in bare URIs and the dirEntryId is enclosed + * in double quote marks ("..."). This has been successfully tested on titles with apostrophes. + * * Encodes the html escape characters in the string before using it as html class name,id etc. * * @param {String} string The string in which html characters are to be escaped - * + * @returns {String} The escaped HTML string */ function htmlEscapeChars(string) { var escapechars = { From 669410568b0e66e6768779a845d92a8b515fa591 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 23 Jan 2022 09:53:46 +0000 Subject: [PATCH 03/20] Deal correctly with anchor parameters in JQuery mode --- www/js/app.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 175250744..1030fe94f 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1505,7 +1505,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys // the link. This is currently the case for epub and pdf files in Project Gutenberg ZIMs -- add any further types you need // to support to this regex. The "zip" has been added here as an example of how to support further filetypes var regexpDownloadLinks = /^.*?\.epub($|\?)|^.*?\.pdf($|\?)|^.*?\.zip($|\?)/i; - + + // A string to hold any anchor parameter in clicked ZIM URLs (as we must strip these to find the article in the ZIM) + var anchorParameter; + /** * Display the the given HTML article in the web page, * and convert links to javascript calls @@ -1595,7 +1598,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys //loadJavaScriptJQuery(); loadCSSJQuery(); insertMediaBlobsJQuery(); - + // Jump to any anchor parameter + if (anchorParameter) { + var target = iframeContentDocument.getElementById(anchorParameter); + if (target) target.scrollIntoView(); + anchorParameter = ''; + } if (iframeArticleContent.contentWindow) { // Configure home key press to focus #prefix only if the feature is in active state if (params.useHomeKeyToFocusSearchBar) @@ -1658,6 +1666,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys // Add an onclick event to extract this article or file from the ZIM // instead of following the link anchor.addEventListener('click', function (e) { + anchorParameter = href.match(/#([^#]+)$/); + anchorParameter = anchorParameter ? anchorParameter[1] : ''; var zimUrl = uiUtil.deriveZimUrlFromRelativeUrl(uriComponent, baseUrl); goToArticle(zimUrl, downloadAttrValue, contentType); e.preventDefault(); From b5d0644434b3215cf88abf9923be8d084e3793b3 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 23 Jan 2022 10:26:05 +0000 Subject: [PATCH 04/20] Make removeUrlParameters more robust --- service-worker.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service-worker.js b/service-worker.js index e9165ece2..4fb565dac 100644 --- a/service-worker.js +++ b/service-worker.js @@ -340,7 +340,11 @@ function fetchRequestFromZIM(fetchEvent) { * @returns {String} The same URL without its parameters and anchors */ function removeUrlParameters(url) { - return url.replace(/[?#].*$/, ''); + // Remove any querystring + var strippedUrl = url.replace(/\?[^?]*$/, ''); + // Remove any anchor parameters + strippedUrl = strippedUrl.replace(/#[^#]*$/, ''); + return strippedUrl; } /** From 73077b92efdbe1b457317f22e2b1cff33b5912f5 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 23 Jan 2022 11:35:34 +0000 Subject: [PATCH 05/20] Make anchor stripping more robust --- service-worker.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/service-worker.js b/service-worker.js index 4fb565dac..8913f2e62 100644 --- a/service-worker.js +++ b/service-worker.js @@ -342,8 +342,10 @@ function fetchRequestFromZIM(fetchEvent) { function removeUrlParameters(url) { // Remove any querystring var strippedUrl = url.replace(/\?[^?]*$/, ''); - // Remove any anchor parameters - strippedUrl = strippedUrl.replace(/#[^#]*$/, ''); + // Remove any anchor parameters - note that IN PRACTICE anchor parameters cannot contain a semicolon because JavaScript maintains + // compatibility with HTML4, so we can avoid accidentally stripping e.g. ' by excluding an anchor if any semicolon is found + // between it and the end of the string. See https://stackoverflow.com/a/79022/9727685. + strippedUrl = strippedUrl.replace(/#[^#;]*$/, ''); return strippedUrl; } From 2726c9f466877f6d65f726160a32b389af26981b Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 23 Jan 2022 13:06:15 +0000 Subject: [PATCH 06/20] Apply more robust regex also to jQuery mode --- www/js/app.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 1030fe94f..be526df7c 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1625,7 +1625,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys // NB dirEntry.url can also contain path separator / in some ZIMs (Stackexchange). } and ] do not need to be escaped as they have no meaning on their own. var escapedUrl = encodeURIComponent(dirEntry.url).replace(/([\\$^.|?*+/()[{])/g, '\\$1'); // Pattern to match a local anchor in an href even if prefixed by escaped url; will also match # on its own - var regexpLocalAnchorHref = new RegExp('^(?:#|' + escapedUrl + '#)([^#]*$)'); + // Note that we exclude any # with a semicolon between it and the end of the string, to avoid accidentally matching e.g. ' + var regexpLocalAnchorHref = new RegExp('^(?:#|' + escapedUrl + '#)([^#;]*$)'); var iframe = iframeArticleContent.contentDocument; Array.prototype.slice.call(iframe.querySelectorAll('a, area')).forEach(function (anchor) { // Attempts to access any properties of 'this' with malformed URLs causes app crash in Edge/UWP [kiwix-js #430] @@ -1637,11 +1638,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys } var href = anchor.getAttribute('href'); if (href === null || href === undefined) return; + var anchorTarget = href.match(regexpLocalAnchorHref); if (href.length === 0) { // It's a link with an empty href, pointing to the current page: do nothing. - } else if (regexpLocalAnchorHref.test(href)) { + } else if (anchorTarget) { // It's a local anchor link : remove escapedUrl if any (see above) - anchor.setAttribute('href', href.replace(/^[^#]*/, '')); + anchor.setAttribute('href', '#' + anchorTarget[1]); } else if (anchor.protocol !== currentProtocol || anchor.host !== currentHost) { // It's an external URL : we should open it in a new tab @@ -1666,7 +1668,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys // Add an onclick event to extract this article or file from the ZIM // instead of following the link anchor.addEventListener('click', function (e) { - anchorParameter = href.match(/#([^#]+)$/); + anchorParameter = href.match(/#([^#;]+)$/); anchorParameter = anchorParameter ? anchorParameter[1] : ''; var zimUrl = uiUtil.deriveZimUrlFromRelativeUrl(uriComponent, baseUrl); goToArticle(zimUrl, downloadAttrValue, contentType); From e32bf376fbfb533fe5a9e159072a071ce95bdc86 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 23 Jan 2022 15:12:00 +0000 Subject: [PATCH 07/20] Ignore javascript links --- www/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/app.js b/www/js/app.js index be526df7c..b9c1d3051 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1637,7 +1637,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys return; } var href = anchor.getAttribute('href'); - if (href === null || href === undefined) return; + if (href === null || href === undefined || /^javascript:/i.test(anchor.protocol)) return; var anchorTarget = href.match(regexpLocalAnchorHref); if (href.length === 0) { // It's a link with an empty href, pointing to the current page: do nothing. From bacb852a4d704ac98366755754a67ed8aaf158e4 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 23 Jan 2022 20:18:18 +0000 Subject: [PATCH 08/20] Align function in uiUtil with same function in Service Worker --- www/js/lib/uiUtil.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 6f89bbd6a..cca509c34 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -103,15 +103,19 @@ define(rqDef, function(settingsStore) { link.parentNode.replaceChild(cssElement, link); } - var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/); - /** * Removes parameters and anchors from a URL - * @param {type} url - * @returns {String} same URL without its parameters and anchors + * @param {type} url The URL to be processed + * @returns {String} The same URL without its parameters and anchors */ function removeUrlParameters(url) { - return url.replace(regexpRemoveUrlParameters, "$1"); + // Remove any querystring + var strippedUrl = url.replace(/\?[^?]*$/, ''); + // Remove any anchor parameters - note that IN PRACTICE anchor parameters cannot contain a semicolon because JavaScript maintains + // compatibility with HTML4, so we can avoid accidentally stripping e.g. ' by excluding an anchor if any semicolon is found + // between it and the end of the string. See https://stackoverflow.com/a/79022/9727685. + strippedUrl = strippedUrl.replace(/#[^#;]*$/, ''); + return strippedUrl; } /** From bece1a7abb3a1b72fb7f1ffd8e19d1f4be3a5370 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Tue, 25 Jan 2022 22:32:27 +0000 Subject: [PATCH 09/20] Use URL function in Service Worker --- service-worker.js | 35 ++++++++++++++--------------------- tests/tests.js | 19 +++++++++++-------- www/js/app.js | 4 ++-- www/js/lib/uiUtil.js | 4 +--- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/service-worker.js b/service-worker.js index 8913f2e62..34deeb8c8 100644 --- a/service-worker.js +++ b/service-worker.js @@ -278,19 +278,15 @@ let fetchCaptureEnabled = false; */ function fetchRequestFromZIM(fetchEvent) { return new Promise(function (resolve, reject) { - var nameSpace; - var title; - var titleWithNameSpace; - var regexpResult = regexpZIMUrlWithNamespace.exec(fetchEvent.request.url); - var prefix = regexpResult[1]; - nameSpace = regexpResult[2]; - title = regexpResult[3]; - // We need to remove the potential parameters in the URL. Note that titles may contain question marks or hashes, so we test the // encoded URI before decoding it. Be sure that you haven't encoded any querystring along with the URL, e.g. for clicked links. - title = decodeURIComponent(removeUrlParameters(title)); + var strippedUrl = decodeURIComponent(new URL(fetchEvent.request.url).pathname); + var partsOfZIMUrl = regexpZIMUrlWithNamespace.exec(strippedUrl); + var prefix = partsOfZIMUrl[1]; + var nameSpace = partsOfZIMUrl[2]; + var title = partsOfZIMUrl[3]; - titleWithNameSpace = nameSpace + '/' + title; + var titleWithNameSpace = nameSpace + '/' + title; // Let's instantiate a new messageChannel, to allow app.js to give us the content var messageChannel = new MessageChannel(); @@ -335,18 +331,15 @@ function fetchRequestFromZIM(fetchEvent) { } /** - * Removes parameters and anchors from a URL - * @param {type} url The URL to be processed - * @returns {String} The same URL without its parameters and anchors + * Parses a fully qualified URL. Note that relative URLs cannot be used with this method, because we do not have access + * to the base URL, and using a dummy URL will strip any relative path information, returning an incorrect result. + * @param {String} encodedUrl The URI-encoded fully qualified URL string to be processed (must include protocol and domain). + * Note that the path and parameters must be URI-encoded, but parameter separators at the end of the URL (? & = ; #) + * must not be encoded. + * @returns {URL} A URL object with properties such as 'pathname', 'search', and 'anchor'. Pathname is URI-encoded. */ -function removeUrlParameters(url) { - // Remove any querystring - var strippedUrl = url.replace(/\?[^?]*$/, ''); - // Remove any anchor parameters - note that IN PRACTICE anchor parameters cannot contain a semicolon because JavaScript maintains - // compatibility with HTML4, so we can avoid accidentally stripping e.g. ' by excluding an anchor if any semicolon is found - // between it and the end of the string. See https://stackoverflow.com/a/79022/9727685. - strippedUrl = strippedUrl.replace(/#[^#;]*$/, ''); - return strippedUrl; +function parseUrlParameters(encodedUrl) { + return new URL(encodedUrl); } /** diff --git a/tests/tests.js b/tests/tests.js index 110af528e..fca69ab90 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -118,14 +118,17 @@ define(['jquery', 'zimArchive', 'zimDirEntry', 'util', 'uiUtil', 'utf8'], assert.equal(util.allCaseFirstLetters(testString6, "full").indexOf("ΚΑΛΆ ΝΕΡΆ ΜΑΓΝΗΣΊΑ ŽIŽEK") >= 0, true, "All Unicode letters should be uppercase"); }); QUnit.test("check removal of parameters in URL", function(assert) { - var testUrl1 = "A/question.html"; - var testUrl2 = "A/question.html?param1=toto¶m2=titi"; - var testUrl3 = "A/question.html?param1=toto¶m2=titi#anchor"; - var testUrl4 = "A/question.html#anchor"; - assert.equal(uiUtil.removeUrlParameters(testUrl1), testUrl1); - assert.equal(uiUtil.removeUrlParameters(testUrl2), testUrl1); - assert.equal(uiUtil.removeUrlParameters(testUrl3), testUrl1); - assert.equal(uiUtil.removeUrlParameters(testUrl4), testUrl1); + var baseUrl = "A/Che cosa è l'amore?.html"; + var testUrls = [ + "A/Che%20cosa%20%C3%A8%20l'amore%3F.html?param1=toto¶m2=titi", + "A/Che%20cosa%20%C3%A8%20l'amore%3F.html?param1=toto¶m2=titi#anchor", + "A/Che%20cosa%20%C3%A8%20l'amore%3F.html#anchor" + ]; + testUrls.forEach(function (testUrl) { + assert.equal(decodeURIComponent( + uiUtil.removeUrlParameters(testUrl) + ), baseUrl); + }); }); QUnit.module("ZIM initialisation"); diff --git a/www/js/app.js b/www/js/app.js index b9c1d3051..359a2e7e7 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1738,7 +1738,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys Array.prototype.slice.call(iframe.querySelectorAll('link[data-kiwixurl]')).forEach(function (link) { cssCount++; var linkUrl = link.getAttribute('data-kiwixurl'); - var url = uiUtil.removeUrlParameters(decodeURIComponent(linkUrl)); + var url = decodeURIComponent(uiUtil.removeUrlParameters(linkUrl)); if (cssCache.has(url)) { var nodeContent = cssCache.get(url); if (/stylesheet/i.test(link.rel)) uiUtil.replaceCSSLinkWithInlineCSS(link, nodeContent); @@ -1793,7 +1793,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys // var script = $(this); // var scriptUrl = script.attr("data-kiwixurl"); // // TODO check that the type of the script is text/javascript or application/javascript - // var title = uiUtil.removeUrlParameters(decodeURIComponent(scriptUrl)); + // var title = uiUtil.removeUrlParameters(scriptUrl); // selectedArchive.getDirEntryByPath(title).then(function(dirEntry) { // if (dirEntry === null) { // console.log("Error: js file not found: " + title); diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index cca509c34..65d93e401 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -111,9 +111,7 @@ define(rqDef, function(settingsStore) { function removeUrlParameters(url) { // Remove any querystring var strippedUrl = url.replace(/\?[^?]*$/, ''); - // Remove any anchor parameters - note that IN PRACTICE anchor parameters cannot contain a semicolon because JavaScript maintains - // compatibility with HTML4, so we can avoid accidentally stripping e.g. ' by excluding an anchor if any semicolon is found - // between it and the end of the string. See https://stackoverflow.com/a/79022/9727685. + // Remove any anchor parameters - note that we are deliberately excluding entity references, e.g. '''. strippedUrl = strippedUrl.replace(/#[^#;]*$/, ''); return strippedUrl; } From bd550f15ff3cefb8486ec325d0af55788be49809 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Tue, 25 Jan 2022 22:37:44 +0000 Subject: [PATCH 10/20] Clean up --- service-worker.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/service-worker.js b/service-worker.js index 34deeb8c8..447513dad 100644 --- a/service-worker.js +++ b/service-worker.js @@ -330,18 +330,6 @@ function fetchRequestFromZIM(fetchEvent) { }); } -/** - * Parses a fully qualified URL. Note that relative URLs cannot be used with this method, because we do not have access - * to the base URL, and using a dummy URL will strip any relative path information, returning an incorrect result. - * @param {String} encodedUrl The URI-encoded fully qualified URL string to be processed (must include protocol and domain). - * Note that the path and parameters must be URI-encoded, but parameter separators at the end of the URL (? & = ; #) - * must not be encoded. - * @returns {URL} A URL object with properties such as 'pathname', 'search', and 'anchor'. Pathname is URI-encoded. - */ -function parseUrlParameters(encodedUrl) { - return new URL(encodedUrl); -} - /** * Looks up a Request in a cache and returns a Promise for the matched Response * @param {String} cache The name of the cache to look in From ce15388f1110546c788de36223f8b9e7ca600fe5 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Tue, 25 Jan 2022 23:53:08 +0000 Subject: [PATCH 11/20] Finesse querystrings in cache --- service-worker.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/service-worker.js b/service-worker.js index 447513dad..7947352bb 100644 --- a/service-worker.js +++ b/service-worker.js @@ -190,11 +190,16 @@ let fetchCaptureEnabled = false; self.addEventListener('fetch', function (event) { // Only cache GET requests if (event.request.method !== "GET") return; - // Remove any querystring before requesting from the cache - var rqUrl = event.request.url.replace(/\?[^?]+$/i, ''); + var rqUrl = event.request.url; + // Test the URL with querystring removed (hashes are not relevant in this context) + var searchParam = decodeURIComponent(new URL(rqUrl).search); + var strippedUrl = event.request.url.replace(searchParam, ''); // Select cache depending on request format - var cache = /\.zim\//i.test(rqUrl) ? ASSETS_CACHE : APP_CACHE; + var cache = /\.zim\//i.test(strippedUrl) ? ASSETS_CACHE : APP_CACHE; if (cache === ASSETS_CACHE && !fetchCaptureEnabled) return; + // For APP_CACHE assets, we should ignore any querystring (whereas it should be conserved for ZIM assets, + // especially .js assets, where it may be significant) + if (cache === APP_CACHE) rqUrl = strippedUrl; event.respondWith( // First see if the content is in the cache fromCache(cache, rqUrl).then(function (response) { @@ -203,12 +208,12 @@ let fetchCaptureEnabled = false; }, function () { // The response was not found in the cache so we look for it in the ZIM // and add it to the cache if it is an asset type (css or js) - if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(rqUrl)) { + if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(strippedUrl)) { return fetchRequestFromZIM(event).then(function (response) { // Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) && - !regexpExcludedURLSchema.test(event.request.url)) { - event.waitUntil(updateCache(ASSETS_CACHE, event.request, response.clone())); + !regexpExcludedURLSchema.test(strippedUrl)) { + event.waitUntil(updateCache(ASSETS_CACHE, rqUrl, response.clone())); } return response; }).catch(function (msgPortData, title) { @@ -219,8 +224,8 @@ let fetchCaptureEnabled = false; // It's not an asset, or it doesn't match a ZIM URL pattern, so we should fetch it with Fetch API return fetch(event.request).then(function (response) { // If request was successful, add or update it in the cache, but be careful not to cache the ZIM archive itself! - if (!regexpExcludedURLSchema.test(rqUrl) && !/\.zim\w{0,2}$/i.test(rqUrl)) { - event.waitUntil(updateCache(APP_CACHE, event.request, response.clone())); + if (!regexpExcludedURLSchema.test(strippedUrl) && !/\.zim\w{0,2}$/i.test(strippedUrl)) { + event.waitUntil(updateCache(APP_CACHE, rqUrl, response.clone())); } return response; }).catch(function (error) { From 710ca07d39d00f04ca01e2298891a798d9c7303a Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Wed, 26 Jan 2022 08:52:16 +0000 Subject: [PATCH 12/20] Tidying and avoid repetition --- service-worker.js | 15 ++++++++------- www/js/lib/uiUtil.js | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/service-worker.js b/service-worker.js index 7947352bb..ef6650367 100644 --- a/service-worker.js +++ b/service-worker.js @@ -187,12 +187,13 @@ let fetchCaptureEnabled = false; /** * Intercept selected Fetch requests from the browser window */ - self.addEventListener('fetch', function (event) { +self.addEventListener('fetch', function (event) { // Only cache GET requests if (event.request.method !== "GET") return; var rqUrl = event.request.url; + var urlObject = new URL(rqUrl); // Test the URL with querystring removed (hashes are not relevant in this context) - var searchParam = decodeURIComponent(new URL(rqUrl).search); + var searchParam = decodeURIComponent(urlObject.search); var strippedUrl = event.request.url.replace(searchParam, ''); // Select cache depending on request format var cache = /\.zim\//i.test(strippedUrl) ? ASSETS_CACHE : APP_CACHE; @@ -209,7 +210,7 @@ let fetchCaptureEnabled = false; // The response was not found in the cache so we look for it in the ZIM // and add it to the cache if it is an asset type (css or js) if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(strippedUrl)) { - return fetchRequestFromZIM(event).then(function (response) { + return fetchUrlFromZIM(urlObject).then(function (response) { // Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) && !regexpExcludedURLSchema.test(strippedUrl)) { @@ -276,16 +277,16 @@ let fetchCaptureEnabled = false; }); /** - * Handles fetch events that need to be extracted from the ZIM + * Handles URLs that need to be extracted from the ZIM * - * @param {Event} fetchEvent The fetch event to be processed + * @param {URL} urlObject The URL object to be processed * @returns {Promise} A Promise for the Response, or rejects with the invalid message port data */ -function fetchRequestFromZIM(fetchEvent) { +function fetchUrlFromZIM(urlObject) { return new Promise(function (resolve, reject) { // We need to remove the potential parameters in the URL. Note that titles may contain question marks or hashes, so we test the // encoded URI before decoding it. Be sure that you haven't encoded any querystring along with the URL, e.g. for clicked links. - var strippedUrl = decodeURIComponent(new URL(fetchEvent.request.url).pathname); + var strippedUrl = decodeURIComponent(urlObject.pathname); var partsOfZIMUrl = regexpZIMUrlWithNamespace.exec(strippedUrl); var prefix = partsOfZIMUrl[1]; var nameSpace = partsOfZIMUrl[2]; diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 65d93e401..2b6228f83 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -127,7 +127,7 @@ define(rqDef, function(settingsStore) { function deriveZimUrlFromRelativeUrl(url, base) { // We use a dummy domain because URL API requires a valid URI var dummy = 'http://d/'; - var deriveZimUrl = function(url, base) { + var deriveZimUrl = function (url, base) { if (typeof URL === 'function') return new URL(url, base); // IE11 lacks URL API: workaround adapted from https://stackoverflow.com/a/28183162/9727685 var d = document.implementation.createHTMLDocument('t'); From ff5889c26a0102ed777f41062fad426682636eea Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Wed, 26 Jan 2022 08:55:47 +0000 Subject: [PATCH 13/20] Remove unused htmlEscapeChars --- www/js/lib/uiUtil.js | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 2b6228f83..fef45ef63 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -308,35 +308,6 @@ define(rqDef, function(settingsStore) { return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; } - /** - * DEV: This function is no longer used in the project and could be removed unless it is of historical interest. - * It has been superseded by encodeURIComponent which encodes all characters except A-Z a-z 0-9 - _ . ! ~ * ' ( ) - * The only character from below that is not encoded is apostrophe ('), but this does not need to be encoded to - * show correctly in our UI, given that it is an allowed character in bare URIs and the dirEntryId is enclosed - * in double quote marks ("..."). This has been successfully tested on titles with apostrophes. - * - * Encodes the html escape characters in the string before using it as html class name,id etc. - * - * @param {String} string The string in which html characters are to be escaped - * @returns {String} The escaped HTML string - */ - function htmlEscapeChars(string) { - var escapechars = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }; - string = String(string).replace(/[&<>"'`=/]/g, function (s) { - return escapechars[s]; - }); - return string; - } - /** * Removes the animation effect between various sections */ @@ -529,7 +500,6 @@ define(rqDef, function(settingsStore) { checkServerIsAccessible: checkServerIsAccessible, spinnerDisplay: spinnerDisplay, isElementInView: isElementInView, - htmlEscapeChars: htmlEscapeChars, removeAnimationClasses: removeAnimationClasses, applyAnimationToSection: applyAnimationToSection, applyAppTheme: applyAppTheme, From fca24d5622618d4f67abe49e35782d9abc952cb5 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Fri, 28 Jan 2022 10:21:27 +0000 Subject: [PATCH 14/20] Rationalize code and prevent caching not found responses --- service-worker.js | 28 ++++++++++++++-------------- www/js/app.js | 7 +++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/service-worker.js b/service-worker.js index ef6650367..f4108ae8d 100644 --- a/service-worker.js +++ b/service-worker.js @@ -192,14 +192,13 @@ self.addEventListener('fetch', function (event) { if (event.request.method !== "GET") return; var rqUrl = event.request.url; var urlObject = new URL(rqUrl); - // Test the URL with querystring removed (hashes are not relevant in this context) - var searchParam = decodeURIComponent(urlObject.search); - var strippedUrl = event.request.url.replace(searchParam, ''); + // Test the URL with parameters removed + var strippedUrl = urlObject.pathname; // Select cache depending on request format var cache = /\.zim\//i.test(strippedUrl) ? ASSETS_CACHE : APP_CACHE; if (cache === ASSETS_CACHE && !fetchCaptureEnabled) return; // For APP_CACHE assets, we should ignore any querystring (whereas it should be conserved for ZIM assets, - // especially .js assets, where it may be significant) + // especially .js assets, where it may be significant). Anchor targets are irreleveant in this context. if (cache === APP_CACHE) rqUrl = strippedUrl; event.respondWith( // First see if the content is in the cache @@ -277,17 +276,17 @@ self.addEventListener('fetch', function (event) { }); /** - * Handles URLs that need to be extracted from the ZIM + * Handles URLs that need to be extracted from the ZIM archive * - * @param {URL} urlObject The URL object to be processed + * @param {URL} urlObject The URL object to be processed for extraction from the ZIM * @returns {Promise} A Promise for the Response, or rejects with the invalid message port data */ function fetchUrlFromZIM(urlObject) { return new Promise(function (resolve, reject) { - // We need to remove the potential parameters in the URL. Note that titles may contain question marks or hashes, so we test the - // encoded URI before decoding it. Be sure that you haven't encoded any querystring along with the URL, e.g. for clicked links. - var strippedUrl = decodeURIComponent(urlObject.pathname); - var partsOfZIMUrl = regexpZIMUrlWithNamespace.exec(strippedUrl); + // Note that titles may contain bare question marks or hashes, so we must use only the pathname without any URL parameters. + // Be sure that you haven't encoded any querystring along with the URL. + var barePathname = decodeURIComponent(urlObject.pathname); + var partsOfZIMUrl = regexpZIMUrlWithNamespace.exec(barePathname); var prefix = partsOfZIMUrl[1]; var nameSpace = partsOfZIMUrl[2]; var title = partsOfZIMUrl[3]; @@ -344,7 +343,7 @@ function fetchUrlFromZIM(urlObject) { */ function fromCache(cache, requestUrl) { // Prevents use of Cache API if user has disabled it - if (!useAppCache && cache === APP_CACHE || !useAssetsCache && cache === ASSETS_CACHE) return Promise.reject('disabled'); + if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) return Promise.reject('disabled'); return caches.open(cache).then(function (cacheObj) { return cacheObj.match(requestUrl).then(function (matching) { if (!matching || matching.status === 404) { @@ -359,15 +358,16 @@ function fromCache(cache, requestUrl) { /** * Stores or updates in a cache the given Request/Response pair * @param {String} cache The name of the cache to open - * @param {Request} request The original Request object + * @param {Request|String} request The original Request object or the URL string requested * @param {Response} response The Response received from the server/ZIM * @returns {Promise} A Promise for the update action */ function updateCache(cache, request, response) { // Prevents use of Cache API if user has disabled it - if (!useAppCache && cache === APP_CACHE || !useAssetsCache && cache === ASSETS_CACHE) return Promise.resolve(); + if (!response.ok || !(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) + return Promise.resolve(); return caches.open(cache).then(function (cacheObj) { - console.debug('[SW] Adding ' + request.url + ' to ' + cache + '...'); + console.debug('[SW] Adding ' + (request.url || request) + ' to ' + cache + '...'); return cacheObj.put(request, response); }); } diff --git a/www/js/app.js b/www/js/app.js index 359a2e7e7..8e72f7d49 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -710,9 +710,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys } return; } - // Because the "outer" Service Worker still runs in a PWA app, we don't actually disable the SW in this context, but it will no longer - // be intercepting requests - if ('serviceWorker' in navigator) { + // Because the Service Worker must still run in a PWA app so that it can work offline, we don't actually disable the SW in this context, + // but it will no longer be intercepting requests for ZIM assets (only requests for the app's own code) + if (isServiceWorkerAvailable()) { serviceWorkerRegistration = null; } refreshAPIStatus(); @@ -1316,7 +1316,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys findDirEntryFromDirEntryIdAndLaunchArticleRead(dirEntryId); return false; } - /** * Creates an instance of DirEntry from given dirEntryId (including resolving redirects), From fb107695f4a24673ab2dd25c7cf29c245f8168e7 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sat, 29 Jan 2022 07:23:21 +0000 Subject: [PATCH 15/20] Cache 404 responses --- service-worker.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/service-worker.js b/service-worker.js index f4108ae8d..8d8924cc9 100644 --- a/service-worker.js +++ b/service-worker.js @@ -346,9 +346,7 @@ function fromCache(cache, requestUrl) { if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) return Promise.reject('disabled'); return caches.open(cache).then(function (cacheObj) { return cacheObj.match(requestUrl).then(function (matching) { - if (!matching || matching.status === 404) { - return Promise.reject('no-match'); - } + if (!matching) return Promise.reject('no-match'); console.debug('[SW] Supplying ' + requestUrl + ' from ' + cache + '...'); return matching; }); @@ -364,7 +362,7 @@ function fromCache(cache, requestUrl) { */ function updateCache(cache, request, response) { // Prevents use of Cache API if user has disabled it - if (!response.ok || !(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) + if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) return Promise.resolve(); return caches.open(cache).then(function (cacheObj) { console.debug('[SW] Adding ' + (request.url || request) + ' to ' + cache + '...'); From 9612e82b582f208026a042ac3290421068e86373 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sat, 29 Jan 2022 10:27:39 +0000 Subject: [PATCH 16/20] Revert "Cache 404 responses" --- service-worker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service-worker.js b/service-worker.js index 8d8924cc9..0f0124ec6 100644 --- a/service-worker.js +++ b/service-worker.js @@ -346,7 +346,7 @@ function fromCache(cache, requestUrl) { if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) return Promise.reject('disabled'); return caches.open(cache).then(function (cacheObj) { return cacheObj.match(requestUrl).then(function (matching) { - if (!matching) return Promise.reject('no-match'); + if (!matching || matching.status === 404) return Promise.reject('no-match'); console.debug('[SW] Supplying ' + requestUrl + ' from ' + cache + '...'); return matching; }); @@ -362,7 +362,7 @@ function fromCache(cache, requestUrl) { */ function updateCache(cache, request, response) { // Prevents use of Cache API if user has disabled it - if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) + if (!response.ok || !(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) return Promise.resolve(); return caches.open(cache).then(function (cacheObj) { console.debug('[SW] Adding ' + (request.url || request) + ' to ' + cache + '...'); From d1c0fa21765921ae773428dc30d49d8a92a03828 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sat, 29 Jan 2022 11:26:18 +0000 Subject: [PATCH 17/20] Fix potential invalid messaging --- service-worker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service-worker.js b/service-worker.js index 0f0124ec6..e86eb8813 100644 --- a/service-worker.js +++ b/service-worker.js @@ -216,8 +216,8 @@ self.addEventListener('fetch', function (event) { event.waitUntil(updateCache(ASSETS_CACHE, rqUrl, response.clone())); } return response; - }).catch(function (msgPortData, title) { - console.error('Invalid message received from app.js for ' + title, msgPortData); + }).catch(function (msgPortData) { + console.error('Invalid message received from app.js for ' + strippedUrl, msgPortData); return msgPortData; }); } else { From 1e0961707d053107aa184e49061411f7a09e66fe Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sun, 30 Jan 2022 13:17:07 +0000 Subject: [PATCH 18/20] Fix exclusion of certain URL schemata --- service-worker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service-worker.js b/service-worker.js index e86eb8813..eba777112 100644 --- a/service-worker.js +++ b/service-worker.js @@ -212,7 +212,7 @@ self.addEventListener('fetch', function (event) { return fetchUrlFromZIM(urlObject).then(function (response) { // Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) && - !regexpExcludedURLSchema.test(strippedUrl)) { + !regexpExcludedURLSchema.test(event.request.url)) { event.waitUntil(updateCache(ASSETS_CACHE, rqUrl, response.clone())); } return response; @@ -224,7 +224,7 @@ self.addEventListener('fetch', function (event) { // It's not an asset, or it doesn't match a ZIM URL pattern, so we should fetch it with Fetch API return fetch(event.request).then(function (response) { // If request was successful, add or update it in the cache, but be careful not to cache the ZIM archive itself! - if (!regexpExcludedURLSchema.test(strippedUrl) && !/\.zim\w{0,2}$/i.test(strippedUrl)) { + if (!regexpExcludedURLSchema.test(event.request.url) && !/\.zim\w{0,2}$/i.test(strippedUrl)) { event.waitUntil(updateCache(APP_CACHE, rqUrl, response.clone())); } return response; From e4503f071a04f5987681655ad9f80f72eedb4cfa Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Mon, 31 Jan 2022 18:21:52 +0000 Subject: [PATCH 19/20] Cleanup after rebase --- www/js/app.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 8e72f7d49..0ae904426 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1724,16 +1724,14 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys // These sections can be opened by clicking on them, but this is done with some javascript. // The code below is a workaround we still need for compatibility with ZIM files generated by mwoffliner in 2018. // A better fix has been made for more recent ZIM files, with the use of noscript tags : see https://github.com/openzim/mwoffliner/issues/324 - var iframe = document.getElementById('articleContent').contentDocument; + var iframe = iframeArticleContent.contentDocument; var collapsedBlocks = iframe.querySelectorAll('.collapsible-block:not(.open-block), .collapsible-heading:not(.open-block)'); // Using decrementing loop to optimize performance : see https://stackoverflow.com/questions/3520688 for (var i = collapsedBlocks.length; i--;) { collapsedBlocks[i].classList.add('open-block'); } - var cssCount = 0; var cssFulfilled = 0; - var iframe = iframeArticleContent.contentDocument; Array.prototype.slice.call(iframe.querySelectorAll('link[data-kiwixurl]')).forEach(function (link) { cssCount++; var linkUrl = link.getAttribute('data-kiwixurl'); @@ -1746,6 +1744,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys } else { if (params.assetsCache) $('#cachingAssets').show(); selectedArchive.getDirEntryByPath(url).then(function (dirEntry) { + if (!dirEntry) throw 'DirEntry null or undefined'; var mimetype = dirEntry.getMimetype(); var readFile = /^text\//i.test(mimetype) ? selectedArchive.readUtf8File : selectedArchive.readBinaryFile; return readFile(dirEntry, function (fileDirEntry, content) { From 133c259f12875c010a482f03125218e556a3eb4f Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Mon, 31 Jan 2022 20:25:25 +0000 Subject: [PATCH 20/20] Further cleanup after rebase --- www/js/app.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/www/js/app.js b/www/js/app.js index 0ae904426..ee2b4233e 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1744,7 +1744,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys } else { if (params.assetsCache) $('#cachingAssets').show(); selectedArchive.getDirEntryByPath(url).then(function (dirEntry) { - if (!dirEntry) throw 'DirEntry null or undefined'; + if (!dirEntry) { + cssCache.set(url, ''); // Prevent repeated lookups of this unfindable asset + throw 'DirEntry ' + typeof dirEntry; + } var mimetype = dirEntry.getMimetype(); var readFile = /^text\//i.test(mimetype) ? selectedArchive.readUtf8File : selectedArchive.readBinaryFile; return readFile(dirEntry, function (fileDirEntry, content) { @@ -1756,7 +1759,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys renderIfCSSFulfilled(fileDirEntry.url); }); }).catch(function (e) { - console.error("could not find DirEntry for CSS : " + url, e); + console.error("Could not find DirEntry for link element: " + url, e); cssCount--; renderIfCSSFulfilled(); });