Skip to content

Commit

Permalink
feat: implement Firefox-only inject-into mode wrap
Browse files Browse the repository at this point in the history
  • Loading branch information
gera2ld committed Nov 21, 2019
1 parent 81d68e7 commit dea7de0
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 86 deletions.
15 changes: 14 additions & 1 deletion src/common/consts.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
export const INJECT_AUTO = 'auto';
export const INJECT_PAGE = 'page';
export const INJECT_CONTENT = 'content';
export const INJECT_AUTO = 'auto';

export const INJECT_INTERNAL_PAGE = 'page';
export const INJECT_INTERNAL_CONTENT = 'content';
export const INJECT_INTERNAL_WRAP = 'wrap';

export const INJECT_MAPPING = {
// `auto` tries to provide `window` from the real page as `unsafeWindow`
[INJECT_AUTO]: [INJECT_INTERNAL_PAGE, INJECT_INTERNAL_WRAP, INJECT_INTERNAL_CONTENT],
// inject into page context, if failed, try `wrap` mode for Firefox
[INJECT_PAGE]: [INJECT_INTERNAL_PAGE, INJECT_INTERNAL_WRAP],
// inject into content context only
[INJECT_CONTENT]: [INJECT_INTERNAL_CONTENT],
};

export const CMD_SCRIPT_ADD = 'AddScript';
export const CMD_SCRIPT_UPDATE = 'UpdateScript';
Expand Down
5 changes: 4 additions & 1 deletion src/common/options-defaults.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { INJECT_AUTO } from './consts';

