src/{content,dom,hinting}.ts: Implement addEventListener hijacking

This commit makes Tridactyl aware of elements made interactive through
JavaScript and lets Tridactyl put hints on them.
Something possibly dangerous is done here: exporting a function to the
page's context. While it is believed that the current implementation is
secure and that pages can't discover whether the function has been
exported or not, this might change due to new standards being adopted by
firefox. One of these standards is the Custom Elements api:
https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements

It will be necessary to check how Tridactyl behaves once this API has
landed. The status of this API seems to be tracked here:
https://bugzilla.mozilla.org/show_bug.cgi?id=1406825
This commit is contained in:
glacambre 2017-12-30 08:43:19 +01:00
parent a4a3d4feb5
commit f584c67f16
No known key found for this signature in database
GPG key ID: B9625DB1767553AC
3 changed files with 73 additions and 2 deletions

View file

@ -41,3 +41,5 @@ import * as webext from './lib/webext'
webext,
l: prom => prom.then(console.log).catch(console.error),
})
dom.hijackPageListenerFunctions()

View file

@ -270,3 +270,69 @@ export function compareElementArea(a: HTMLElement, b: HTMLElement): number {
return aArea - bArea
}
export const hintworthy_js_elems = []
/** Adds or removes an element from the hintworthy_js_elems array of the
* current tab.
* This function is made available to the page. We should be very careful
* about the input it accepts and what it does with it. It should especially
* be tested again when Custom elements land in Firefox.
* https://bugzilla.mozilla.org/show_bug.cgi?id=1406825
* @param {EventTarget} elem The element add/removeEventListener is called on
* @param {boolean} add true when called from addEventListener,
* false from removeEventListener
* @param {string} event The event name given to add/removeEventListener
*/
export function registerEvListenerAction(elem: EventTarget, add: boolean, event: string) {
// Make sure elem is can exist in a document
try {
// We need to clone elem here otherwise it will be removed frome the page
document.createDocumentFragment().appendChild((elem as Node).cloneNode())
} catch (e) {
return
}
switch (event) {
case "click":
case "mousedown":
case "mouseup":
case "mouseover":
if (add) {
hintworthy_js_elems.push(elem)
} else {
// Possible bug: If a page adds an event listener for "click" and
// "mousedown" and removes "mousedown" twice, we lose track of the
// elem even though it still has a "click" listener.
// Fixing this might not be worth the added complexity.
let index = hintworthy_js_elems.indexOf(elem)
if (index >= 0)
hintworthy_js_elems.splice(index, 1)
}
}
}
/** Replace the page's addEventListener with a closure containing a reference
* to the original addEventListener and [[registerEvListenerAction]]. Do the
* same with removeEventListener.
*/
export function hijackPageListenerFunctions(): void {
let exportedName = 'registerEvListenerAction'
exportFunction(registerEvListenerAction, window, {defineAs: exportedName})
let eval_str = ["addEventListener", "removeEventListener"].reduce((acc, cur) => `${acc};
EventTarget.prototype.${cur} = ((realFunction, register) => {
return function (...args) {
let result = realFunction.apply(this, args)
try {
register(this, ${cur === "addEventListener"}, args[0])
} catch (e) {
// Don't let the page know something wrong happened here
}
return result
}
})(EventTarget.prototype.${cur}, ${exportedName})`
, "")
window.eval(eval_str + `;delete ${exportedName}`)
}

View file

@ -12,7 +12,7 @@
import * as DOM from './dom'
import {log} from './math'
import {permutationsWithReplacement, islice, izip, map} from './itertools'
import {permutationsWithReplacement, islice, izip, map, unique} from './itertools'
import {hasModifiers} from './keyseq'
import state from './state'
import {messageActiveTab, message} from './messaging'
@ -371,7 +371,10 @@ function pushKey(ke) {
2. Not hidden by another element
*/
function hintables(selectors=HINTTAGS_selectors) {
return DOM.getElemsBySelector(selectors, [DOM.isVisible])
let elems = DOM.getElemsBySelector(selectors, [])
elems = elems.concat(DOM.hintworthy_js_elems)
elems = unique(elems)
return elems.filter(DOM.isVisible)
}
function elementswithtext() {