--- a
+++ b/background.js
@@ -0,0 +1,761 @@
+/************************************************************************/
+/* */
+/* Save Page WE - Generic WebExtension - Background Page */
+/* */
+/* Javascript for Background Page */
+/* */
+/* Last Edit - 27 Oct 2017 */
+/* */
+/* Copyright (C) 2016-2017 DW-dev */
+/* */
+/* Distributed under the GNU General Public License version 2 */
+/* See LICENCE.txt file and http://www.gnu.org/licenses/ */
+/* */
+/************************************************************************/
+
+/************************************************************************/
+/* */
+/* Refer to Google Chrome developer documentation: */
+/* */
+/* https://developer.chrome.com/extensions/overview */
+/* https://developer.chrome.com/extensions/content_scripts */
+/* https://developer.chrome.com/extensions/messaging */
+/* https://developer.chrome.com/extensions/xhr */
+/* https://developer.chrome.com/extensions/contentSecurityPolicy */
+/* */
+/* https://developer.chrome.com/extensions/manifest */
+/* https://developer.chrome.com/extensions/declare_permissions */
+/* https://developer.chrome.com/extensions/match_patterns */
+/* */
+/* https://developer.chrome.com/extensions/browserAction */
+/* https://developer.chrome.com/extensions/contextMenus */
+/* https://developer.chrome.com/extensions/downloads */
+/* https://developer.chrome.com/extensions/notifications */
+/* https://developer.chrome.com/extensions/permissions */
+/* https://developer.chrome.com/extensions/runtime */
+/* https://developer.chrome.com/extensions/storage */
+/* https://developer.chrome.com/extensions/tabs */
+/* */
+/* Refer to IETF data uri and mime type documentation: */
+/* */
+/* RFC 2397 - https://tools.ietf.org/html/rfc2397 */
+/* RFC 2045 - https://tools.ietf.org/html/rfc2045 */
+/* */
+/************************************************************************/
+
+/************************************************************************/
+/* */
+/* Notes on Save Page WE Operation */
+/* */
+/* 1. The basic approach is to traverse the DOM tree three times. */
+/* */
+/* 2. The current states of the HTML elements are extracted from */
+/* the DOM tree. External resources are downloaded and scanned. */
+/* */
+/* 3. The first pass gathers external style sheet resources: */
+/* */
+/* - <style> element: find style sheet url()'s in @import rules, */
+/* then remember locations. */
+/* */
+/* - <link rel="stylesheet" href="..."> element: find style sheet */
+/* url()'s in @import rules, then remember locations. */
+/* */
+/* 4. After the first pass, the referenced external style sheets are */
+/* downloaded from the remembered locations. */
+/* */
+/* 5. The second pass gathers external script/font/image resources: */
+/* */
+/* - <script> element: remember location from src attribute. */
+/* */
+/* - <link rel="icon" href="..."> element: remember location */
+/* from href attribute. */
+/* */
+/* - <img> element: remember location from src attribute. */
+/* */
+/* if just saving currently displayed CSS images: */
+/* */
+/* - all elements: find url()'s in CSS computed style for element */
+/* and for before/after pseudo-elements and remember locations. */
+/* */
+/* otherwise, if saving all CSS images: */
+/* */
+/* - style attribute on any element: find image url()'s in CSS */
+/* rules and remember locations. */
+/* */
+/* - <style> element: handle @import rules, then find font and */
+/* image url()'s in CSS rules and remember locations. */
+/* */
+/* - <link rel="stylesheet" href="..."> element: handle @import */
+/* rules, then find font and image url()'s in CSS rules and */
+/* remember locations. */
+/* */
+/* 6. After the second pass, the referenced external resources are */
+/* downloaded from the remembered locations. */
+/* */
+/* 7. The third pass generates HTML and data uri's: */
+/* */
+/* - style attribute on any element: replace image url()'s in */
+/* CSS rules with data uri's. */
+/* */
+/* - <script> element: Javascript is not changed. */
+/* */
+/* - <script src="..."> element: convert Javascript to data uri */
+/* and use this to replace url in src attribute. */
+/* */
+/* - <style> element: handle @import rules, then replace font and */
+/* image url()'s in CSS rules with data uri's. */
+/* */
+/* - <link rel="stylesheet" href="..."> element: handle @import */
+/* rules, then replace font and image url()'s in CSS rules */
+/* with data uri's, then enclose in new <style> element and */
+/* replace original <link> element. */
+/* */
+/* - <link rel="icon" href="..."> element: convert icon to data */
+/* uri and use this to replace url in href attribute. */
+/* */
+/* - <base href="..." target="..."> element: remove existing */
+/* base element (if any) and insert new base element with href */
+/* attribute set to document.baseURI and target attribute set */
+/* to the same value as for removed base element (if any). */
+/* */
+/* - <body background="..."> element: convert image to data uri */
+/* and use this to replace url in background attribute. */
+/* */
+/* - <img src="..."> element: convert image to data uri and use */
+/* this to replace url in src attribute. */
+/* */
+/* - <img srcset="..."> element: replace list of images by "". */
+/* */
+/* - <input type="image" src="..."> element: convert image to */
+/* data uri and use this to replace url in src attribute. */
+/* */
+/* - <input type="file"> or <input type="password"> element: */
+/* no changes made to maintain security. */
+/* */
+/* - <input type="checkbox"> or <input type="radio"> element: */
+/* add or remove checked attribute depending on the value of */
+/* element.checked reflecting any user changes. */
+/* */
+/* - <input type="-other-"> element: add value attribute set to */
+/* element.value reflecting any user changes. */
+/* */
+/* - <audio src="..."> element: if current source, convert audio */
+/* to data uri and use this to replace url in src attribute. */
+/* */
+/* - <video src="..."> element: if current source, convert video */
+/* to data uri and use this to replace url in src attribute. */
+/* */
+/* - <video poster="..."> element: convert image to data uri and */
+/* use this to replace url in poster attribute. */
+/* */
+/* - <source src="..."> element: if current source, convert audio */
+/* or video to data uri and use this to replace url in src */
+/* attribute. */
+/* */
+/* - <track src="..."> element: convert subtitles to data uri and */
+/* use this to replace url in src attribute. */
+/* */
+/* - <object data="..."> element: convert binary data to data uri */
+/* and use these to replace url in data attribute. */
+/* */
+/* - <embed src="..."> element: convert binary data to data uri */
+/* and use this to replace url in src attribute. */
+/* */
+/* - <frame src="..."> or <iframe src="..."> element: process */
+/* sub-tree to extract HTML, then convert HTML to data uri and */
+/* use this to replace url in src attribute. */
+/* */
+/* - other elements: process child nodes to extract HTML. */
+/* */
+/* - text nodes: escape '<' and '>' characters. */
+/* */
+/* - comment nodes: enclose within <!-- and --> */
+/* */
+/* 8. Data URI syntax and defaults: */
+/* */
+/* - data:[<media type>][;base64],<encoded data> */
+/* */
+/* - where <media type> is: <mime type>[;charset=<charset>] */
+/* */
+/* - default for text content: text/plain;charset=US-ASCII */
+/* */
+/* - default for binary content: application/octet-stream;base64 */
+/* */
+/************************************************************************/
+
+/************************************************************************/
+/* */
+/* Potential Improvements */
+/* */
+/* 1. The main document and <frame> and <iframe> documents could be */
+/* downloaded and scanned to extract the original states of the */
+/* HTML elements, as an alternative to the current states. */
+/* */
+/* 2. <script src="..."> element could be converted to <script> */
+/* element to avoid data uri in href attribute, which would also */
+/* avoid using encodeURIComponent(), but any 'async' or 'defer' */
+/* attributes would be lost and the order of execution of scripts */
+/* could change. */
+/* */
+/************************************************************************/
+
+/************************************************************************/
+/* */
+/* Handling of URL's in HTML Attributes */
+/* */
+/* Element Attribute HTML Content Handling */
+/* */
+/* <a> href 4 5 - - */
+/* <applet> codebase 4 java - */
+/* <area> href 4 5 - - */
+/* <audio> src 5 audio data uri (1) */
+/* <base> href 4 5 - - */
+/* <blockquote> cite 4 5 info - */
+/* <body> background 4 image data uri */
+/* <button> formaction 5 - - */
+/* <del> cite 4 5 info - */
+/* <embed> src 5 data data uri */
+/* <form> action 4 5 - - */
+/* <frame> longdesc 4 info - */
+/* <frame> src 4 html data uri */
+/* <head> profile 4 metadata - */
+/* <html> manifest 5 - - */
+/* <iframe> longdesc 4 info - */
+/* <iframe> src 4 5 html data uri */
+/* <img> longdesc 4 info - */
+/* <img> src 4 5 image data uri (2) */
+/* <img> srcset 5 images - (2) */
+/* <input> formaction 5 - - */
+/* <input> src 4 5 image data uri */
+/* <ins> cite 4 5 info - */
+/* <link> href 4 5 css style (3) */
+/* <link> href 4 5 icon data uri */
+/* <object> archive 4 - - */
+/* <object> classid 4 - - */
+/* <object> codebase 4 - - */
+/* <object> data 4 5 data data uri */
+/* <q> cite 4 5 info - */
+/* <script> src 4 5 javscript data uri */
+/* <source> src 5 audio/video data uri (1) */
+/* <track> src 5 audio/video data uri */
+/* <video> poster 5 image data uri */
+/* <video> src 5 video data uri (1) */
+/* */
+/* Notes: */
+/* */
+/* (1) data uri is created only if the URL in the 'src' attribute */
+/* is the same as the URL in element.currentSrc of the related */
+/* <audio> or <video> element. */
+/* */
+/* (2) data uri is created for the URL in the 'src' attribute, but */
+/* not for URL's in the 'srcset' attribute. */
+/* */
+/* (3) replace <link> element with <style> element containing the */
+/* style sheet referred to by the URL in the 'href' attribute. */
+/* */
+/************************************************************************/
+
+/************************************************************************/
+/* */
+/* Handling of Binary Data and Characters */
+/* */
+/* 1. Files downloaded by XMLHttpRequest GET request are received */
+/* as a Uint8Array (8-bit unsigned integers) representing: */
+/* - either binary data (image, font, audio or video) */
+/* - or encoded characters (style sheets or scripts) */
+/* */
+/* 2. The Uint8Array is then converted to a Javascript string */
+/* (16-bit unsigned integers) containing 8-bit unsigned values */
+/* (a binary string) which is sent to the content script. */
+/* */
+/* 3. A binary string containing binary data is copied directly */
+/* into the resourceContent store. */
+/* */
+/* 4. A binary string containing UTF-8 characters is converted to */
+/* a normal Javascript string (containing UTF-16 characters) */
+/* before being copied into the resourceContent store. */
+/* */
+/* 5. A binary string containing non-UTF-8 (ASCII, ANSI, ISO-8859-1) */
+/* characters is copied directly into the resourceContent store. */
+/* */
+/* 6. When creating a Base64 data uri, the binary string from the */
+/* resourceContent store is converted to a Base64 ASCII string */
+/* using btoa(). */
+/* */
+/* 7. When creating a UTF-8 data uri, the UTF-16 string from the */
+/* resourceContent store is converted to a UTF-8 %-escaped */
+/* string using encodeURIComponent(). The following characters */
+/* are not %-escaped: alphabetic, digits, - _ . ! ~ * ' ( ) */
+/* */
+/* 8. Character encodings are determined as follows: */
+/* - UTF-8 Byte Order Mark (BOM) at the start of a text file */
+/* - charset parameter in the HTTP Content-Type header field */
+/* - @charset rule at the start of a style sheet */
+/* - charset attribute on an element referencing a text file */
+/* - charset encoding of the parent document or style sheet */
+/* */
+/************************************************************************/
+
+"use strict";
+
+/************************************************************************/
+
+/* Global variables */
+
+var isFirefox;
+var ffVersion;
+
+var buttonAction;
+var showSubmenu;
+
+var badgeTabId;
+
+/************************************************************************/
+
+/* Initialize on browser startup */
+
+isFirefox = (navigator.userAgent.indexOf("Firefox") >= 0);
+
+chrome.storage.local.set({ "environment-isfirefox": isFirefox });
+
+if (isFirefox)
+{
+ chrome.runtime.getBrowserInfo(
+ function(info)
+ {
+ ffVersion = info.version.substr(0,info.version.indexOf("."));
+
+ initialize();
+ });
+}
+else initialize();
+
+function initialize()
+{
+ chrome.storage.local.get(null,
+ function(object)
+ {
+ var context;
+
+ /* Initialize or migrate options */
+
+ if (!("options-buttonaction" in object)) object["options-buttonaction"] =
+ ("options-savebuttonaction" in object) ? object["options-savebuttonaction"] : 0; /* Version 2.0-2.1 */
+
+ if (!("options-showsubmenu" in object)) object["options-showsubmenu"] =
+ ("options-showmenuitem" in object) ? object["options-showmenuitem"] : true; /* Version 3.0-5.0 */
+
+ if (!("options-showwarning" in object)) object["options-showwarning"] = true;
+
+ if (!("options-showurllist" in object)) object["options-showurllist"] = false;
+
+ if (!("options-prefixfilename" in object)) object["options-prefixfilename"] = false;
+
+ if (!("options-suffixfilename" in object)) object["options-suffixfilename"] = false;
+
+ if (!("options-usepageloader" in object)) object["options-usepageloader"] = true;
+
+ if (!("options-removeunsavedurls" in object)) object["options-removeunsavedurls"] = true;
+
+ if (!("options-includeinfobar" in object)) object["options-includeinfobar"] =
+ ("options-includenotification" in object) ? object["options-includenotification"] : false; /* Version 7.4 */
+
+
+ if (!("options-includesummary" in object)) object["options-includesummary"] = false;
+
+ if (!("options-savehtmlaudiovideo" in object)) object["options-savehtmlaudiovideo"] = false;
+
+ if (!("options-savehtmlobjectembed" in object)) object["options-savehtmlobjectembed"] = false;
+
+ if (!("options-savehtmlimagesall" in object)) object["options-savehtmlimagesall"] =
+ ("options-saveallhtmlimages" in object) ? object["options-saveallhtmlimages"] : false; /* Version 2.0-3.0 */
+
+ if (!("options-savecssimagesall" in object)) object["options-savecssimagesall"] =
+ ("options-saveallcssimages" in object) ? object["options-saveallcssimages"] : false; /* Version 2.0-3.0 */
+
+ if (!("options-savecssfontswoff" in object)) object["options-savecssfontswoff"] =
+ ("options-saveallcustomfonts" in object) ? object["options-saveallcustomfonts"] : false; /* Version 2.0-3.0 */
+
+ if (!("options-savescripts" in object)) object["options-savescripts"] =
+ ("options-saveallscripts" in object) ? object["options-saveallscripts"] : false; /* Version 2.0-3.0 */
+
+ if (!("options-maxframedepth" in object)) object["options-maxframedepth"] =
+ ("options-saveframedepth" in object) ? object["options-saveframedepth"] : 2; /* Version 2.0-2.1 */
+
+ if (!("options-maxresourcesize" in object)) object["options-maxresourcesize"] = 50;
+
+ /* Update stored options */
+
+ chrome.storage.local.set(object);
+
+ /* Initialize local options */
+
+ buttonAction = object["options-buttonaction"];
+
+ showSubmenu = object["options-showsubmenu"];
+
+ /* Add context menu items */
+
+ context = showSubmenu ? "all" : "browser_action";
+
+ chrome.contextMenus.create({ id: "currentstate", title: "Save Current State", contexts: [ context ], enabled: true });
+ chrome.contextMenus.create({ id: "chosenitems", title: "Save Chosen Items", contexts: [ context ], enabled: true });
+ chrome.contextMenus.create({ id: "completepage", title: "Save Complete Page", contexts: [ context ], enabled: true });
+ chrome.contextMenus.create({ id: "separator", type: "separator", contexts: [ context ], enabled: true });
+ chrome.contextMenus.create({ id: "viewpageinfo", title: "View Saved Page Info", contexts: [ context ], enabled: true });
+ chrome.contextMenus.create({ id: "removepageloader", title: "Remove Page Loader", contexts: [ context ], enabled: true });
+ chrome.contextMenus.create({ id: "extractmedia", title: "Extract Image/Audio/Video", contexts: [ "image", "audio", "video" ], enabled: true });
+
+ /* Set button and menu states */
+
+ chrome.tabs.query({ lastFocusedWindow: true, active: true },
+ function(tabs)
+ {
+ setButtonAndMenuStates(tabs[0].id,tabs[0].url);
+ });
+
+ /* Add listeners */
+
+ addListeners();
+ });
+}
+
+/************************************************************************/
+
+/* Add listeners */
+
+function addListeners()
+{
+ /* Storage changed listener */
+
+ chrome.storage.onChanged.addListener(
+ function(changes,areaName)
+ {
+ chrome.storage.local.get(null,
+ function(object)
+ {
+ var context;
+
+ buttonAction = object["options-buttonaction"];
+
+ showSubmenu = object["options-showsubmenu"];
+
+ if ("options-showsubmenu" in changes)
+ {
+ context = showSubmenu ? "all" : "browser_action";
+
+ chrome.contextMenus.update("currentstate",{ contexts: [ context ] });
+ chrome.contextMenus.update("chosenitems",{ contexts: [ context ] });
+ chrome.contextMenus.update("completepage",{ contexts: [ context ] });
+ chrome.contextMenus.update("separator", { contexts: [ context ] });
+ chrome.contextMenus.update("viewpageinfo", { contexts: [ context ] });
+ chrome.contextMenus.update("removepageloader", { contexts: [ context ] });
+ chrome.contextMenus.update("extractmedia", { contexts: [ "image", "audio", "video" ] });
+
+ chrome.tabs.query({ lastFocusedWindow: true, active: true },
+ function(tabs)
+ {
+ setButtonAndMenuStates(tabs[0].id,tabs[0].url);
+ });
+ }
+ });
+ });
+
+ /* Browser action listener */
+
+ chrome.browserAction.onClicked.addListener(
+ function(tab)
+ {
+ initiateAction(tab,buttonAction);
+ });
+
+ /* Context menu listener */
+
+ chrome.contextMenus.onClicked.addListener(
+ function(info,tab)
+ {
+ if (info.menuItemId == "currentstate") initiateAction(tab,0,null);
+ else if (info.menuItemId == "chosenitems") initiateAction(tab,1,null);
+ else if (info.menuItemId == "completepage") initiateAction(tab,2,null);
+ else if (info.menuItemId == "viewpageinfo") initiateAction(tab,3,null);
+ else if (info.menuItemId == "removepageloader") initiateAction(tab,4,null);
+ else if (info.menuItemId == "extractmedia") initiateAction(tab,5,info.srcUrl);
+ });
+
+ /* Tab event listeners */
+
+ chrome.tabs.onActivated.addListener( /* tab selected */
+ function(activeInfo)
+ {
+ chrome.tabs.get(activeInfo.tabId,
+ function(tab)
+ {
+ setButtonAndMenuStates(activeInfo.tabId,tab.url);
+ });
+ });
+
+ chrome.tabs.onUpdated.addListener( /* URL updated */
+ function(tabId,changeInfo,tab)
+ {
+ setButtonAndMenuStates(tabId,tab.url);
+ });
+
+ /* Web navigation listeners */
+
+ chrome.webNavigation.onCompleted.addListener( /* page loaded or (Firefox) extracted resource downloaded */
+ function(details)
+ {
+ if (details.frameId == 0)
+ {
+ /* If triggered by Extract Image/Audio/Video, details.url */
+ /* will contain resource URL, so need to get page URL */
+
+ chrome.tabs.get(details.tabId,
+ function(tab)
+ {
+ setButtonAndMenuStates(details.tabId,tab.url);
+
+ setSaveBadge("","#000000");
+ });
+ }
+ });
+
+ /* Message received listener */
+
+ chrome.runtime.onMessage.addListener(
+ function(message,sender,sendResponse)
+ {
+ var xhr = new Object();
+
+ /* Messages from content script */
+
+ switch (message.type)
+ {
+ case "downloadFile":
+ {
+ console.log("SAVEPAGE: Downloading " + message.location + " to " + message.filename);
+ var downloading = browser.downloads.download({filename: message.filename, url: message.location, saveAs: false });
+ downloading.then(function() {console.log("Download started");}, function(reason) {console.log(reason);});
+ }
+ break;
+
+ case "loadResource":
+
+ if (message.location.substr(0,6) == "https:" ||
+ (message.location.substr(0,5) == "http:" && message.pagescheme == "http:")) /* block https: resource within http: page */
+ {
+ try
+ {
+ xhr = new XMLHttpRequest();
+
+ xhr.open("GET",message.location,true);
+ xhr.setRequestHeader("Cache-Control","no-store");
+ xhr.responseType = "arraybuffer";
+ xhr.timeout = 10000;
+ xhr._tabId = sender.tab.id;
+ xhr._resourceIndex = message.index;
+ xhr.onload = onloadResource;
+ xhr.onerror = onerrorResource;
+ xhr.ontimeout = ontimeoutResource;
+
+ xhr.send(); /* throws exception if url is invalid */
+ }
+ catch(e)
+ {
+ chrome.tabs.sendMessage(sender.tab.id,{ type: "loadFailure", index: message.index });
+ }
+ }
+ else chrome.tabs.sendMessage(sender.tab.id,{ type: "loadFailure", index: message.index });
+
+ function onloadResource()
+ {
+ var i,binaryString,contentType,allowOrigin;
+ var byteArray = new Uint8Array(this.response);
+
+ if (this.status == 200)
+ {
+ binaryString = "";
+ for (i = 0; i < byteArray.byteLength; i++) binaryString += String.fromCharCode(byteArray[i]);
+
+ contentType = this.getResponseHeader("Content-Type");
+ if (contentType == null) contentType = "";
+
+ allowOrigin = this.getResponseHeader("Access-Control-Allow-Origin");
+ if (allowOrigin == null) allowOrigin = "";
+
+ chrome.tabs.sendMessage(this._tabId,{ type: "loadSuccess", index: this._resourceIndex, content: binaryString, contenttype: contentType, alloworigin: allowOrigin });
+ }
+ else chrome.tabs.sendMessage(this._tabId,{ type: "loadFailure", index: this._resourceIndex });
+ }
+
+ function onerrorResource()
+ {
+ chrome.tabs.sendMessage(this._tabId,{ type: "loadFailure", index: this._resourceIndex });
+ }
+
+ function ontimeoutResource()
+ {
+ chrome.tabs.sendMessage(this._tabId,{ type: "loadFailure", index: this._resourceIndex });
+ }
+
+ break;
+
+ case "setSaveBadge":
+
+ setSaveBadge(message.text,message.color);
+
+ break;
+ }
+ });
+}
+
+/************************************************************************/
+
+/* Initiate action function */
+
+function initiateAction(tab,menuaction,srcurl)
+{
+ if (specialPage(tab.url)) /* special page - no operations allowed */
+ {
+ alertNotify("Cannot be used with these special pages:\n" +
+ "about:, moz-extension:,\n" +
+ "https://addons.mozilla.org,\n" +
+ "chrome:, chrome-extension:,\n" +
+ "https://chrome.google.com/webstore.");
+ }
+ else if (tab.url.substr(0,5) != "file:" && menuaction >= 3) /* probably not saved page - view saved page info and extract media operations not allowed */
+ {
+ alertNotify("Cannot view saved page information or extract media files for unsaved pages.");
+ }
+ else /* normal page - save operations allowed, saved page - all operations allowed */
+ {
+ badgeTabId = tab.id;
+
+ chrome.tabs.sendMessage(tab.id,{ type: "performAction", menuaction: menuaction, srcurl: srcurl },
+ function(response)
+ {
+ if (chrome.runtime.lastError != null || typeof response == "undefined") /* no response received - content script not loaded in active tab */
+ {
+ chrome.tabs.executeScript(tab.id,{ file: "content.js" },
+ function()
+ {
+ chrome.tabs.sendMessage(tab.id,{ type: "performAction", menuaction: menuaction, srcurl: srcurl },
+ function(response)
+ {
+ if (chrome.runtime.lastError != null || typeof response == "undefined") /* no response received - content script cannot be loaded in active tab*/
+ {
+ alertNotify("Cannot be used with this page.");
+ }
+ });
+ });
+ }
+ });
+ }
+}
+
+/************************************************************************/
+
+/* Special page function */
+
+function specialPage(url)
+{
+ return (url.substr(0,6) == "about:" || url.substr(0,14) == "moz-extension:" || url.substr(0,26) == "https://addons.mozilla.org" ||
+ url.substr(0,7) == "chrome:" || url.substr(0,17) == "chrome-extension:" || url.substr(0,34) == "https://chrome.google.com/webstore");
+}
+
+/************************************************************************/
+
+/* Set button and menu states function */
+
+function setButtonAndMenuStates(tabId,url)
+{
+ if (specialPage(url)) /* special page - disable all operations */
+ {
+ chrome.browserAction.disable(tabId);
+
+ if (isFirefox && ffVersion <= 54) chrome.browserAction.setIcon({ tabId: tabId, path: "icon16-disabled.png"}); /* Firefox 54- - icon not changed */
+
+ chrome.browserAction.setTitle({ tabId: tabId, title: "Save Page WE - cannot be used with this page" });
+
+ chrome.contextMenus.update("currentstate",{ enabled: false });
+ chrome.contextMenus.update("chosenitems",{ enabled: false });
+ chrome.contextMenus.update("completepage",{ enabled: false });
+ chrome.contextMenus.update("separator", { enabled: true });
+ chrome.contextMenus.update("viewpageinfo", { enabled: false });
+ chrome.contextMenus.update("removepageloader", { enabled: false });
+ if (showSubmenu) chrome.contextMenus.update("extractmedia", { contexts: [ "image", "audio", "video" ], enabled: false });
+ else chrome.contextMenus.update("extractmedia", { contexts: [ "page_action" ], enabled: false }); /* never shown because there is no page action */
+ }
+ else if (url.substr(0,5) == "file:") /* probably saved page - enable all operations */
+ {
+ chrome.browserAction.enable(tabId);
+
+ if (isFirefox && ffVersion <= 54) chrome.browserAction.setIcon({ tabId: tabId, path: "icon16.png"}); /* Firefox 54- - icon not changed */
+
+ chrome.browserAction.setTitle({ tabId: tabId, title: "Save Page WE" });
+
+ chrome.contextMenus.update("currentstate",{ enabled: true });
+ chrome.contextMenus.update("chosenitems",{ enabled: true });
+ chrome.contextMenus.update("completepage",{ enabled: true });
+ chrome.contextMenus.update("separator", { enabled: true });
+ chrome.contextMenus.update("viewpageinfo", { enabled: true });
+ chrome.contextMenus.update("removepageloader", { enabled: true });
+ chrome.contextMenus.update("extractmedia", { contexts: [ "image", "audio", "video" ], enabled: true });
+ }
+ else /* normal page - enable save operations */
+ {
+ chrome.browserAction.enable(tabId);
+
+ if (isFirefox && ffVersion <= 54) chrome.browserAction.setIcon({ tabId: tabId, path: "icon16.png"}); /* Firefox 54- - icon not changed */
+
+ chrome.browserAction.setTitle({ tabId: tabId, title: "Save Page WE" });
+
+ chrome.contextMenus.update("currentstate",{ enabled: true });
+ chrome.contextMenus.update("chosenitems",{ enabled: true });
+ chrome.contextMenus.update("completepage",{ enabled: true });
+ chrome.contextMenus.update("separator", { enabled: true });
+ chrome.contextMenus.update("viewpageinfo", { enabled: false });
+ chrome.contextMenus.update("removepageloader", { enabled: false });
+ if (showSubmenu) chrome.contextMenus.update("extractmedia", { contexts: [ "image", "audio", "video" ], enabled: false });
+ else chrome.contextMenus.update("extractmedia", { contexts: [ "page_action" ], enabled: false }); /* never shown because there is no page action */
+ }
+}
+
+/************************************************************************/
+
+/* Set save badge function */
+
+function setSaveBadge(text,color)
+{
+ chrome.browserAction.setBadgeText({ tabId: badgeTabId, text: text });
+ chrome.browserAction.setBadgeBackgroundColor({ tabId: badgeTabId, color: color });
+}
+
+/************************************************************************/
+
+/* Display alert notification */
+
+function alertNotify(message)
+{
+ chrome.notifications.create("alert",{ type: "basic", iconUrl: "icon32.png", title: "SAVE PAGE WE", message: "" + message });
+}
+
+/************************************************************************/
+
+/* Display debug notification */
+
+function debugNotify(message)
+{
+ chrome.notifications.create("debug",{ type: "basic", iconUrl: "icon32.png", title: "SAVE PAGE WE - DEBUG", message: "" + message });
+}
+
+function debugModal(message)
+{
+ chrome.tabs.query({ lastFocusedWindow: true, active: true },
+ function(tabs)
+ {
+ chrome.tabs.executeScript(tabs[0].id,{ code: "alert('" + message + "');" });
+ });
+}
+
+/************************************************************************/