export default {
isApplied: true,
autoUpdate: true,
Expand All @@ -18,7 +20,8 @@ export default {
importSettings: true,
notifyUpdates: false,
version: null,
defaultInjectInto: 'page', // 'page' | 'auto',
/** @type 'auto' | 'page' | 'content' */
defaultInjectInto: INJECT_AUTO,
filters: {
/** @type 'exec' | 'alpha' | 'update' */
sort: 'exec',
Expand Down
13 changes: 1 addition & 12 deletions src/common/ui/setting-text.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<textarea
class="monospace-font"
:class="{'has-error': error}"
spellcheck="false"
v-model="value"
:disabled="disabled"
Expand Down Expand Up @@ -62,15 +63,3 @@ export default {
},
};
</script>

<style>
textarea {
&[title] {
border-color: #4004;
background: #f001;
&:focus {
border-color: #400c;
}
}
}
</style>
7 changes: 7 additions & 0 deletions src/common/ui/style/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ textarea {
&:focus {
border-color: darkgray;
}
&.has-error {
border-color: #4004;
background: #f001;
&:focus {
border-color: #400c;
}
}
}
code {
padding: 0 .2em;
Expand Down
47 changes: 31 additions & 16 deletions src/injected/content/inject.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { isFirefox } from '#/common/ua';
import { getUniqId, sendCmd } from '#/common';
import {
INJECT_PAGE, INJECT_CONTENT, INJECT_AUTO,
INJECT_AUTO,
INJECT_CONTENT,
INJECT_INTERNAL_CONTENT,
INJECT_INTERNAL_PAGE,
INJECT_INTERNAL_WRAP,
INJECT_MAPPING,
INJECT_PAGE,
browser,
} from '#/common/consts';
import { getUniqId, sendCmd } from '#/common';
import { isFirefox } from '#/common/ua';

import { attachFunction } from '../utils';
import bridge from './bridge';
import {
forEach, join, jsonDump, setJsonDump, append, createElementNS, NS_HTML,
charCodeAt, fromCharCode,
} from '../utils/helpers';
import bridge from './bridge';

// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1408996
const VMInitInjection = window[process.env.INIT_FUNC_NAME];
Expand All @@ -26,9 +33,12 @@ bridge.addHandlers({

export function triageScripts(data) {
const scriptLists = {
[INJECT_PAGE]: [],
[INJECT_CONTENT]: [],
[INJECT_INTERNAL_PAGE]: [],
[INJECT_INTERNAL_CONTENT]: [],
};
// `INJECT_INTERNAL_WRAP` is a special case of `INJECT_INTERNAL_CONTENT`
// so we will just reuse the list to keep the execution orders
scriptLists[INJECT_INTERNAL_WRAP] = scriptLists[INJECT_INTERNAL_CONTENT];
if (data.scripts) {
data.scripts = data.scripts.filter(({ meta, props, config }) => {
if (!meta.noframes || window.top === window) {
Expand All @@ -41,14 +51,19 @@ export function triageScripts(data) {
return false;
});
let support;
data.scripts.forEach((script) => {
let injectInto = script.custom.injectInto || script.meta.injectInto || data.injectInto;
if (injectInto === INJECT_AUTO) {
const injectChecking = {
[INJECT_INTERNAL_PAGE]: () => {
if (!support) support = { injectable: checkInjectable() };
injectInto = support.injectable ? INJECT_PAGE : INJECT_CONTENT;
}
const list = scriptLists[injectInto];
if (list) list.push(script);
return support.injectable;
},
[INJECT_INTERNAL_WRAP]: () => isFirefox,
[INJECT_INTERNAL_CONTENT]: () => true,
};
data.scripts.forEach((script) => {
const injectInto = script.custom.injectInto || script.meta.injectInto || data.injectInto;
const internalInjectInto = INJECT_MAPPING[injectInto] || INJECT_MAPPING[INJECT_AUTO];
const availableInjectInto = internalInjectInto.find(key => injectChecking[key]?.());
scriptLists[availableInjectInto]?.push({ script, injectInto: availableInjectInto });
});
}
return scriptLists;
Expand All @@ -73,7 +88,7 @@ export function injectScripts(contentId, webId, data, scriptLists) {
if (injectContent.length) {
const invokeGuest = VMInitInjection()(...args, bridge.onHandle);
const postViaBridge = bridge.post;
bridge.invokableIds.push(...injectContent.map(script => script.props.id));
bridge.invokableIds.push(...injectContent.map(({ script }) => script.props.id));
bridge.post = msg => (
msg.realm === INJECT_CONTENT
? invokeGuest(msg)
Expand All @@ -84,7 +99,7 @@ export function injectScripts(contentId, webId, data, scriptLists) {
data: {
...data,
mode: INJECT_CONTENT,
scripts: injectContent,
items: injectContent,
},
realm: INJECT_CONTENT,
});
Expand All @@ -97,7 +112,7 @@ export function injectScripts(contentId, webId, data, scriptLists) {
data: {
...data,
mode: INJECT_PAGE,
scripts: injectPage,
items: injectPage,
},
});
}
Expand Down
56 changes: 32 additions & 24 deletions src/injected/web/gm-wrapper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { INJECT_PAGE, INJECT_CONTENT, METABLOCK_RE } from '#/common/consts';
import {
INJECT_INTERNAL_PAGE, INJECT_INTERNAL_CONTENT, INJECT_INTERNAL_WRAP, METABLOCK_RE,
} from '#/common/consts';
import bridge from './bridge';
import {
forEach, includes, match, objectKeys, map, defineProperties, filter, defineProperty, slice,
Expand All @@ -12,13 +14,20 @@ const { startsWith } = String.prototype;
// - some properties (like `isFinite`) are defined in `global` but not `window`
// - all `window` properties can be accessed from `global`

// store the initial eval now (before the page scripts run) just in case
let wrapperInfo = {
[INJECT_CONTENT]: { unsafeWindow: global },
[INJECT_PAGE]: { unsafeWindow: window },
// store the initial eval now (before the page scripts run) just in case
eval: {
[INJECT_CONTENT]: global.eval, // eslint-disable-line no-eval
[INJECT_PAGE]: window.eval, // eslint-disable-line no-eval
global,
eval: global.eval, // eslint-disable-line no-eval
unsafeWindow: {
// run script in page context
// `unsafeWindow === pageWindow === pageGlobal`
[INJECT_INTERNAL_PAGE]: global,
// run script in content context
// `unsafeWindow === contentGlobal, contentGlobal.window === contentWindow`
[INJECT_INTERNAL_CONTENT]: global,
// run script in content context, but access pageWindow with the Firefox specific `wrappedJSObject`
// `unsafeWindow === wrappedJSObject === pageWindow`
[INJECT_INTERNAL_WRAP]: global.wrappedJSObject || global,
},
};

Expand All @@ -31,8 +40,8 @@ export function deletePropsCache() {
bridge.props = null;
}

export function wrapGM(script, code, cache) {
const { unsafeWindow } = wrapperInfo[bridge.mode];
export function wrapGM(script, code, cache, injectInto) {
const unsafeWindow = wrapperInfo.unsafeWindow[injectInto];
// Add GM functions
// Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
const gm = {};
Expand All @@ -58,7 +67,7 @@ export function wrapGM(script, code, cache) {
scriptWillUpdate: !!script.config.shouldUpdate,
scriptHandler: 'Violentmonkey',
version: bridge.version,
injectInto: bridge.mode,
injectInto,
script: {
description: script.meta.description || '',
excludes: [...script.meta.exclude],
Expand Down Expand Up @@ -121,7 +130,7 @@ export function wrapGM(script, code, cache) {
}

function createWrapperMethods(info) {
const { unsafeWindow } = info;
const { global } = info;
const methods = {};
[
// 'uneval',
Expand Down Expand Up @@ -171,9 +180,9 @@ function createWrapperMethods(info) {
'setTimeout',
'stop',
]::forEach((name) => {
const method = unsafeWindow[name];
const method = global[name];
if (method) {
methods[name] = (...args) => method.apply(unsafeWindow, args);
methods[name] = (...args) => method.apply(global, args);
}
});
info.methods = methods;
Expand All @@ -183,18 +192,17 @@ function createWrapperMethods(info) {
* @desc Wrap helpers to prevent unexpected modifications.
*/
function getWrapper() {
const info = wrapperInfo[bridge.mode];
const { unsafeWindow } = info;
if (!info.methods) createWrapperMethods(info);
const { global } = wrapperInfo;
if (!wrapperInfo.methods) createWrapperMethods(wrapperInfo);
// http://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects
// http://developer.mozilla.org/docs/Web/API/Window
const wrapper = {
// `eval` should be called directly so that it is run in current scope
eval: wrapperInfo.eval[bridge.mode],
...info.methods,
eval: wrapperInfo.eval,
...wrapperInfo.methods,
};
if (!info.propsToWrap) {
info.propsToWrap = bridge.props::filter(p => !(p in wrapper));
if (!wrapperInfo.propsToWrap) {
wrapperInfo.propsToWrap = bridge.props::filter(p => !(p in wrapper));
bridge.props = null;
}

Expand All @@ -209,7 +217,7 @@ function getWrapper() {
let value;
defineProperty(wrapper, name, {
get() {
if (!modified) value = wrapWindowValue(unsafeWindow[name]);
if (!modified) value = wrapWindowValue(global[name]);
return value;
},
set(val) {
Expand All @@ -224,10 +232,10 @@ function getWrapper() {
function defineReactedProperty(name) {
defineProperty(wrapper, name, {
get() {
return wrapWindowValue(unsafeWindow[name]);
return wrapWindowValue(global[name]);
},
set(val) {
unsafeWindow[name] = val;
global[name] = val;
},
});
}
Expand All @@ -236,7 +244,7 @@ function getWrapper() {
// A major GC may hit here no matter how we define props
// all at once, in batches of 10, 100, 500, or one by one.
// TODO: try Proxy API if userscripts wouldn't notice the difference
info.propsToWrap::forEach((name) => {
wrapperInfo.propsToWrap::forEach((name) => {
if (name::startsWith('on')) {
defineReactedProperty(name);
} else {
Expand Down
11 changes: 6 additions & 5 deletions src/injected/web/load-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ bridge.addHandlers({
'document-idle': idle,
'document-end': end,
};
if (data.scripts) {
data.scripts.forEach((script) => {
if (data.items) {
data.items.forEach((item) => {
const { script } = item;
const runAt = script.custom.runAt || script.meta.runAt;
const list = listMap[runAt] || end;
list.push(script);
list.push(item);
store.values[script.props.id] = data.values[script.props.id];
});
run(start);
Expand All @@ -53,12 +54,12 @@ bridge.addHandlers({
}
if (store.state) bridge.load();

function buildCode(script) {
function buildCode({ script, injectInto }) {
const pathMap = script.custom.pathMap || {};
const requireKeys = script.meta.require || [];
const requires = requireKeys::map(key => data.require[pathMap[key] || key])::filter(Boolean);
const code = data.code[script.props.id] || '';
const { wrapper, thisObj, keys } = wrapGM(script, code, data.cache);
const { wrapper, thisObj, keys } = wrapGM(script, code, data.cache, injectInto);
const id = getUniqId('VMin');
const fnId = getUniqId('VMfn');
const codeSlices = [
Expand Down
2 changes: 1 addition & 1 deletion src/options/views/edit/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
</nav>
<div class="edit-name text-center ellipsis flex-1 mr-1" v-text="scriptName"/>
<div class="edit-hint text-right ellipsis mr-1">
<a href="https://violentmonkey.github.io/2017/03/14/How-to-edit-scripts-with-your-favorite-editor/"
<a href="https://violentmonkey.github.io/posts/how-to-edit-scripts-with-your-favorite-editor/"
target="_blank"
rel="noopener noreferrer"
v-text="i18n('editHowToHint')"/>
Expand Down
Loading

0 comments on commit dea7de0

Please sign in to comment.