diff --git a/Readme.md b/Readme.md index 7b853a8..d94b2a2 100644 --- a/Readme.md +++ b/Readme.md @@ -1,12 +1,12 @@ Re-Pagination === -This add-on is mostly a rewrite of the old re-pagination/antipagination extension for Firefox. -Lev assigned ownership over to this project, so that this incarnation is now the official one. +This add-on is an update of the old re-pagination/antipagination extension for Firefox 57+. +Nils Maier has not assigned ownership over to this project, so this is still a fork. Features === -* Fully restartless -* Should prevent some |jank|ing compared to the original extension +* WebExtension compatible with Firefox Quantum +* Compatible with more sites * All, limited and slideshow diff --git a/_locales/de/messages.json b/_locales/de/messages.json new file mode 100644 index 0000000..8477d08 --- /dev/null +++ b/_locales/de/messages.json @@ -0,0 +1,85 @@ +{ + "displaySubmenu_label": { + "message": "Untermenü anzeigen" + }, + "displaySubmenu_desc": { + "message": "Wenn diese Option aktiviert ist (Standard), dann wird Re-Pagination als Untermenü angezeigt anstatt die verschiedenen Aktionen direkt im Kontextmenü anzuzeigen." + }, + "showslideshow_label": { + "message": "'Slide-Show' Menüeintrag anzeigen" + }, + "showalldomain_label": { + "message": "'Alle Tabs der aktuellen Domain' Menüeintrag anzeigen" + }, + "loglevel_label": { + "message": "Protokoll Stufe" + }, + "loglevel_desc": { + "message": "Die Nachrichten werden in der Fehlerkonsole protokolliert. Man sollte diese Einstellung auf 'Keine Protokollierung' belassen, wenn nicht anders angewiesen." + }, + "loglevel_none_label": { + "message": "Keine Protokollierung" + }, + "loglevel_error_label": { + "message": "Fehler protokollieren" + }, + "loglevel_info_label": { + "message": "Fehler und Infos protokollieren" + }, + "loglevel_debug_label": { + "message": "Alles protokollieren!" + }, + "menu_label": { + "message": "Re-Pagination" + }, + "repagination_nolimit": { + "message": "Alle laden" + }, + "repagination_nolimit_domain": { + "message": "'Alle Tabs der aktuellen Domain'" + }, + "repagination_limit": { + "message": "Begrenzt" + }, + "repagination_slide": { + "message": "Slide-Show" + }, + "repagination_slide_0": { + "message": "Sofort" + }, + "repagination_slide_1": { + "message": "1 Sekunde" + }, + "repagination_slide_60": { + "message": "1 Minute" + }, + "repagination_stop": { + "message": "Stoppen!" + }, + "repagination_limit_x": { + "message": "$1 Seiten" + }, + "repagination_slide_120": { + "message": "2 Minuten" + }, + "repagination_slide_x": { + "message": "$1 Sekunden" + }, + + "repagination_limited": { + "message": "($1 of $2) Re-Pagination arbeitet..." + }, + "repagination_unlimited": { + "message": "($1) Re-Pagination arbeitet..." + }, + "repagination_running": { + "message": "Re-Pagination arbeitet..." + }, + + "extensionName": { + "message": "Re-Pagination" + }, + "extensionDescription": { + "message": "Alle fortlaufenden Seiten in einem einzigem Tab auf einmal laden" + } +} \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json new file mode 100644 index 0000000..5d54487 --- /dev/null +++ b/_locales/en/messages.json @@ -0,0 +1,83 @@ +{ + "displaySubmenu_label": { + "message": "Display submenu" + }, + "displaySubmenu_desc": { + "message": "When this option is enabled (default), then Re-Pagination will be displayed as a submenu instead of displaying the various actions directly within the context menu." + }, + "showslideshow_label": { + "message": "Show 'Slideshow' menu item" + }, + "showalldomain_label": { + "message": "Show 'All tabs for current domain' menu item" + }, + "loglevel_label": { + "message": "Log level" + }, + "loglevel_desc": { + "message": "The messages will be logged to the Error Console. You should leave this at 'No Logging' unless instructed otherwise" + }, + "loglevel_none_label": { + "message": "No Logging" + }, + "loglevel_error_label": { + "message": "Log Errors" + }, + "loglevel_info_label": { + "message": "Log Errors and Infos" + }, + "loglevel_debug_label": { + "message": "Log Everything!" + }, + "menu_label": { + "message": "Re-Pagination" + }, + "repagination_nolimit": { + "message": "Load all" + }, + "repagination_nolimit_domain": { + "message": "All tabs for current domain" + }, + "repagination_limit": { + "message": "Limited" + }, + "repagination_limit_x": { + "message": "$1 pages" + }, + "repagination_slide": { + "message": "Slideshow" + }, + "repagination_slide_0": { + "message": "Immediately" + }, + "repagination_slide_1": { + "message": "1 second" + }, + "repagination_slide_x": { + "message": "$1 seconds" + }, + "repagination_slide_60": { + "message": "1 minute" + }, + "repagination_slide_120": { + "message": "2 minutes" + }, + "repagination_stop": { + "message": "Stop!" + }, + "repagination_limited": { + "message": "($1 of $2) Re-Pagination is running..." + }, + "repagination_unlimited": { + "message": "($1) Re-Pagination is running..." + }, + "repagination_running": { + "message": "Re-Pagination is running..." + }, + "extensionName": { + "message": "Re-Pagination" + }, + "extensionDescription": { + "message": "Load all consecutive pages in a single tab at once." + } +} \ No newline at end of file diff --git a/bootstrap.js b/bootstrap.js deleted file mode 100644 index eeda145..0000000 --- a/bootstrap.js +++ /dev/null @@ -1,24 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -const global = this; - -function install() {} -function uninstall() {} -function startup(data) { - Components.utils.import("chrome://repagination/content/loader.jsm"); - _setupLoader(data, function real_startup() { - require("main"); - }); -} -function shutdown(reason) { - if (reason === APP_SHUTDOWN) { - // No need to cleanup; stuff will vanish anyway - return; - } - unload("shutdown"); -} - -/* vim: set et ts=2 sw=2 : */ diff --git a/build.py b/build.py index 91f86ed..2411991 100644 --- a/build.py +++ b/build.py @@ -10,15 +10,11 @@ from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED resources = [ - "install.rdf", - "chrome.manifest", - "*.xul", - "locale/*/*.dtd", - "locale/*/*.properties", - "defaults/preferences/prefs.js", - "icon.png", "icon64.png", + "manifest.json", + "_locales/*/messages.json", + "icon.png", "icon32.png", "icon64.png", "LICENSE.txt", - "content-script.js" + "*.js", "options.html" ] destination = "repagination.xpi" @@ -34,26 +30,7 @@ def Minor(): yield -def get_js_requires(scripts): - known = set() - scripts = list(scripts) - for script in scripts: - with open(script) as sp: - for line in sp: - m = re.search(r"(?:r|lazyR)equire\((['\"])(.+?)\1", line) - if not m: - continue - m = m.group(2) + ".js" - if m in known: - continue - known.add(m) - scripts += m, - return set(scripts) - - def get_files(resources): - for r in get_js_requires(("bootstrap.js", "loader.jsm")): - yield r for r in resources: if os.path.isfile(r): yield r @@ -73,9 +50,9 @@ def __exit__(self, type, value, traceback): self.close() -if os.path.exists(destination): - print >>sys.stderr, destination, "is in the way" - sys.exit(1) +# if os.path.exists(destination): + # print >>sys.stderr, destination, "is in the way" + # sys.exit(1) with Minor(), ZipOutFile(destination) as zp: for f in sorted(get_files(resources), key=str.lower): diff --git a/chrome.manifest b/chrome.manifest deleted file mode 100644 index f574b5b..0000000 --- a/chrome.manifest +++ /dev/null @@ -1,4 +0,0 @@ -content repagination ./ - -locale repagination en-US locale/en-US/ -locale repagination de locale/de/ diff --git a/content-script.js b/content-script.js index 0e36fbd..b263df9 100644 --- a/content-script.js +++ b/content-script.js @@ -3,131 +3,58 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -/* global - addMessageListener, - content, - removeMessageListener, - sendAsyncMessage, - sendSyncMessage -*/ - -(function() { - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; -let {Services, Promise} = Cu.import("resource://gre/modules/Services.jsm", {}); -let {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); - -const [LOG_DEBUG, LOG_INFO, LOG_ERROR] = [0, 1, 2]; -const log = function(level, message, exception) { - sendAsyncMessage("repagination:log", { - level: level, - message: message, - exception: exception && { - message: exception.message, - fileName: exception.fileName, - lineNumber: exception.lineNumber, - stack: exception.stack - } - }); -}; +{ +console.log("Framescript loaded!"); +let port = browser.runtime.connect(); -const _ = function() { - return sendSyncMessage("repagination:_", { - arguments: Array.map(arguments, e => e.toString()) - })[0]; -}; +var focusElement = null; -const getFirstSnapshot = (doc, node, query) => - doc.evaluate(query, node, null, 7, null).snapshotItem(0); +let _ = function () { + let args = Array.from(arguments).map(e => e.toString()); + return browser.i18n.getMessage(args[0], args.slice(1)); +} -const checkSameOrigin = (node, tryLoadUri) => { - try { - if (!(tryLoadUri instanceof Ci.nsIURI)) { - tryLoadUri = Services.io.newURI(tryLoadUri, null, null); - } - if (tryLoadUri.schemeIs("data")) { - return true; - } - let pr = node.nodePrincipal; - pr = Cc["@mozilla.org/scriptsecuritymanager;1"]. - getService(Ci.nsIScriptSecurityManager). - getAppCodebasePrincipal(pr.URI, - pr.appId, - pr.isInBrowserElement); - if (pr.checkMayLoad.length == 3) { - pr.checkMayLoad(tryLoadUri, false, false); - } - else { - pr.checkMayLoad(tryLoadUri, false); - } - return true; - } - catch (ex) { - log(LOG_DEBUG, "denied load of " + (tryLoadUri.spec || tryLoadUri), ex); - return false; - } -}; +let equalLinks = (left, right) => + left.pathname === right.pathname && + left.search === right.search && + left.host === right.host && + left.protocol === right.protocol; -const createFrame = (window, src, allowScripts, loadFun) => { - log(LOG_INFO, "creating frame for " + src); - if (!checkSameOrigin(window.document, src)) { - throw new Error("same origin mismatch; frame creation denied"); - } - let frame = window.document.createElement("iframe"); - frame.setAttribute("sandbox", "allow-scripts"); - frame.style.display = "none"; - window.document.body.appendChild(frame); - let docShell = frame.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell); - docShell.allowImages = false; - docShell.allowPlugins = false; - docShell.allowJavascript = allowScripts; - - frame.addEventListener("load", function loadHandler() { - frame.removeEventListener("load", loadHandler, false); - log(LOG_INFO, "frame loaded, going to process"); - try { - loadFun(frame); - } - catch (ex) { - log(LOG_ERROR, "failed to invoke callback on frame", ex); - } - }, false); - frame.src = src; +let getFirstSnapshot = (doc, node, query) => + doc.evaluate(query, node, null, 7, null).snapshotItem(0); +let createPageRequest = (srcurl, allowScripts, loadFun) => { let errorCount = 0; - let errorHandler = function() { - log(LOG_INFO, "frame err'ed out"); - if (++errorCount > 5) { - frame.removeEventListener("error", errorHandler, false); - frame.removeEventListener("abort", errorHandler, false); - log(LOG_ERROR, "frame err'ed out, giving up"); - try { - loadFun(frame); - } - catch (ex) { - log(LOG_ERROR, "failed to invoke callback on frame", ex); + function sendRequest() { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + if (xhr.status == 200) { + console.info("XHR loaded, going to process"); + try { + loadFun(xhr.responseXML); + } + catch (ex) { + console.error("failed to invoke callback on XHR", ex); + } + } else if (++errorCount <= 5) { + console.info("XHR err'ed out, retrying"); + setTimeout(function(){ sendRequest(); }, 500 * (2**errorCount)); + } else { + console.error("XHR err'ed out, giving up"); } - return; - } - if (frame.history) { - frame.history.reload(); - } - else { - frame.src = src; } - }; - frame.addEventListener("error", errorHandler, false); - frame.addEventListener("abort", errorHandler, false); - - return frame; + xhr.open('GET', srcurl); + xhr.responseType = "document"; + xhr.send(); + } + sendRequest(); }; -const Repaginator = function Repaginator(focusElement, count, allowScripts, yielding) { +let Repaginator = function Repaginator(count, allowScripts, yielding) { this.pageLimit = count || 0; this.allowScripts = allowScripts; this.yielding = yielding; - this.init(focusElement); + this.init(); }; Repaginator.prototype = { slideshow: false, @@ -136,7 +63,7 @@ Repaginator.prototype = { pageCount: 0, attemptToIncrement: true, - init: function R_init(focusElement) { + init: function R_init() { // find anchor (function findInitialAnchor() { for (let parent = focusElement; parent; parent = parent.parentNode) { @@ -147,17 +74,22 @@ Repaginator.prototype = { } throw new Error("No focus element"); })(); - - this._window = focusElement.ownerDocument.defaultView; }, buildQuery: function R_buildQuery(el) { - function escapeXStr(str) { - return str.replace(/'/g, "\\'"); + function escapeXStr(mystr) { + return mystr.replace(/'/g, "\\'"); } this.query = ""; (function buildQuery() { + // Homestuck hack + if(el.href && el.href.includes("mspaintadventures.com")) { + this.query = "//center[position() = 1]/table[position() = 1]/tbody[position() = 1]/tr[position() = 2]/td[position() = 1]/table[position() = 1]/tbody[position() = 1]/tr[position() = 1]/td[position() = 1]/table[position() = 1]/tbody[position() = 1]/tr[position() = 2]/td[position() = 1]/center[position() = 1]/table[position() = 1]/tbody[position() = 1]/tr[position() = 6]/td[position() = 1]/table[position() = 1]/tbody[position() = 1]/tr[position() = 1]/td[position() = 1]/font[position() = 1]/a[position() = 1]"; + this.numberToken = /(\[@href='.*)(\d+)(.*?'\])/; + return; + } + // See if the anchor has an ID // Note: cannot use the id() xpath function here, as there might // be duplicate ids @@ -177,7 +109,7 @@ Repaginator.prototype = { this.query = "/html/head//link[@rel='" + escapeXStr(rel) + "']"; // no point in checking for numbers this.attemptToIncrement = false; - log(LOG_DEBUG, "using link[@rel]"); + console.log("using link[@rel]"); return; } } @@ -191,55 +123,87 @@ Repaginator.prototype = { break; } if (pn.id) { - log(LOG_DEBUG, "got id: " + pn.id); + console.log("got id: " + pn.id); pieces.unshift("//" + pn.localName + "[@id='" + escapeXStr(pn.id) + "']"); break; // one id is enough } if (pn.className) { - log(LOG_DEBUG, "got class: " + pn.className); + console.log("got class: " + pn.className); pieces.unshift("//" + pn.localName + "[@class='" + escapeXStr(pn.className) + "']"); + break; } } this.query = pieces.join(""); - log(LOG_DEBUG, "findPathPrefix result: " + this.query); + console.log("findPathPrefix result: " + this.query); }).call(this); // find the anchor (function findAnchor() { let text = el.textContent; - // First: try the node text + // See if there is rel=next or rel=prev + let rel = (el.getAttribute("rel") || "").trim(); + if (rel && (rel.includes("next") || rel.includes("prev"))) { + this.query += "//a[@rel='" + escapeXStr(rel) + "']"; + // no point in checking for numbers + this.attemptToIncrement = false; + console.log("using a[@rel]"); + return; + } + + // Try the node text if (text.trim()) { this.query += "//a[.='" + escapeXStr(text) + "']"; this.numberToken = /(a\[.='.*?)(\d+)(.*?\])/; - log(LOG_DEBUG, "using text"); + console.log("using text"); return; } - // Second: see if it has a child with a @src we may use - let srcEl = getFirstSnapshot(el.ownerDocument, el, "child::*[@src]"); + // See if it has a descendant with a @src we may use + let srcEl = getFirstSnapshot(el.ownerDocument, el, "descendant::*[@src]"); if (srcEl) { let src = srcEl.getAttribute("src") || ""; if (src.trim()) { this.query += "//" + srcEl.localName + "[@src='" + escapeXStr(src) + "']/ancestor::a"; this.numberToken = /(\[@src='.*?)(\d+)(.*?'\])/; - log(LOG_DEBUG, "using @src"); + console.log("using @src"); return; } } - // Third: See if there is a child with a @value we may use - srcEl = getFirstSnapshot(el.ownerDocument, el, "child::*[@value]"); + // See if there is a descendant with a @value we may use + srcEl = getFirstSnapshot(el.ownerDocument, el, "descendant::*[@value]"); if (srcEl) { let val = srcEl.getAttribute("value") || ""; if (val.trim()) { this.query += "//" + srcEl.localName + "[@value='" + escapeXStr(val) + "']/ancestor::a"; this.numberToken = /(\[@value='.*?)(\d+)(.*?'\])/; - log(LOG_DEBUG, "using @value"); + console.log("using @value"); + return; + } + } + + // See if there is a class we may use + if (el.className) { + this.query += "//a[@class='" + escapeXStr(el.className) + "']"; + this.numberToken = /(\[@class='.*?)(\d+)(.*?'\])/; + console.log("using a[@class]"); + return; + } + + // See if there is a descendant with an id we may use + srcEl = getFirstSnapshot(el.ownerDocument, el, "descendant::*[@id]"); + if (srcEl) { + let val = srcEl.getAttribute("id") || ""; + if (val.trim()) { + this.query += "//" + srcEl.localName + "[@id='" + + escapeXStr(val) + "']/ancestor::a"; + this.numberToken = /(\[@value='.*?)(\d+)(.*?'\])/; + console.log("using descendant class"); return; } } @@ -251,73 +215,45 @@ Repaginator.prototype = { // We're after the last result this.query = "(" + this.query + ")[last()]"; - log(LOG_INFO, "query: " + this.query); + console.info("query: " + this.query); }, setTitle: function R_setTitle() { - let wnd = this._window; - if (!wnd) { - return; - } - if (!this._title) { - this._title = wnd.document.title; - } if (this.pageLimit) { - wnd.document.title = + document.title = _("repagination_limited", this.pageCount, this.pageLimit); } else if (this.pageCount > 0) { - wnd.document.title = _("repagination_unlimited", this.pageCount); + document.title = _("repagination_unlimited", this.pageCount); } else { - wnd.document.title = _("repagination_running"); + document.title = _("repagination_running"); } }, - restoreTitle: function R_restoreTitle() { - let wnd = this._window; - if (this._title && wnd) { - wnd.document.title = this._title; + finished: function R_finished() { + port.postMessage({msg: "finished"}); + stop(); + // restore title + if("_title" in this) { + document.title = this._title; delete this._title; } }, - unregister: function R_unregister() { - sendAsyncMessage("repagination:unregister", {id: this.frameId}); - if (this._window) { - this._window.document.body.removeAttribute("repagination"); - this._window.removeEventListener("beforeunload", this.unload, true); - } - }, repaginate: function R_repaginate() { + this._title = document.title; this.setTitle(); - let wnd = this._window; - if (!wnd) { - log(LOG_INFO, "window is gone!"); - return; - } - this.frameId = wnd.QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIDOMWindowUtils). - outerWindowID; - sendAsyncMessage("repagination:register", {id: this.frameId}); - this.unload = () => { - log(LOG_DEBUG, "unload"); - this.unregister(); - }; - wnd.addEventListener("beforeunload", this.unload, true); - try { - let node = wnd.document.evaluate( - this.query, wnd.document, null, 9, null).singleNodeValue; + let node = document.evaluate( + this.query, document, null, 9, null).singleNodeValue; if (!node) { throw new Error("no node"); } - wnd.document.body.setAttribute("repagination", "true"); - createFrame(wnd, node.href, this.allowScripts, frame => { - this.loadNext(frame, 0); + document.body.setAttribute("repagination", "true"); + createPageRequest(node.href, this.allowScripts, frame => { + this.loadNext(node.href, frame, 0); }); - } - catch (ex) { - this.unregister(); - this.restoreTitle(); - log(LOG_ERROR, "repaginate failed", ex); + } catch (ex) { + this.finished(); + console.error("repaginate failed", ex); } }, incrementQuery: function R_incrementQuery() { @@ -325,283 +261,176 @@ Repaginator.prototype = { return pre + (parseInt(num, 10) + 1) + post; }); }, - loadNext: function R_loadNext(element, delay) { - if (!this.yielding) { - if (delay > 0) { - log(LOG_DEBUG, "delaying (sto) " + delay); - content.setTimeout(() => this.loadNext(element, 0), delay); - return; - } - - try { - for (let f in this._loadNext_gen.bind(this, element)()) { - if (!f) { - break; - } - } - } - catch (ex) { - log(LOG_ERROR, "failed to process loadNext (non-yielding)", ex); - } + loadNext: function R_loadNext(src, element, delay) { + if (delay > 0) { + console.log("delaying (sto) " + delay); + setTimeout(() => this.loadNext(src, element, 0), delay); return; } - Task.spawn(function*() { - try { - if (delay > 0) { - yield new Promise(r => { - log(LOG_DEBUG, "delaying " + delay); - content.setTimeout(() => { - log(LOG_DEBUG, "invoke " + delay); - r(); - }, delay); - }); - } - let gen = this._loadNext_gen(element); - let deadline = +(new Date()) + 60; - for (let r in gen) { - if (!r) { - break; - } - if (deadline < +(new Date())) { - yield new Promise(r => { - content.setTimeout(() => r(), 0); - }); - deadline = +(new Date()) + 60; - } - } - } - catch (ex) { - log(LOG_ERROR, "failed to iterate loadNext", ex); - } - }.bind(this)); + try { + this._loadNext_gen.bind(this, src, element)(); + } catch (ex) { + console.error("failed to process loadNext (non-yielding)", ex); + this.finished(); + } + return; }, - _loadNext_gen: function R__loadNext_gen(element) { + _loadNext_gen: function R__loadNext_gen(src, element) { + let ownerDoc = document; + try { - let ownerDoc = element.ownerDocument; - if (!ownerDoc || !this._window) { - yield true; - this.unregister(); - this.restoreTitle(); - log(LOG_INFO, "gone, giving up!"); - return; + if (!ownerDoc.body.hasAttribute("repagination")) { + throw new Error("not running"); } - try { - if (!ownerDoc.body.hasAttribute("repagination")) { - throw new Error("not running"); - } - - var doc = element.contentDocument; - if (!checkSameOrigin(ownerDoc, doc.defaultView.location)) { - throw new Error("not in the same origin anymore"); - } - this.pageCount++; - - // Remove scripts from frame - // The scripts should already be present in the parent (first page) - // Duplicate scripts would cause more havoc (performance-wise) than - // behaviour failures due to missing scripts - // Note: This is NOT a security mechanism, but a performance thing. - Array.forEach(doc.querySelectorAll("script"), - s => s.parentNode.removeChild(s)); - Array.forEach(doc.querySelectorAll("noscript"), - s => s.parentNode.removeChild(s)); - - yield true; - - // Do the dirty deed - // Note: same-origin checked; see above - if (this.slideshow) { - log(LOG_INFO, "replacing content (slideshow)"); - ownerDoc.body.innerHTML = doc.body.innerHTML; - ownerDoc.body.setAttribute("repagination", "true"); - } - else { - log(LOG_INFO, "inserting content (normal)"); - // Remove non-same-origin iframes, such as ad iframes - // Otherwise we might create a shitload of (nearly) identical frames - // which might even kill the browser - if (!this.pageLimit || this.pageLimit > 10) { - log(LOG_INFO, "removing non-same-origin iframes to avoid dupes"); - let host = ownerDoc.defaultView.location.hostName; - Array.forEach(doc.querySelectorAll("iframe"), function(f) { - if (f.contentWindow.location.hostname != host) { - f.parentNode.removeChild(f); - } - }); - yield true; - } - for (let c = doc.body.firstChild; c; c = c.nextSibling) { - ownerDoc.body.appendChild(ownerDoc.importNode(c, true)); - yield true; - } + var doc = element; + this.pageCount++; + + // Remove scripts from frame + // The scripts should already be present in the parent (first page) + // Duplicate scripts would cause more havoc (performance-wise) than + // behaviour failures due to missing scripts + // Note: This is NOT a security mechanism, but a performance thing. + doc.querySelectorAll("script").forEach(s => s.parentNode.removeChild(s)); + doc.querySelectorAll("style").forEach(s => s.parentNode.removeChild(s)); + + // Do the dirty deed + if (this.slideshow) { + console.info("replacing content (slideshow)"); + // this should be safe since innerHTML returns serialized HTML + ownerDoc.body.innerHTML = doc.body.innerHTML; + ownerDoc.body.setAttribute("repagination", "true"); + } + else { + console.info("inserting content (normal)"); + // Remove non-same-origin iframes, such as ad iframes + // Otherwise we might create a shitload of (nearly) identical frames + // which might even kill the browser + if (!this.pageLimit || this.pageLimit > 10) { + console.info("removing non-same-origin iframes to avoid dupes"); + let host = ownerDoc.defaultView.location.hostName; + doc.querySelectorAll("iframe").forEach(function(f) { + var url = new URL(f.src, ownerDoc.defaultView.location.href); + if (url.hostname != host) { + f.parentNode.removeChild(f); + } + }); } - - // Synthesize load events to trigger other add-ons and - // page scripts. - if (ownerDoc.defaultView) { - log(LOG_DEBUG, "about to fire load events"); - - let levt = ownerDoc.createEvent("Events"); - levt.initEvent("DOMContentLoaded", true, true); - ownerDoc.defaultView.dispatchEvent(levt); - log(LOG_DEBUG, "fired DOMContentLoaded"); - - levt = ownerDoc.createEvent("Events"); - levt.initEvent("load", true, true); - ownerDoc.defaultView.dispatchEvent(levt); - log(LOG_DEBUG, "fired load"); + for (let c = doc.body.firstChild; c; c = c.nextSibling) { + ownerDoc.body.appendChild(ownerDoc.importNode(c, true)); } - yield true; + } - if (this.pageLimit && this.pageCount >= this.pageLimit) { - throw new Error("done"); - } + if (this.pageLimit && this.pageCount >= this.pageLimit) { + throw new Error("done"); + } - let savedQuery; - if (this.attemptToIncrement) { - log(LOG_DEBUG, "attempting to increment query"); - let nq = this.incrementQuery(); - if (nq == this.query) { - log(LOG_DEBUG, "query did not increment"); - this.attemptToIncrement = false; - } - else { - log(LOG_DEBUG, "query did increment"); - savedQuery = this.query; - this.query = nq; - } - } - let node = doc.evaluate(this.query, doc, null, 9, null).singleNodeValue; - let loc = (doc.location || {}).href || null; - if (this.attemptToIncrement && (!node || node.href == loc)) { - log(LOG_DEBUG, "no result after incrementing; restoring"); - log(LOG_DEBUG, "inc:" + this.query + " orig:" + savedQuery); - this.query = savedQuery; - node = doc.evaluate(this.query, doc, null, 9, null).singleNodeValue; + let savedQuery; + if (this.attemptToIncrement) { + console.log("attempting to increment query"); + let nq = this.incrementQuery(); + if (nq == this.query) { + console.log("query did not increment"); this.attemptToIncrement = false; } - if (!node) { - throw new Error("no next node found for query: " + this.query); - } - if (loc && loc == node.href) { - throw new Error("location did not change for query" + this.query); + else { + console.log("query did increment"); + savedQuery = this.query; + this.query = nq; } - - this.setTitle(); - log(LOG_INFO, "next please"); - createFrame(ownerDoc.defaultView, node.href, this.allowScripts, - frame => { - if (!this._window || this._window.closed) { - log(LOG_DEBUG, "self is gone by now"); - this.unregister(); - this.restoreTitle(); - return; - } - if (this.slideshow && this.seconds) { - log(LOG_INFO, "slideshow; delay: " + this.seconds * 1000); - this.loadNext(frame, this.seconds * 1000); - } - else { - log(LOG_INFO, "regular; no-delay"); - this.loadNext(frame, 0); - } - }); } - catch (ex) { - log(LOG_INFO, "loadNext complete", ex); - this.unregister(); - this.restoreTitle(); + let node = doc.evaluate(this.query, doc, null, 9, null).singleNodeValue; + let loc = src || null; + if (this.attemptToIncrement && (!node || node.href == loc)) { + console.log("no result after incrementing; restoring"); + console.log("inc:" + this.query + " orig:" + savedQuery); + this.query = savedQuery; + node = doc.evaluate(this.query, doc, null, 9, null).singleNodeValue; + this.attemptToIncrement = false; + } + if (!node) { + throw new Error("no next node found for query: " + this.query); + } + let nexturl = node.href.toString(); + if (loc && loc == nexturl) { + throw new Error("location did not change for query" + this.query); } + if (equalLinks(node,window.location)) { + throw new Error("loop back to first item"); + } + + this.setTitle(); + console.info("next please: " + nexturl); + createPageRequest(nexturl, this.allowScripts, frame => { + if (this.slideshow && this.seconds) { + console.info("slideshow; delay: " + this.seconds * 1000); + this.loadNext(nexturl, frame, this.seconds * 1000); + } + else { + console.info("regular; no-delay"); + this.loadNext(nexturl, frame, 0); + } + }); } - finally { - element.parentElement.removeChild(element); + catch (ex) { + console.log(ex); + console.info("loadNext complete"); + this.finished(); } } }; Object.seal(Repaginator.prototype); -const Slideshow = function Slideshow(focusElement, seconds, allowScripts, yielding) { +let Slideshow = function Slideshow(seconds, allowScripts, yielding) { this.seconds = seconds || 0; this.slideshow = true; this.allowScripts = allowScripts; this.yielding = yielding; - this.init(focusElement); - this.buildQuery(focusElement); + this.init(); }; Slideshow.prototype = Repaginator.prototype; -const fe = () => { - let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); - let focusedWindow = {}; - let elt = fm.getFocusedElementForWindow(content, true, focusedWindow); - return elt; -}; - -const repaginate = m => { - let {num, slideshow, allowScripts, yielding} = m.data; +let repaginate = (target, num, slideshow, allowScripts, yielding) => { try { let Ctor = slideshow ? Slideshow : Repaginator; let rep; - if ("query" in m.data) { - let el = getFirstSnapshot(content.document, content.document, - m.data.query); - if (!el) { - log(LOG_DEBUG, "did not find explicit query element"); - return; - } - rep = new Ctor(el, num, allowScripts, yielding); - rep.query = m.data.query; - } - else { - let el = fe(); - rep = new Ctor(fe(), num, allowScripts, yielding); - rep.buildQuery(el); - } + focusElement = browser.menus.getTargetElement(target); + rep = new Ctor(num, allowScripts, yielding); + rep.buildQuery(focusElement); rep.repaginate(); + } catch (ex) { + console.error("Failed to run repaginate", ex); } - catch (ex) { - log(LOG_ERROR, "Failed to run repaginate", ex); - } -}; - -const query = () => { - let el = fe(); - let rv = new Repaginator(el); - rv.buildQuery(el); - sendAsyncMessage("repagination:query", rv.query); }; -const stop = () => { +let stop = () => { try { - if (!content) { - return; - } - let body = content.document.getElementsByTagName("body")[0]; + let body = document.body; if (body) { body.removeAttribute("repagination"); } + } catch (ex) { + console.error("failed to stop repagination", ex); } - catch (ex) { - log(LOG_ERROR, "failed to stop repagination", ex); - } -}; - -const shutdown = () => { - removeMessageListener("repagination:normal", repaginate); - removeMessageListener("repagination:query", query); - removeMessageListener("repagination:stop", stop); - removeMessageListener("repagination:shutdown", shutdown); }; -addMessageListener("repagination:normal", repaginate); -addMessageListener("repagination:query", query); -addMessageListener("repagination:stop", stop); -addMessageListener("repagination:shutdown", shutdown); - -log(LOG_DEBUG, "Framescript loaded!"); +port.onMessage.addListener(msg => { + switch (msg.msg) { + case "normal": repaginate(msg.target, msg.num, msg.slideshow, msg.allowScripts, msg.yielding); break; + case "stop" : stop(); break; + } +}); -})(); // "module" +// https://bugzilla.mozilla.org/show_bug.cgi?id=1370368 +window.addEventListener('pagehide', function(event) { + stop(); + try { + port.disconnect(); + } catch (ex) { + console.log(ex) + } +}); +} /* vim: set et ts=2 sw=2 : */ diff --git a/defaults/preferences/prefs.js b/defaults/preferences/prefs.js deleted file mode 100644 index 68093cc..0000000 --- a/defaults/preferences/prefs.js +++ /dev/null @@ -1,6 +0,0 @@ -pref("extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.submenu", true); -pref("extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.loglevel", 0x7fffffff); -pref("extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.showalldomain", false); -pref("extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.showslideshow", true); -pref("extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.yielding", false); -pref("extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.allowscripts", true); diff --git a/install.rdf b/install.rdf deleted file mode 100644 index 770e4af..0000000 --- a/install.rdf +++ /dev/null @@ -1,40 +0,0 @@ - - - - - {6072cb90-a0bd-11da-a746-0800200c9a66} - Re-Pagination - Load all consecutive pages in a single tab at once. - 2016.08.03 - - true - true - 2 - - chrome://repagination/content/options.xul - 2 - - Nils Maier - Nils Maier - Lev, Omar - - - - - {ec8030f7-c20a-464f-9b0e-13a3a9e97384} - 40.0 - 50.* - - - - - - {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} - 2.35 - 2.47.* - - - - diff --git a/loader.jsm b/loader.jsm deleted file mode 100644 index 6a7da1a..0000000 --- a/loader.jsm +++ /dev/null @@ -1,242 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -var EXPORTED_SYMBOLS = ["BASE_PATH", "require", "unload", "_setupLoader"]; - -var { - classes: Cc, - interfaces: Ci, - utils: Cu, - results: Cr, - Constructor: ctor, - manager: Cm -} = Components; - -var weak = Cu.getWeakReference.bind(Cu); -var reportError = Cu.reportError.bind(Cu); - -Cm.QueryInterface(Ci.nsIComponentRegistrar); - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -var lazy = XPCOMUtils.defineLazyGetter; - -// hide our internals -// Since require() uses .scriptloader, the loaded require scopes will have -// access to the named stuff within this module scope, but we actually want -// them to have access to certain stuff. -(function setup_scope(exports) { - Services = exports.Services = Object.create(Services); - let dlsg = XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils, Services); - dlsg("catman", "@mozilla.org/categorymanager;1", "nsICategoryManager"); - dlsg("clipbrd", "@mozilla.org/widget/clipboard;1", "nsIClipboard"); - dlsg("drags", "@mozilla.org/widget/dragservice;1", "nsIDragService"); - dlsg("eps", "@mozilla.org/uriloader/external-protocol-service;1", - "nsIExternalProtocolService"); - dlsg("fixups", "@mozilla.org/docshell/urifixup;1", "nsIURIFixup"); - dlsg("memrm", "@mozilla.org/memory-reporter-manager;1", - "nsIMemoryReporterManager"); - dlsg("mime", "@mozilla.org/uriloader/external-helper-app-service;1", - "nsIMIMEService"); - dlsg("mimeheader", "@mozilla.org/network/mime-hdrparam;1", - "nsIMIMEHeaderParam"); - dlsg("sysprincipal", "@mozilla.org/systemprincipal;1", "nsIPrincipal"); - dlsg("uuid", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); - - const Instances = exports.Instances = { - get: function I_get(symbol, contract, iface, initializer) { - if (!(symbol in this)) { - this.register(symbol, contract, iface, initializer); - } - return this[symbol]; - }, - register: function I_register(symbol, contract, iface, initializer) { - if (symbol in this) { - let msg = "Symbol " + symbol + " already in Instances"; - log(LOG_ERROR, msg); - throw new Error(msg); - } - if (initializer) { - lazy(this, symbol, () => ctor(contract, iface, initializer)); - lazy(this, symbol + "_p", () => ctor(contract, iface)); - } - else { - lazy(this, symbol, () => ctor(contract, iface)); - lazy(this, symbol.toLowerCase(), () => new (ctor(contract, iface))()); - } - } - }; - - const {SELF_PATH, BASE_PATH} = (function() { - let rv; - try { throw new Error("narf"); } - catch (ex) { - rv = { - SELF_PATH: ex.fileName, - BASE_PATH: /^(.+\/).*?$/.exec(ex.fileName)[1] - }; - } - return rv; - })(); - exports.BASE_PATH = BASE_PATH; - - // logging stubs - var log = function() {}; // stub - var LOG_DEBUG = 0, LOG_INFO = 0, LOG_ERROR = 0; - - var _unloaders = []; - let _runUnloader = function _runUnloader(fn) { - try { - fn(); - } - catch (ex) { - log(LOG_ERROR, "unloader failed", ex); - } - }; - exports.unload = function unload(fn) { - if (fn == "shutdown") { - log(LOG_INFO, "shutdown"); - for (let i = _unloaders.length; ~(--i);) { - _runUnloader(_unloaders[i]); - } - _unloaders.splice(0); - return; - } - - // add an unloader - if (typeof(fn) != "function") { - throw new Error("unloader is not a function"); - } - _unloaders.push(fn); - return function() { - _runUnloader(fn); - _unloaders = _unloaders.filter(c => c != fn); - }; - }; - - const _registry = new Map(); - exports.require = function require(mod) { - mod = BASE_PATH + mod + ".js"; - - // already loaded? - let scope = _registry.get(mod); - if (scope) { - return scope.exports; - } - - // try to load the mod - log(LOG_DEBUG, "going to load: " + mod); - scope = Object.create(exports); - scope.exports = Object.create(null); - try { - scope = Cu.Sandbox(Services.sysprincipal, { - sandboxName: mod, - sandboxPrototype: scope, - wantXRays: false - }); - Services.scriptloader.loadSubScript(mod, scope, "utf-8"); - } - catch (ex) { - log(LOG_ERROR, "failed to load " + mod, ex); - throw ex; - } - - _registry.set(mod, scope); - log(LOG_DEBUG, "loaded module: " + mod); - - return scope.exports; - }; - - exports.lazyRequire = function lazyRequire(mod) { - function lazyBind(props, prop) { - log(LOG_DEBUG, "lazily binding " + props + " for module " + mod); - let m = require(mod); - for (let [,p] in new Iterator(props)) { - delete this[p]; - this[p] = m[p]; - } - return this[prop]; - } - - // Already loaded? - let scope = _registry.get(mod); - if (scope) { - return scope.exports; - } - - let props = Array.slice(arguments, 1); - let rv = {}; - let binder = lazyBind.bind(rv, props); - for (let [,p] in new Iterator(props)) { - let _p = p; - lazy(rv, _p, () => binder(_p)); - } - return rv; - }; - - unload(function() { - for (let [mod, scope] of _registry) { - _registry.delete(mod); - Cu.nukeSandbox(scope); - } - if (_registry.clear) { - _registry.clear(); - } - // unload ourselves - Cu.unload(SELF_PATH); - }); - - exports._setupLoader = function _setupLoader(data, callback) { - delete exports._setupLoader; - - let _am = {}; - Cu.import("resource://gre/modules/AddonManager.jsm", _am); - _am.AddonManager.getAddonByID(data.id, function loader_startup(addon) { - exports.ADDON = addon; - unload(() => delete exports.ADDON); - - let logging; - try { - logging = require("sdk/logging"); - for (let [k,v] in new Iterator(logging)) { - exports[k] = v; - } - - let prefs = require("sdk/preferences"); - exports.prefs = prefs.prefs; - exports.globalPrefs = prefs.globalPrefs; - - try { - prefs.prefs.observe("loglevel", - (p, v) => logging.setLogLevel(v), - logging.LOG_NONE); - } - catch (ex) { - logging.log(logging.LOG_ERROR, "failed to set log level", ex); - } - } - catch (ex) { - // probably do not have a working log() yet - reportError(ex); - return; - } - - try { - if (callback) { - logging.log(logging.LOG_DEBUG, "loader: running callback"); - callback(); - } - } - catch (ex) { - logging.log(logging.LOG_ERROR, "callback failed!", ex); - } - logging.log(logging.LOG_DEBUG, "loader: done"); - }); - }; - -})(this); - -/* vim: set et ts=2 sw=2 : */ diff --git a/locale/de/options.dtd b/locale/de/options.dtd deleted file mode 100644 index 932c656..0000000 --- a/locale/de/options.dtd +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/locale/de/repagination.dtd b/locale/de/repagination.dtd deleted file mode 100644 index e6c5a21..0000000 --- a/locale/de/repagination.dtd +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/locale/de/repagination.properties b/locale/de/repagination.properties deleted file mode 100644 index cd3e05c..0000000 --- a/locale/de/repagination.properties +++ /dev/null @@ -1,11 +0,0 @@ -pages.label=%S Seiten -minutes.label=%S Minuten -seconds.label=%S Sekunden - -repagination_limited=(%S of %S) Re-Pagination arbeitet... -repagination_unlimited=(%S) Re-Pagination arbeitet... -repagination_running=Re-Pagination arbeitet... - -extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.name=Re-Pagination -extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.description=Alle fortlaufenden Seiten in einem einzigem Tab auf einmal laden -extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.translator.1=Nils Maier diff --git a/locale/en-US/options.dtd b/locale/en-US/options.dtd deleted file mode 100644 index 6100f15..0000000 --- a/locale/en-US/options.dtd +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/locale/en-US/repagination.dtd b/locale/en-US/repagination.dtd deleted file mode 100644 index 61a26ec..0000000 --- a/locale/en-US/repagination.dtd +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/locale/en-US/repagination.properties b/locale/en-US/repagination.properties deleted file mode 100644 index cdb1b4a..0000000 --- a/locale/en-US/repagination.properties +++ /dev/null @@ -1,11 +0,0 @@ -pages.label=%S pages -minutes.label=%S minutes -seconds.label=%S seconds - -repagination_limited=(%S of %S) Re-Pagination is running... -repagination_unlimited=(%S) Re-Pagination is running... -repagination_running=Re-Pagination is running... - -extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.name=Re-Pagination -extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.description=Load all consecutive pages in a single tab at once. -extensions.{6072cb90-a0bd-11da-a746-0800200c9a66}.translator.1=Nils Maier diff --git a/main.js b/main.js index 4ee8a04..6767e66 100644 --- a/main.js +++ b/main.js @@ -3,271 +3,205 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const {registerOverlay, unloadWindow} = require("sdk/windows"); - +const PORTS = {}; +const PENDING = {}; const RUNNING = new Set(); -const globalMM = Cc["@mozilla.org/globalmessagemanager;1"]. - getService(Ci.nsIMessageListenerManager); -function registerRunning(m) { - RUNNING.add(m.data.id); - log(LOG_DEBUG, `added ${m.data.id} to running`); +function onError(error) { + console.error(error); } -function unregisterRunning(m) { - RUNNING.delete(m.data.id); - log(LOG_DEBUG, `removed ${m.data.id} from running`); +const MENU = { + NOLIMIT: 'REPAGINATION_NOLIMIT', + LIMIT: 'REPAGINATION_LIMIT', + SLIDE: 'REPAGINATION_SLIDE', + STOP: 'REPAGINATION_STOP' } -globalMM.addMessageListener("repagination:register", registerRunning); -globalMM.addMessageListener("repagination:unregister", unregisterRunning); -unload(() => { - globalMM.removeMessageListener("repagination:register", registerRunning); - globalMM.removeMessageListener("repagination:unregister", unregisterRunning); -}); - -/* globals _ */ -lazy(this, "_", function() { - let bundle = require("sdk/strings"). - getBundle("chrome://repagination/locale/repagination.properties"); - return function() { - return bundle.getString.apply(bundle, arguments); - }; -}); - -function checkSameOrigin(principal, tryLoadUri) { - try { - if (!(tryLoadUri instanceof Ci.nsIURI)) { - tryLoadUri = Services.io.newURI(tryLoadUri, null, null); - } - if (tryLoadUri.schemeIs("data")) { - return true; - } - principal.checkMayLoad(tryLoadUri, false, true); - return true; - } - catch (ex) { - log(LOG_DEBUG, "denied load of " + (tryLoadUri.spec || tryLoadUri), ex); - return false; + +function onCreated(n) { + if (browser.runtime.lastError) { + console.error("Error creating menu item: %o", browser.runtime.lastError); } } -function main(window, document) { - const $ = id => document.getElementById(id); - const $$$ = q => document.querySelectorAll(q); +function createMenu(prefs) { + let c = ["link"]; // ContextTypes for most menu items + + browser.menus.create({ + id: MENU.NOLIMIT, + title: browser.i18n.getMessage("repagination_nolimit"), + contexts: c + }, onCreated); + + let limits = [5,10,25,50,100]; + + for(let j in limits) { + let i = limits[j]; + browser.menus.create({ + id: MENU.LIMIT + "_" + i, + title: browser.i18n.getMessage("repagination_limit_x",[i]), + contexts: c + }, onCreated); - function repaginate(num, slideshow) { - log(LOG_INFO, "repaginate: " + num + "/" + slideshow); - try { - let mm = window.gBrowser.selectedBrowser.messageManager; - mm.sendAsyncMessage("repagination:normal", { - num: num, - slideshow: slideshow, - allowScripts: prefs.get("allowscripts", true), - yielding: prefs.yielding - }); - } - catch (ex) { - log(LOG_ERROR, "failed to run repaginate", ex); - } } - function repaginate_domain() { - log(LOG_INFO, "repaginate_domain"); - try { - let mm = window.gBrowser.selectedBrowser.messageManager; - let queried = m => { - mm.removeMessageListener("repagination:query", queried); - log(LOG_DEBUG, "recv query " + m.data); - if (!m.data) { - return; - } - const host = window.gBrowser.selectedBrowser.currentURI.host; - for (let i = 0; i < window.gBrowser.browsers.length; ++i) { - let browser = window.gBrowser.getBrowserAtIndex(i); - if (!browser) { - continue; - } - if (browser.currentURI.host != host) { - continue; - } - browser.messageManager.sendAsyncMessage("repagination:normal", { - num: 0, - slideshow: false, - allowScripts: prefs.get("allowscripts", true), - yielding: prefs.yielding, - query: m.data - }); - } - }; - mm.addMessageListener("repagination:query", queried); - mm.sendAsyncMessage("repagination:query"); - } - catch (ex) { - log(LOG_ERROR, "failed to run repaginate_domain", ex); + if(prefs.slideshows) { + browser.menus.create({ + id: MENU.SLIDE, + title: browser.i18n.getMessage("repagination_slide"), + contexts: c + }, onCreated); + + let slides = [0,1,2,4,5,10,15,30,60,120]; + + for(let j in slides) { + let i = slides[j]; + browser.menus.create({ + id: MENU.SLIDE + "_" + i, + parentId: MENU.SLIDE, + title: ([0,1,60,120].indexOf(i) != -1) ? browser.i18n.getMessage("repagination_slide_" + i) : browser.i18n.getMessage("repagination_slide_x",[i]), + contexts: c + }, onCreated); } } - function stop() { - log(LOG_INFO, "stop"); - let mm = window.gBrowser.selectedBrowser.messageManager; - mm.sendAsyncMessage("repagination:stop"); + updStop(); +} + +var added = false; + +function updStop() { + if(RUNNING.size > 0 && !added) { + browser.menus.create({ + id: MENU.STOP, + title: browser.i18n.getMessage("repagination_stop"), + contexts: ["all"] + }, onCreated); + added = true; + } else if(added) { + browser.menus.remove(MENU.STOP); + added = false; } +} - function onAll() { repaginate(); } - function onAllDomain() { repaginate_domain(); } - function onStop() { stop(); } - function onLimitCommand(evt) { - let t = evt.target; - if (t.localName != "menuitem") { - return; - } - repaginate(parseInt(t.getAttribute("value"), 10)); +var defaultSettings = { + slideshows: false, + exists: true // special pref to restore defaults +}; + +function prefReset(newSettings, areaName) { + console.log("prefs changed") + if (areaName == "local" && ("exists" in newSettings)) { + browser.menus.removeAll(); + console.log("recreating menu") + browser.storage.local.get().then(initSettings, onError); } - function onSlideCommand(evt) { - let t = evt.target; - if (t.localName != "menuitem") { - return; - } - repaginate(parseInt(t.getAttribute("value"), 10), true); +} + +function initSettings(prefs) { + if (!("exists" in prefs) || !prefs.exists) { + browser.storage.onChanged.removeListener(prefReset); + browser.storage.local.set(defaultSettings); + browser.storage.onChanged.addListener(prefReset); + prefs = defaultSettings; } - log(LOG_INFO, "main called!"); - - let frameToLog = m => log(m.data.level, m.data.message, m.data.exception); - let frameL10N = m => { - return _.apply(null, m.data.arguments); - }; - window.messageManager.addMessageListener("repagination:log", frameToLog); - window.messageManager.addMessageListener("repagination:_", frameL10N); - let fs = "chrome://repagination/content/content-script.js?" + (+new Date()); - window.messageManager.loadFrameScript(fs, true); - unloadWindow(window, () => { - window.messageManager.broadcastAsyncMessage("repagination:shutdown"); - window.messageManager.removeMessageListener("repagination:log", frameToLog); - window.messageManager.removeMessageListener("repagination:_", frameL10N); - window.messageManager.removeDelayedFrameScript(fs); - }); + createMenu(prefs); +} - // finish the localization - let nodes = $$$(":-moz-any(#repagination_limit, " + - "#repagination_menu_limit) menuitem"); - for (let n of nodes) { - n.setAttribute("label", _("pages.label", n.getAttribute("value"))); - } - nodes = $$$(":-moz-any(#repagination_slide, #repagination_menu_slide) " + - "menuitem:not([label])"); - for (let n of nodes) { - let s = parseInt(n.getAttribute("value"), 10); - if (s < 60) { - n.setAttribute("label", _("seconds.label", s)); - } - else { - n.setAttribute("label", _("minutes.label", parseInt(s / 60, 10))); - } - } - let contextMenu = $("contentAreaContextMenu"); - - let menuCascaded = { - menu: $("repagination_menu"), - allMenu: $("repagination_menu_nolimit"), - allDomainMenu: $("repagination_menu_nolimit_domain"), - stopMenu: $("repagination_menu_stop"), - limitMenu: $("repagination_menu_limit"), - slideMenu: $("repagination_menu_slide") - }; - let menuPlain = { - menu: {}, - allMenu: $("repagination_nolimit"), - allDomainMenu: $("repagination_nolimit_domain"), - stopMenu: $("repagination_stop"), - limitMenu: $("repagination_limit"), - slideMenu: $("repagination_slide") - }; - let menuCurrent; - - prefs.observe("submenu", function(pref, value) { - menuCurrent = value ? menuCascaded : menuPlain; - let menuDisabled = value ? menuPlain : menuCascaded; - for (let [,mi] in new Iterator(menuDisabled)) { - mi.hidden = true; +function myinit(prefs) { + initSettings(prefs); + + function process(info, tab) { + let str = info.menuItemId; + + if(str == MENU.STOP) { + console.info("stop"); + PORTS[tab].postMessage({ + msg: "stop" + }); + return; } - }, true); - let onContextMenu = function onContextMenu() { - function setMenuHidden(hidden) { - log(LOG_DEBUG, "set menu hidden = " + hidden); - for (let [,mi] in new Iterator(menuCurrent)) { - mi.hidden = hidden; - } - menuCurrent.slideMenu.hidden = - menuCurrent.slideMenu.hidden || !prefs.showslideshow; - menuCurrent.allDomainMenu.hidden = - menuCurrent.allDomainMenu.hidden || !prefs.showalldomain; + let num, slideshow; + if(str == MENU.NOLIMIT) { + num = 0; + slideshow = false; + } else if(str.startsWith(MENU.LIMIT)) { + // https://stackoverflow.com/questions/5555518/split-variable-from-last-slash-jquery + num = parseInt(str.substring(str.lastIndexOf("_") + 1, str.length), 10); + slideshow = false; + } else if(str.startsWith(MENU.SLIDE)) { + num = parseInt(str.substring(str.lastIndexOf("_") + 1, str.length), 10); + slideshow = true; } - let {gContextMenu} = window; - log(LOG_DEBUG, `context menu showing for ${gContextMenu.frameOuterWindowID}!`); + console.info("repaginate: " + num + "/" + slideshow); try { - if (gContextMenu.onLink && /^https?$/.test(gContextMenu.linkURI.scheme)) { - setMenuHidden(!checkSameOrigin(gContextMenu.principal, - gContextMenu.linkURL)); - if (!RUNNING.has(gContextMenu.frameOuterWindowID)) { - menuCurrent.stopMenu.hidden = true; - } - return; - } + PORTS[tab].postMessage({ + msg: "normal", + target: info.targetElementId, + num: num, + slideshow: slideshow, + allowScripts: prefs.allowScripts, + yielding: prefs.yielding + }); + RUNNING.add(tab); + updStop(); + } catch (ex) { + console.log(ex); + console.error("failed to run repaginate"); + delete PORTS[tabid]; + delete PENDING[tabid]; + RUNNING.delete(tabid); } - catch (ex) { - log(LOG_ERROR, "failed to setup menu (onLink)", ex); + } + + // We lazily inject the main content script in a vague hope for efficiency + // We use ports for messaging but have to store the messages until the port is opened. + + browser.menus.onClicked.addListener((info, tab) => { + console.log(info, tab); + if(tab.id in PORTS) { + process(info, tab.id); + } else { + console.log("injecting " + tab.id); + browser.tabs.executeScript(tab.id, { file: "content-script.js" } ); + PENDING[tab.id] = info; } - try { - setMenuHidden(true); - if (RUNNING.has(gContextMenu.frameOuterWindowID)) { - menuCurrent.menu.hidden = menuCurrent.stopMenu.hidden = false; + }); + + + browser.runtime.onConnect.addListener(function(port) { + let tabid = port.sender.tab.id; + PORTS[tabid] = port; + port.onDisconnect.addListener((p) => { + delete PORTS[tabid]; + delete PENDING[tabid]; + RUNNING.delete(tabid); + updStop(); + }); + port.onMessage.addListener(msg => { + switch (msg.msg) { + case "finished": + RUNNING.delete(tabid); + updStop(); + break; } + }); + + if (port.sender.tab.id in PENDING) { + var info = PENDING[port.sender.tab.id]; + delete PENDING[port.sender.tab.id]; + process(info, port.sender.tab.id); } - catch (ex) { - log(LOG_ERROR, "failed to setup menu (plain)", ex); - } - }; - - contextMenu.addEventListener("popupshowing", onContextMenu, true); - menuCascaded.allMenu.addEventListener("command", onAll, true); - menuPlain.allMenu.addEventListener("command", onAll, true); - menuCascaded.allDomainMenu.addEventListener("command", onAllDomain, true); - menuPlain.allDomainMenu.addEventListener("command", onAllDomain, true); - menuCascaded.stopMenu.addEventListener("command", onStop, true); - menuPlain.stopMenu.addEventListener("command", onStop, true); - menuCascaded.limitMenu.addEventListener("command", onLimitCommand, true); - menuPlain.limitMenu.addEventListener("command", onLimitCommand, true); - menuCascaded.slideMenu.addEventListener("command", onSlideCommand, true); - menuPlain.slideMenu.addEventListener("command", onSlideCommand, true); - unloadWindow(window, function() { - contextMenu.removeEventListener("popuphowing", onContextMenu, true); - menuCascaded.allMenu.removeEventListener("command", onAll, true); - menuPlain.allMenu.removeEventListener("command", onAll, true); - menuCascaded.allDomainMenu.removeEventListener("command", onAllDomain, - true); - menuPlain.allDomainMenu.removeEventListener("command", onAllDomain, true); - menuCascaded.stopMenu.removeEventListener("command", onStop, true); - menuPlain.stopMenu.removeEventListener("command", onStop, true); - menuCascaded.limitMenu.removeEventListener("command", onLimitCommand, true); - menuPlain.limitMenu.removeEventListener("command", onLimitCommand, true); - menuCascaded.slideMenu.removeEventListener("command", onSlideCommand, true); - menuPlain.slideMenu.removeEventListener("command", onSlideCommand, true); - contextMenu = menuPlain = menuCascaded = null; }); - log(LOG_INFO, "all good!"); } -registerOverlay( - "repagination.xul", - "chrome://browser/content/browser.xul", - main -); -registerOverlay( - "repagination.xul", - "chrome://navigator/content/navigator.xul", - main -); + +browser.storage.local.get().then(myinit, onError); + +browser.storage.onChanged.addListener(prefReset); + +console.info("all good!"); /* vim: set et ts=2 sw=2 : */ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..640435a --- /dev/null +++ b/manifest.json @@ -0,0 +1,34 @@ +{ + "manifest_version": 2, + "name": "__MSG_extensionName__", + "version": "2020.5.27", + "description": "__MSG_extensionDescription__", + "homepage_url": "https://github.com/Mathnerd314/repagination/", + + "default_locale": "en", + "options_ui": { + "page": "options.html", + "browser_style": true + }, + "icons": { "32": "icon32.png", + "48": "icon.png", + "64": "icon64.png"}, + + "applications": { + "gecko": { + "id": "repagination-fork-mathnerd314@github.com", + "strict_min_version": "63.0" + } + }, + + "background": { + "scripts": ["main.js"] + }, + + "permissions": [ + "activeTab", + "storage", + "menus", + "" + ] +} diff --git a/options.html b/options.html new file mode 100644 index 0000000..10acd23 --- /dev/null +++ b/options.html @@ -0,0 +1,53 @@ + + + + + + + + +
+ +
+ + +
+

Credits

+
+ \ No newline at end of file diff --git a/options.js b/options.js new file mode 100644 index 0000000..3af0b55 --- /dev/null +++ b/options.js @@ -0,0 +1,19 @@ +/* see also main.js */ + +/* +Update the options UI with the settings values retrieved from storage, +or the default settings if the stored settings are empty. +*/ +function updateUI(restoredSettings) { + document.getElementById("slideshows").checked = restoredSettings.slideshows || false; +} + +function onError(e) { + console.error(e); +} + +browser.storage.local.get().then(updateUI, onError); + +document.getElementById("slideshows").onchange = function setSlideshows() { + browser.storage.local.set({ exists: true, slideshows: document.getElementById("slideshows").checked}); +}; diff --git a/options.xul b/options.xul deleted file mode 100644 index 91391d6..0000000 --- a/options.xul +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/repagination.xul b/repagination.xul deleted file mode 100644 index 579d1d3..0000000 --- a/repagination.xul +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sdk/cothreads.js b/sdk/cothreads.js deleted file mode 100644 index 139701a..0000000 --- a/sdk/cothreads.js +++ /dev/null @@ -1,244 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -// Note: only used for dispatching CoThreads to the mainThread event loop -const ThreadManager = Cc["@mozilla.org/thread-manager;1"]. - getService(Ci.nsIThreadManager); -const MainThread = ThreadManager.mainThread; - -const CoThreadBase = { - _idx: 0, - _ran: false, - _finishFunc: null, - - init: function CoThreadBase_init(func, yieldEvery, thisCtx) { - this._thisCtx = thisCtx ? thisCtx : this; - - // default to 1 - this._yieldEvery = typeof yieldEvery == 'number' ? - Math.floor(yieldEvery) : - 1; - if (yieldEvery < 1) { - throw Cr.NS_ERROR_INVALID_ARG; - } - - if (typeof func != 'function' && !(func instanceof Function)) { - throw Cr.NS_ERROR_INVALID_ARG; - } - this._func = func; - this.init = function() {}; - }, - - start: function CoThreadBase_run(finishFunc) { - if (this._ran) { - throw new Error("You cannot run a CoThread/CoThreadListWalker instance " + - "more than once."); - } - this._finishFunc = finishFunc; - this._ran = true; - MainThread.dispatch(this, 0); - }, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsICancelable, - Ci.nsIRunnable]), - - _terminated: false, - - run: function CoThreadBase_run() { - if (this._terminated) { - return; - } - - let y = this._yieldEvery; - let g = this._generator; - let f = this._func; - let ctx = this._thisCtx; - let callf = this._callf; - const isStar = !('send' in g); - try { - for (let i = 0; i < y; ++i) { - let next = g.next(); - if (isStar) { - if (next.done) { - throw "complete"; - } - next = next.value; - } - if (!callf(ctx, next, this._idx++, f)) { - throw 'complete'; - } - } - if (!this._terminated) { - MainThread.dispatch(this, 0); - } - } - catch (ex) { - this.cancel(); - } - }, - - cancel: function CoThreadBase_cancel() { - if (this._terminated) { - return; - } - this._terminated = true; - if (this._finishFunc) { - this._finishFunc.call(this._thisCtx); - } - } -}; - -/** - * Constructs a new CoThread (aka. pseudo-thread). - * A CoThread will repeatedly call a specified function, but "breaking" - * the operation temporarily after a certain amount of calls, - * so that the main thread gets a chance to process any outstanding - * events. - * - * Example: - * new CoThread( - * // What to do with each item? - * // Print it! - * function(count) document.write(count + "
") || (count < 30000), - * // When to turn over Control? - * // Each 1000 items - * 1000 - * ).start(); - * - * @param {Function} func Function to be called. Is passed call count as - * argument. Returning false will cancel the operation. - * @param {Number} yieldEvery Optional. After how many items control should be - * turned over to the main thread - * @param {Object} thisCtx Optional. The function will be called in the scope of - * this object (or if omitted in the scope of the - * CoThread instance) - */ -exports.CoThread = function CoThread(func, yieldEvery, thisCtx) { - this.init(func, yieldEvery, thisCtx); - // fake generator so we may use a common implementation. ;) - this._generator = (function() { - for(;;) { - yield null; - } - })(); -}; - -exports.CoThread.prototype = Object.create(CoThreadBase, { - _callf: { - value: function CoThread__callf(ctx, i, idx, fn) { - return fn.call(ctx, idx); - }, - enumerable: true - } -}); - -/** - * Constructs a new CoThreadInterleaved (aka. pseudo-thread). - * The CoThread will process a interleaved function (generator) - * - * Example: - * new CoThread( - * function(count) { - * do_some(); - * yield true; - * do_more(); - * yield true; - * if (!do_even_more()) { - * return; - * } - * do_last(); - * }, - * // When to turn over Control? - * // Each 2 items - * 2 - * ).start(); - * - * @param {Function} func Function to be called. Is passed call count as - * argument. Returning false will cancel the operation. - * @param {Number} yieldEvery Optional. After how many items control should be - * turned over to the main thread - * @param {Object} thisCtx Optional. The function will be called in the scope of - * this object (or if omitted in the scope of the - * CoThread instance) - */ -exports.CoThreadInterleaved = function CoThreadInterleaved(generator, - yieldEvery, - thisCtx) { - this.init(() => true, yieldEvery, thisCtx); - this._generator = generator; -}; -exports.CoThreadInterleaved.prototype = Object.create(CoThreadBase, { - _callf: { - value: () => true, - enumerable: true - } -}); - -/** - * Constructs a new CoThreadListWalker (aka. pseudo-thread). - * A CoThreadListWalker will walk a specified list and call a specified function - * on each item, but "breaking" the operation temporarily after a - * certain amount of processed items, so that the main thread may - * process any outstanding events. - * - * Example: - * new CoThreadListWalker( - * // What to do with each item? - * // Print it! - * function(item, idx) { - * return document.write(item + "/" + idx + "
") || true; -* }, - * // What items? - * // 0 - 29999 - * (function() { for (let i = 0; i < 30000; ++i) yield i; })(), - * // When to turn over Control? - * // Each 1000 items - * 1000, - * null, - * ).start(function() alert('done')); - * - * @param {Function} func Function to be called on each item. Is passed item - * and index as arguments. Returning false will cancel - * the operation. - * @param {Array/Generator} arrayOrGenerator Array or Generator object to be - * used as the input list - * @param {Number} yieldEvery Optional. After how many items control should be - * turned over to the main thread - * @param {Object} thisCtx Optional. The function will be called in the scope of - * this object (or if omitted in the scope of the - * CoThread instance) - */ -exports.CoThreadListWalker = function CoThreadListWalker(func, arrayOrGenerator, - yieldEvery, thisCtx) { - this.init(func, yieldEvery, thisCtx); - - if (arrayOrGenerator instanceof Array || 'length' in arrayOrGenerator) { - // make a generator - this._generator = (function*() { - for (let i of arrayOrGenerator) { - yield i; - } - })(); - } - else { - this._generator = arrayOrGenerator; - } - - if (this._lastFunc && (typeof func != 'function' && - !(func instanceof Function))) { - throw Cr.NS_ERROR_INVALID_ARG; - } -}; - -exports.CoThreadListWalker.prototype = Object.create(CoThreadBase, { - _callf: { - value: function CoThreadListWalker__callf(ctx, item, idx, fn) { - return fn.call(ctx, item, idx); - }, - enumerable: true - } -}); - -/* vim: set et ts=2 sw=2 : */ diff --git a/sdk/logging.js b/sdk/logging.js deleted file mode 100644 index e17ce79..0000000 --- a/sdk/logging.js +++ /dev/null @@ -1,146 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -const global = this; - -Instances.register("ScriptError", - "@mozilla.org/scripterror;1", - "nsIScriptError", - "init"); - -var UNKNOWN_STACK = { - stackMsg: "", - sourceName: "unknown", - sourceLine: "", - lineNumber: 0, - columnNumber: 0 -}; -Object.freeze(UNKNOWN_STACK); - -function prepareStack(stack) { - if (!stack || !(stack instanceof Ci.nsIStackFrame)) { - stack = Components.stack; - for (let i = 0; stack && i < 2; ++i) { - stack = stack.caller; - } - if (!stack) { - return UNKNOWN_STACK; - } - } - let rv = {}; - rv.sourceName = stack.filename; - rv.sourceLine = stack.sourceLine; - rv.lineNumber = stack.lineNumber; - let message = []; - for (let i = 0; stack && i < 6; ++i, stack = stack.caller) { - if (stack.lineNumber) { - message.push("\t" + (stack.name || "[anonymous]") + "() @ " + - stack.filename + ":" + stack.lineNumber); - } - else { - message.push("\t[native @ " + (stack.languageName || "???" ) + "]"); - } - } - rv.stackMsg = message.join("\n"); - rv.prototype = UNKNOWN_STACK; - return rv; -} - -const { - errorFlag, - warningFlag -} = Ci.nsIScriptError; - -Object.defineProperties(exports, { - LOG_DEBUG: {value: 0, enumerable: true}, - LOG_INFO: {value: 1, enumerable: true}, - LOG_ERROR: {value: 2, enumerable: true}, - LOG_NONE: {value: 0x7FFFFFFF}, - PREFIX: {get: () => prefix}, - setLogLevel: {value: l => global.level = l} -}); - -var prefix = ADDON.name; -global.level = exports.LOG_NONE; - -exports.log = function(level, message, exception) { - try { - if (global.level > level) { - return; - } - - if (message instanceof Ci.nsIScriptError || - message instanceof Ci.nsIException || message.fileName) { - exception = message; - message = exception.message; - } - else if (exception) { - message = message + " [Exception: " + exception.message + "]"; - } - - let { - stackMsg, - sourceName, - sourceLine, - lineNumber, - columnNumber - } = prepareStack((exception && exception.location) || null); - - if (stackMsg) { - message += "\n" + stackMsg; - } - - let category = "component javascript"; - - if (exception) { - if (exception instanceof Ci.nsIScriptError) { - sourceName = exception.sourceName; - sourceLine = exception.sourceLine; - lineNumber = exception.lineNumber; - columnNumber = exception.columnNumber; - category = exception.category; - } - else if (exception instanceof Ci.nsIException) { - sourceName = exception.filename; - lineNumber = exception.lineNumber; - } - else { - sourceName = exception.fileName || sourceName; - lineNumber = exception.lineNumber || lineNumber; - } - } - - let levelMsg; - switch (level) { - case exports.LOG_ERROR: - levelMsg = "error"; - break; - case exports.LOG_INFO: - levelMsg = "info"; - break; - default: - levelMsg = "debug"; - } - - message = global.prefix + " (" + levelMsg + ") - " + message; - - const scriptError = new Instances.ScriptError( - message, - sourceName, - sourceLine, - lineNumber, - columnNumber, - level >= exports.LOG_ERROR ? errorFlag : warningFlag, - category); - Services.console.logMessage(scriptError); - } - catch (ex) { - reportError("failed to log"); - reportError(ex); - reportError(exception || message); - } -}; - -/* vim: set et ts=2 sw=2 : */ diff --git a/sdk/observers.js b/sdk/observers.js deleted file mode 100644 index c29e307..0000000 --- a/sdk/observers.js +++ /dev/null @@ -1,59 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -const manager = { - topics: Object.create(null), - observe: function observe(subject, topic, data) { - let observers = this.topics[topic]; - if (!observers) { - return; - } - for (let i = observers.length; ~(--i);) { - try { - observers[i].call(null, subject, topic, data); - } - catch (ex) { - log(LOG_ERROR, "observer threw", ex); - } - } - }, - add: function add(topic, cb) { - try { - this.topics[topic].push(cb); - } - catch (ex) { - this.topics[topic] = [cb]; - Services.obs.addObserver(this, topic, false); - } - }, - remove: function remove(topic, cb) { - let observers = this.topics[topic]; - if (!observers) { - log(LOG_ERROR, - "tried to remove a non-existant observer for topic: " + topic); - return; - } - observers = this.topics[topic] = observers.filter(fn => fn != cb); - if (!observers.length) { - delete this.topics[topic]; - Services.obs.removeObserver(this, topic, false); - } - }, - teardown: function teardown() { - for (let k in this.topics) { - this.topics[k].splice(0); - delete this.topics[k]; - Services.obs.removeObserver(this, k, false); - } - } -}; -unload(() => manager.teardown()); - -Object.defineProperties(exports, { - add: {value: (topic, cb) => manager.add(topic, cb)}, - remove: {value: (topic, cb) => manager.remove(topic, cb)} -}); - -/* vim: set et ts=2 sw=2 : */ diff --git a/sdk/preferences.js b/sdk/preferences.js deleted file mode 100644 index 9ae7523..0000000 --- a/sdk/preferences.js +++ /dev/null @@ -1,177 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -/* This implementation was heavily inspired by the add-on SDK stuff ;) - * - * TODO: - * - Implement enumerate/getChildren - */ - -try { - Instances.register("SupportsString", "@mozilla.org/supports-string;1", "nsISupportsString"); -} -catch (ex) { - // ignore -} - -const { - PREF_INVALID: INVALID, - PREF_STRING: STR, - PREF_INT: INT, - PREF_BOOL: BOOL -} = Ci.nsIPrefBranch; - -function createProxy(branch) { - return new Proxy(branch, { - get: function(receiver, name) { - if (name in branch) { - log(LOG_DEBUG, "prefproxy: returning plain " + name); - return branch[name]; - } - log(LOG_DEBUG, "prefproxy: returning pref " + branch.branch + name); - return branch.get(name); - }, - set: function(receiver, name, value) { - if (name in branch) { - throw new Error("Cannot use this name as a preference"); - } - log(LOG_DEBUG, "prefproxy: setting pref " + branch.branch + name); - branch.set(name, value); - }, - delete: function(name) { - if (name in branch) { - throw new Error("Cannot use this name as a preference"); - } - branch.delete(name); - }, - has: function(name) { - if (name in branch) { - throw new Error("Cannot use this name as a preference"); - } - log(LOG_DEBUG, "prefproxy: has pref " + branch.branch + name); - return branch.has(name); - } - }); -} - -function Branch(branch) { - if (typeof(branch) == "string") { - if (!/\.$|^$/.test(branch)) { - branch += "."; - } - branch = Services.prefs.getBranch(branch); - if ("nsIPrefBranch2" in Ci) { - branch.QueryInterface(Ci.nsIPrefBranch2); - } - } - this.branch = branch.root; - - let getType = this.getType = pref => branch.getPrefType(pref); - this.has = pref => getType(pref) != INVALID; - this.isChanged = pref => branch.prefHasUserValue(pref); - this.isDefault = pref => !this[pref].isChanged(); - let get = this.get = function(pref, defaultValue) { - switch (getType(pref)) { - case STR: - return branch.getComplexValue(pref, Ci.nsISupportsString).data; - case INT: - return branch.getIntPref(pref); - case BOOL: - return branch.getBoolPref(pref); - default: - if (typeof(defaultValue) != "undefined") { - return defaultValue; - } - log(LOG_DEBUG, "creating subbranch for " + branch.root + pref); - return createProxy(new Branch(branch.root + pref)); - } - }; - this.set = function(pref, value) { - if (value === null || value === undefined) { - log(LOG_DEBUG, "ignoring null value for pref " + pref); - return; - } - switch (value.constructor.name) { - case "Number": - if (!isFinite(value)) { - let msg = "attempt to set an invalid number to pref " + branch.root + - pref + ": " + value; - log(LOG_DEBUG, msg); - throw new Error(msg); - } - if (value % 1) { - log(LOG_DEBUG, "coercing float to int before setting pref " + - branch.root + pref + ": " + value); - value = parseInt(value); - } - branch.setIntPref(pref, value); - break; - - case "Boolean": - branch.setBoolPref(pref, value); - break; - - default: - log(LOG_DEBUG, "coercing object into string before setting pref " + - branch.root + pref); - value = value.toString(); - // fall through - - case "String": - { - let str = new Instances.SupportsString(); - str.data = value; - branch.setComplexValue(pref, Ci.nsISupportsString, str); - } - break; - } - }; - this.delete = function(pref) { - branch.clearUserPref(pref); - }; - this.observe = function(pref, callback, defaultValue) { - let obs = { - observe: function(s, t, d) { - [s, t, d] = [s, t, d]; - callback(pref, get(pref, defaultValue)); - } - }; - branch.addObserver(pref, obs, false); - unload(() => branch.removeObserver(pref, obs)); - obs.observe(); - }; -} - -(function setDefaultPrefs() { - let branch = new Branch(Services.prefs.getDefaultBranch("")); - let scope = { - pref: (key, val) => { - try { - branch.set(key, val); - } - catch (ex) { - Cu.reportError(ex); - } - } - }; - try { - Services.scriptloader.loadSubScript(BASE_PATH + - "defaults/preferences/prefs.js", scope); - } - // errors here should not kill addon - catch (ex) { - log(LOG_ERROR, "failed to setup default preferences", ex); - } -})(); - -var globalPrefs = createProxy(new Branch("")); -var prefs = globalPrefs.extensions[ADDON.id]; - -Object.defineProperties(exports, { - prefs: {get: () => prefs, enumerable: true}, - globalPrefs: {value: globalPrefs, enumerable: true} -}); - -/* vim: set et ts=2 sw=2 : */ diff --git a/sdk/request.js b/sdk/request.js deleted file mode 100644 index 3be32cb..0000000 --- a/sdk/request.js +++ /dev/null @@ -1,119 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -Cu.importGlobalProperties(["XMLHttpRequest", "Blob", "File"]); - -exports.Blob = Blob; -exports.File = File; -exports.xhr = exports.XMLHttpRequest = XMLHttpRequest; - -function Callbacks(underlay) { - try { - this.underlay = underlay.QueryInterface(Ci.nsIInterfaceRequestor); - } - catch (ex) { - this.underlay = null; - } -} -Callbacks.prototype = { - badHost: null, - getInterface: function(iid) { - try { - return this.QueryInterface(iid); - } - catch (ex) { - if (this.underlay) { - return this.underlay.getInterface(iid); - } - throw ex; - } - }, - QueryInterface: XPCOMUtils.generateQI([Ci.nsIBadCertListener2, Ci.nsIInterfaceRequestor]), - notifyCertProblem: function(si, status, host) { - this.badHost = host; - return false; - } -}; - -exports.request = (method, url, data, options) => { - options = options || {}; - return new Promise((resolve, reject) => { - let req = new XMLHttpRequest(); - let cbs = null; - try { - url = url.spec || url; - if (method == "POST" && !data) { - let [u, ...pieces] = url.split("?"); - url = u; - data = pieces.length && pieces.join("?") || null; - } - req.timeout = options.timeout || 10000; - if (options.mime) { - req.overrideMimeType(options.mime); - } - if (options.type) { - req.responseType = options.type; - } - else { - req.responseType = "json"; - } - if (options.setup) { - options.setup(req); - } - req.addEventListener("loadend", function end() { - req.removeEventListener("loadend", end, false); - try { - if (req.status < 100 || req.status >= 400) { - if (cbs && cbs.badHost) { - throw new Error(`${cbs.badHost} has an invalid TLS Certificate`); - } - throw new Error("Bad response"); - } - log(LOG_DEBUG, `request to ${url} resolved`); - resolve(req); - } - catch (ex) { - log(LOG_DEBUG, `request to ${url} rejected ${ex}`); - reject(ex); - } - }, false); - log(LOG_DEBUG, `Making request to ${url} w/ data ${data}`); - req.open(method, url); - if (req.channel) { - try { - cbs = new Callbacks(req.channel.notificationCallbacks); - req.channel.notificationCallbacks = cbs; - } - catch (ex) { - log(LOG_DEBUG, "Failed to set callbacks", ex); - } - if (req.channel instanceof Ci.nsIHttpChannel) { - if (options.cookies) { - for (let n of Object.keys(options.cookies)) { - req.channel.setRequestHeader("Cookie", `${encodeURIComponent(n)}=${encodeURIComponent(options.cookies[n])}`, true); - } - } - if (options.headers) { - for (let n of Object.keys(options.headers)) { - let h = options.headers[n]; - let merge = true; - if (Array.isArray(h)) { - merge = !!h[1]; - h = h[0]; - } - req.channel.setRequestHeader(n, h, merge); - } - } - } - } - req.send(data || null); - } - catch (ex) { - reject(ex); - } - }); -}; - -/* vim: set et ts=2 sw=2 : */ diff --git a/sdk/strings.js b/sdk/strings.js deleted file mode 100644 index 68bbfa6..0000000 --- a/sdk/strings.js +++ /dev/null @@ -1,41 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -var bundleFlush = null; - -function Bundle(uri) { - lazy(this, "_bundle", function() { - if (!bundleFlush) { - bundleFlush = unload(() => Services.strings.flushBundles()); - } - log(LOG_DEBUG, `loading bundle ${uri}`); - return Services.strings.createBundle(uri); - }); - this._dict = Object.create(null); -} -Bundle.prototype = { - getString: function getString(id /*, args */) { - try { - if (arguments.length > 1) { - let args = Array.slice(arguments, 1); - return this._bundle.formatStringFromName(id, args, args.length); - } - return this._bundle.GetStringFromName(id); - } - catch (ex) { - log(LOG_ERROR, "invalid string bundle id: " + id, ex); - throw ex; - } - } -}; - -Object.defineProperty(exports, "getBundle", {value: uri => { - let bundle = new Bundle(uri); - let rv = bundle.getString.bind(bundle); - rv.getString = rv; - return rv; -}}); - -/* vim: set et ts=2 sw=2 : */ diff --git a/sdk/timers.js b/sdk/timers.js deleted file mode 100644 index 99c9417..0000000 --- a/sdk/timers.js +++ /dev/null @@ -1,90 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -Instances.register("Timer", "@mozilla.org/timer;1", "nsITimer", "init"); - -const { - TYPE_ONE_SHOT, - TYPE_REPEATING_SLACK -} = Ci.nsITimer; - -const CLAMP = globalPrefs.dom.min_timeout_value; - -function uuid() { - return Services.uuid.generateUUID().toString(); -} - -let timers = Object.create(Object.prototype, { - create: {value: function(type, func, delay, args) { - let id = uuid(); - let timer = new Instances.Timer({ - observe: function() { - log(LOG_DEBUG, "timer " + id + "fired delay: " + delay + - " type:" + type); - try { - func.apply(null, args); - } - finally { - if (type == TYPE_ONE_SHOT) { - delete this[id]; - } - } - } - }, delay, type); - this[id] = timer; - log(LOG_DEBUG, "timer " + id + " created delay: " + delay + - " type:" + type); - return id; - }}, - destroy: {value: function(id) { - if (id in this) { - try { - this[id].cancel(); - } catch (ex) {} - delete this[id]; - log(LOG_DEBUG, "timer " + id + " destroyed"); - } - }} -}); -unload(function unload_timers() { - for (let [id,timer] in new Iterator(timers)) { - try { - timer.cancel(); - } catch (ex) {} - delete timers[id]; - } -}); - -Object.defineProperties(exports, { - createTimeout: { - value: function createTimeout(func, delay /*, ... */) { - let args = Array.slice(2); - return timers.create(TYPE_ONE_SHOT, func, Math.max(delay, CLAMP), args); - }, - enumerable: true - }, - createInterval: { - value: function createInterval(func, delay /*, ... */) { - let args = Array.slice(2); - return timers.create(TYPE_REPEATING_SLACK, func, - Math.max(delay, CLAMP), args); - }, - enumerable: true - }, - defer: { - value: function defer(func /*. ... */) { - let args = Array.slice(1); - return timers.create(TYPE_ONE_SHOT, func, 0, args); - }, - enumerable: true - }, - destroy: { - value: id => timers.destroy(id), - enumerable: true - } -}); -Object.freeze(exports); - -/* vim: set et ts=2 sw=2 : */ diff --git a/sdk/windows.js b/sdk/windows.js deleted file mode 100644 index 429d697..0000000 --- a/sdk/windows.js +++ /dev/null @@ -1,222 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -Instances.register("XHR", - "@mozilla.org/xmlextras/xmlhttprequest;1", - "nsIXMLHttpRequest"); - -/** - * Specialized unloader that will trigger whenever either the window gets - * unloaded or the add-on is shut down - */ -exports.unloadWindow = function unloadWindow(window, fn) { - let handler = unload(function() { - window.removeEventListener('unload', handler, false); - try { - fn(); - } - catch (ex) { - log(LOG_ERROR, "failed to run window unloader", ex); - } - }); - window.addEventListener('unload', handler, false); -}; - -/** - * Apply a callback to each open and new browser windows. - */ -exports.watchWindows = function watchWindows(location, callback) { - // Wrap the callback in a function that ignores failures - function watcher(window) { - log(LOG_DEBUG, "watchwindows watcher"); - try { - callback(window, window.document); - } - catch(ex) { - log(LOG_ERROR, "window watcher failed", ex); - } - } - - // Wait for the window to finish loading before running the callback - function runOnLoad(window) { - // Listen for one load event before checking the window type - window.addEventListener("load", function runOnLoad_load() { - window.removeEventListener("load", runOnLoad_load, false); - - // Now that the window has loaded, only handle requested windows - if (window.location == location) { - watcher(window); - } - else { - log(LOG_DEBUG, "skipping window: " + window.location + - " as another location was requested: " + location); - } - }, false); - } - - // Add functionality to existing windows - let windows = Services.wm.getEnumerator(null); - while (windows.hasMoreElements()) { - // Only run the watcher immediately if the browser is completely loaded - let window = windows.getNext(); - if (window.document.readyState == "complete") { - if (window.location == location) { - watcher(window); - } - else { - log(LOG_DEBUG, "skipping early window: " + window.location + - " as another location was requested: " + location); - } - } - // Wait for the window to load before continuing - else { - runOnLoad(window); - } - } - - // Watch for new browser windows opening then wait for it to load - function windowWatcher(subject, topic) { - if (topic == "domwindowopened") { - runOnLoad(subject); - } - } - Services.ww.registerNotification(windowWatcher); - - // Make sure to stop watching for windows if we're unloading - unload(() => Services.ww.unregisterNotification(windowWatcher)); -}; - -/** - * Register a new overlay (XUL) - */ -exports.registerOverlay = function registerOverlay(src, location, callback) { - function inject(xul, window, document) { - function $(id) { - return document.getElementById(id); - } - function $$(q) { - return document.querySelector(q); - } - - // loadOverlay for the poor - function addNode(target, node) { - // helper: insert according to position - function insertX(nn, attr, callback) { - if (!nn.hasAttribute(attr)) { - return false; - } - let places = nn.getAttribute(attr) - .split(',') - .map(p => p.trim()) - .filter(p => !!p); - for (let p of places) { - let pn = $$('#' + target.id + ' > #' + p); - if (!pn) { - continue; - } - callback(pn); - return true; - } - return false; - } - - // bring the node to be inserted into the document - let nn = document.importNode(node, true); - - // try to insert according to insertafter/before - if (insertX(nn, 'insertafter', - pn => pn.parentNode.insertBefore(nn, pn.nextSibling)) || - insertX(nn, 'insertbefore', - pn => pn.parentNode.insertBefore(nn, pn))) { - } - // just append - else { - target.appendChild(nn); - } - return nn; - } - log(LOG_DEBUG, "gonna stuff: " + src + " into: " + location); - - try { - // store unloaders for all elements inserted - let unloaders = []; - - // Add all overlays - for (let id in xul) { - let target = $(id); - if (!target) { - log(LOG_INFO, "no target for " + id + ", not inserting"); - continue; - } - - // insert all children - for (let n = xul[id].firstChild; n; n = n.nextSibling) { - if (n.nodeType != n.ELEMENT_NODE) { - continue; - } - let nn = addNode(target, n); - unloaders.push(() => nn.parentNode.removeChild(nn)); - } - } - // install per-window unloader - if (unloaders.length) { - exports.unloadWindow(window, () => unloaders.forEach(u => u())); - } - - callback && callback(window, document); - } - catch (ex) { - log(LOG_ERROR, "failed to inject xul", ex); - } - } - - let _r = new Instances.XHR(); - _r.onload = function() { - log(LOG_DEBUG, "loaded: " + src); - let document = _r.responseXML; - - // clean the document a bit - let emptyNodes = document.evaluate( - "//text()[normalize-space(.) = '']", document, null, 7, null); - for (let i = 0, e = emptyNodes.snapshotLength; i < e; ++i) { - let n = emptyNodes.snapshotItem(i); - n.parentNode.removeChild(n); - } - - // prepare all elements to be inserted - let xul = {}; - for (let n = document.documentElement.firstChild; n; n = n.nextSibling) { - if (n.nodeType != n.ELEMENT_NODE || !n.hasAttribute("id")) { - continue; - } - let id = n.getAttribute("id"); - xul[id] = n; - } - if (!Object.keys(xul).length) { - log(LOG_INFO, "There is only XUL ... but there wasn't"); - return; - } - exports.watchWindows(location, inject.bind(null, xul)); - }; - _r.onerror = _r.onabort = function() { - log(LOG_ERROR, "Failed to load " + src); - } - _r.overrideMimeType("application/xml"); - _r.open("GET", BASE_PATH + src); - // Elevate the request, so DTDs will work. Not a security issue since we - // always load from BASE_PATH, and that is our privileged chrome package. - // This is no different than regular overlays. - let sec = Cc['@mozilla.org/scriptsecuritymanager;1']. - getService(Ci.nsIScriptSecurityManager); - try { - _r.channel.owner = sec.getSystemPrincipal(); - } - catch (ex) { - log(LOG_ERROR, "Failed to set system principal"); - } - _r.send(); -}; - -/* vim: set et ts=2 sw=2 : */