gm-wrapper.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import { INJECT_CONTENT } from '#/common/consts';
  2. import bridge from './bridge';
  3. import { makeGmApi, vmOwnFunc } from './gm-api';
  4. const {
  5. Proxy,
  6. Set, // 2x-3x faster lookup than object::has
  7. Symbol: { toStringTag, iterator: iterSym },
  8. Map: { [Prototype]: { get: mapGet, has: mapHas, [iterSym]: mapIter } },
  9. Set: { [Prototype]: { delete: setDelete, has: setHas, [iterSym]: setIter } },
  10. Object: { getOwnPropertyNames, getOwnPropertySymbols },
  11. } = global;
  12. const { concat, slice: arraySlice } = [];
  13. const { startsWith } = '';
  14. /** Name in Greasemonkey4 -> name in GM */
  15. const GM4_ALIAS = {
  16. __proto__: null, // Object.create(null) may be spoofed
  17. getResourceUrl: 'getResourceURL',
  18. xmlHttpRequest: 'xmlhttpRequest',
  19. };
  20. const GM4_ASYNC = [
  21. 'getResourceUrl',
  22. 'getValue',
  23. 'deleteValue',
  24. 'setValue',
  25. 'listValues',
  26. ];
  27. const IS_TOP = window.top === window;
  28. let gmApi;
  29. let componentUtils;
  30. export function wrapGM(script) {
  31. // Add GM functions
  32. // Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
  33. const grant = script.meta.grant || [];
  34. if (grant.length === 1 && grant[0] === 'none') {
  35. grant.length = 0;
  36. }
  37. const id = script.props.id;
  38. const resources = script.meta.resources || createNullObj();
  39. const context = {
  40. id,
  41. script,
  42. resources,
  43. dataKey: script.dataKey,
  44. pathMap: script.custom.pathMap || createNullObj(),
  45. urls: createNullObj(),
  46. };
  47. const gmInfo = makeGmInfo(script, resources);
  48. const gm = {
  49. __proto__: null, // Object.create(null) may be spoofed
  50. GM: {
  51. __proto__: null,
  52. info: gmInfo,
  53. },
  54. GM_info: gmInfo,
  55. unsafeWindow: global,
  56. };
  57. if (!componentUtils) {
  58. componentUtils = makeComponentUtils();
  59. }
  60. // not using ...spread as it calls Babel's polyfill that calls unsafe Object.xxx
  61. assign(gm, componentUtils);
  62. if (grant::includes('window.close')) {
  63. gm.close = vmOwnFunc(() => bridge.post('TabClose', 0, context));
  64. }
  65. if (grant::includes('window.focus')) {
  66. gm.focus = vmOwnFunc(() => bridge.post('TabFocus', 0, context));
  67. }
  68. if (!gmApi && grant.length) gmApi = makeGmApi();
  69. grant::forEach((name) => {
  70. const gm4name = name::startsWith('GM.') && name::slice(3);
  71. const fn = gmApi[gm4name ? `GM_${GM4_ALIAS[gm4name] || gm4name}` : name];
  72. if (fn) {
  73. if (gm4name) {
  74. gm.GM[gm4name] = makeGmMethodCaller(fn, context, GM4_ASYNC::includes(gm4name));
  75. } else {
  76. gm[name] = makeGmMethodCaller(fn, context);
  77. }
  78. }
  79. });
  80. return grant.length ? makeGlobalWrapper(gm) : gm;
  81. }
  82. function makeGmInfo(script, resources) {
  83. const { meta } = script;
  84. const metaCopy = {};
  85. objectKeys(meta)::forEach((key) => {
  86. let val = meta[key];
  87. switch (key) {
  88. case 'match': // -> matches
  89. case 'excludeMatch': // -> excludeMatches
  90. key += 'e';
  91. // fallthrough
  92. case 'exclude': // -> excludes
  93. case 'include': // -> includes
  94. key += 's';
  95. val = val::arraySlice(); // not using [...val] as it can be broken via Array#Symbol.iterator
  96. break;
  97. default:
  98. }
  99. metaCopy[key] = val;
  100. });
  101. [
  102. 'description',
  103. 'name',
  104. 'namespace',
  105. 'runAt',
  106. 'version',
  107. ]::forEach((key) => {
  108. if (!metaCopy[key]) metaCopy[key] = '';
  109. });
  110. metaCopy.resources = objectKeys(resources)::map(name => ({
  111. name,
  112. url: resources[name],
  113. }));
  114. metaCopy.unwrap = false; // deprecated, always `false`
  115. return {
  116. uuid: script.props.uuid,
  117. scriptMetaStr: script.metaStr,
  118. scriptWillUpdate: !!script.config.shouldUpdate,
  119. scriptHandler: 'Violentmonkey',
  120. version: process.env.VM_VER,
  121. injectInto: bridge.mode,
  122. platform: assign({}, bridge.ua),
  123. script: metaCopy,
  124. };
  125. }
  126. function makeGmMethodCaller(gmMethod, context, isAsync) {
  127. // keeping the native console.log intact
  128. return gmMethod === gmApi.GM_log ? gmMethod : vmOwnFunc(
  129. isAsync
  130. ? (async (...args) => gmMethod::apply(context, args))
  131. : gmMethod::bind(context),
  132. );
  133. }
  134. const globalKeys = getOwnPropertyNames(window).filter(key => !isFrameIndex(key, true));
  135. /* Chrome and FF page mode: `global` is `window`
  136. FF content mode: `global` is different, some props e.g. `isFinite` are defined only there */
  137. if (global !== window) {
  138. const set = new Set(globalKeys);
  139. getOwnPropertyNames(global).forEach(key => {
  140. if (!isFrameIndex(key) && !set.has(key)) {
  141. globalKeys.push(key);
  142. }
  143. });
  144. }
  145. // FF doesn't expose wrappedJSObject as own property so we add it explicitly
  146. if (global.wrappedJSObject) {
  147. globalKeys.push('wrappedJSObject');
  148. }
  149. const inheritedKeys = new Set([
  150. ...getOwnPropertyNames(EventTarget[Prototype]),
  151. ...getOwnPropertyNames(Object[Prototype]),
  152. ]);
  153. inheritedKeys.has = setHas;
  154. /* These can be redefined but can't be assigned, see sandbox-globals.html */
  155. const readonlyKeys = [
  156. 'applicationCache',
  157. 'caches',
  158. 'closed',
  159. 'crossOriginIsolated',
  160. 'crypto',
  161. 'customElements',
  162. 'frameElement',
  163. 'history',
  164. 'indexedDB',
  165. 'isSecureContext',
  166. 'localStorage',
  167. 'mozInnerScreenX',
  168. 'mozInnerScreenY',
  169. 'navigator',
  170. 'sessionStorage',
  171. 'speechSynthesis',
  172. 'styleMedia',
  173. 'trustedTypes',
  174. ].filter(key => key in global); // not using global[key] as some of these (caches) may throw
  175. /* These can't be redefined, see sandbox-globals.html */
  176. const unforgeables = new Map([
  177. 'Infinity',
  178. 'NaN',
  179. 'document',
  180. 'location',
  181. 'top',
  182. 'undefined',
  183. 'window',
  184. ].map(name => {
  185. let thisObj;
  186. const info = (
  187. describeProperty(thisObj = global, name)
  188. || describeProperty(thisObj = window, name)
  189. );
  190. if (info) {
  191. // currently only `document`
  192. if (info.get) info.get = info.get::bind(thisObj);
  193. // currently only `location`
  194. if (info.set) info.set = info.set::bind(thisObj);
  195. }
  196. return info && [name, info];
  197. }).filter(Boolean));
  198. unforgeables.has = mapHas;
  199. unforgeables[iterSym] = mapIter;
  200. /* ~50 methods like alert/fetch/moveBy that need `window` as `this`, see sandbox-globals.html */
  201. const boundMethods = new Map([
  202. 'addEventListener',
  203. 'alert',
  204. 'atob',
  205. 'blur',
  206. 'btoa',
  207. 'cancelAnimationFrame',
  208. 'cancelIdleCallback',
  209. 'captureEvents',
  210. 'clearInterval',
  211. 'clearTimeout',
  212. 'close',
  213. 'confirm',
  214. 'createImageBitmap',
  215. 'dispatchEvent',
  216. 'dump',
  217. 'fetch',
  218. 'find',
  219. 'focus',
  220. 'getComputedStyle',
  221. 'getDefaultComputedStyle',
  222. 'getSelection',
  223. 'matchMedia',
  224. 'moveBy',
  225. 'moveTo',
  226. 'open',
  227. 'openDatabase',
  228. 'postMessage',
  229. 'print',
  230. 'prompt',
  231. 'queueMicrotask',
  232. 'releaseEvents',
  233. 'removeEventListener',
  234. 'requestAnimationFrame',
  235. 'requestIdleCallback',
  236. 'resizeBy',
  237. 'resizeTo',
  238. 'scroll',
  239. 'scrollBy',
  240. 'scrollByLines',
  241. 'scrollByPages',
  242. 'scrollTo',
  243. 'setInterval',
  244. 'setResizable',
  245. 'setTimeout',
  246. 'sizeToContent',
  247. 'stop',
  248. 'updateCommands',
  249. 'webkitCancelAnimationFrame',
  250. 'webkitRequestAnimationFrame',
  251. 'webkitRequestFileSystem',
  252. 'webkitResolveLocalFileSystemURL',
  253. ]
  254. .map((key) => {
  255. const value = global[key];
  256. return typeof value === 'function' && [key, value::bind(global)];
  257. })
  258. .filter(Boolean));
  259. boundMethods.get = mapGet;
  260. /**
  261. * @desc Wrap helpers to prevent unexpected modifications.
  262. */
  263. function makeGlobalWrapper(local) {
  264. const events = createNullObj();
  265. const scopeSym = Symbol.unscopables;
  266. const globals = new Set(globalKeys);
  267. globals[iterSym] = setIter;
  268. globals.delete = setDelete;
  269. globals.has = setHas;
  270. const readonlys = new Set(readonlyKeys);
  271. readonlys.delete = setDelete;
  272. readonlys.has = setHas;
  273. /* Browsers may return [object Object] for Object.prototype.toString(window)
  274. on our `window` proxy so jQuery libs see it as a plain object and throw
  275. when trying to clone its recursive properties like `self` and `window`. */
  276. defineProperty(local, toStringTag, { get: () => 'Window' });
  277. const wrapper = new Proxy(local, {
  278. defineProperty(_, name, desc) {
  279. const isString = typeof name === 'string';
  280. if (!isFrameIndex(name, isString)) {
  281. defineProperty(local, name, desc);
  282. if (isString) maybeSetEventHandler(name);
  283. readonlys.delete(name);
  284. }
  285. return true;
  286. },
  287. deleteProperty(_, name) {
  288. if (!unforgeables.has(name) && delete local[name]) {
  289. globals.delete(name);
  290. return true;
  291. }
  292. },
  293. get(_, name) {
  294. if (name !== 'undefined' && name !== scopeSym) {
  295. const value = local[name];
  296. return value !== undefined || local::hasOwnProperty(name)
  297. ? value
  298. : resolveProp(name);
  299. }
  300. },
  301. getOwnPropertyDescriptor(_, name) {
  302. const ownDesc = describeProperty(local, name);
  303. const desc = ownDesc || globals.has(name) && describeProperty(global, name);
  304. if (!desc) return;
  305. if (desc.value === window) desc.value = wrapper;
  306. // preventing spec violation by duplicating ~10 props like NaN, Infinity, etc.
  307. if (!ownDesc && !desc.configurable) {
  308. const { get } = desc;
  309. if (typeof get === 'function') {
  310. desc.get = get::bind(global);
  311. }
  312. defineProperty(local, name, mapWindow(desc));
  313. }
  314. return desc;
  315. },
  316. has(_, name) {
  317. return name === 'undefined' || local::hasOwnProperty(name) || globals.has(name);
  318. },
  319. ownKeys() {
  320. return [...globals]::concat(
  321. // using ::concat since array spreading can be broken via Array.prototype[Symbol.iterator]
  322. getOwnPropertyNames(local)::filter(notIncludedIn, globals),
  323. getOwnPropertySymbols(local)::filter(notIncludedIn, globals),
  324. );
  325. },
  326. preventExtensions() {},
  327. set(_, name, value) {
  328. const isString = typeof name === 'string';
  329. if (!readonlys.has(name) && !isFrameIndex(name, isString)) {
  330. local[name] = value;
  331. if (isString) maybeSetEventHandler(name, value);
  332. }
  333. return true;
  334. },
  335. });
  336. unforgeables::forEach(entry => {
  337. const name = entry[0];
  338. const desc = entry[1];
  339. if (name === 'window' || name === 'top' && IS_TOP) {
  340. delete desc.get;
  341. delete desc.set;
  342. desc.value = wrapper;
  343. }
  344. defineProperty(local, name, mapWindow(desc));
  345. });
  346. function mapWindow(desc) {
  347. if (desc && desc.value === window) {
  348. desc = assign({}, desc);
  349. desc.value = wrapper;
  350. }
  351. return desc;
  352. }
  353. function resolveProp(name) {
  354. let value = boundMethods.get(name);
  355. const canCopy = value || inheritedKeys.has(name) || globals.has(name);
  356. if (!value && (canCopy || isFrameIndex(name, typeof name === 'string'))) {
  357. value = global[name];
  358. }
  359. if (value === window) {
  360. value = wrapper;
  361. }
  362. if (canCopy && (
  363. typeof value === 'function'
  364. || typeof value === 'object' && value && name !== 'event'
  365. // window.event contains the current event so it's always different
  366. )) {
  367. local[name] = value;
  368. }
  369. return value;
  370. }
  371. function maybeSetEventHandler(name, value) {
  372. if (!name::startsWith('on') || !globals.has(name)) {
  373. return;
  374. }
  375. name = name::slice(2);
  376. window::removeEventListener(name, events[name]);
  377. if (typeof value === 'function') {
  378. // the handler will be unique so that one script couldn't remove something global
  379. // like console.log set by another script
  380. window::addEventListener(name, events[name] = value::bind(window));
  381. } else {
  382. delete events[name];
  383. }
  384. }
  385. return wrapper;
  386. }
  387. // Adding the polyfills in Chrome (always as it doesn't provide them)
  388. // and in Firefox page mode (while preserving the native ones in content mode)
  389. // for compatibility with many [old] scripts that use these utils blindly
  390. function makeComponentUtils() {
  391. const source = bridge.mode === INJECT_CONTENT && global;
  392. return {
  393. cloneInto: source.cloneInto || vmOwnFunc(
  394. (obj) => obj,
  395. ),
  396. createObjectIn: source.createObjectIn || vmOwnFunc(
  397. (targetScope, { defineAs } = {}) => {
  398. const obj = {};
  399. if (defineAs) targetScope[defineAs] = obj;
  400. return obj;
  401. },
  402. ),
  403. exportFunction: source.exportFunction || vmOwnFunc(
  404. (func, targetScope, { defineAs } = {}) => {
  405. if (defineAs) targetScope[defineAs] = func;
  406. return func;
  407. },
  408. ),
  409. };
  410. }
  411. /* The index strings that look exactly like integers can't be forged
  412. but for example '011' doesn't look like 11 so it's allowed */
  413. function isFrameIndex(key, isString) {
  414. return isString && key >= 0 && key <= 0xFFFF_FFFE && key === `${+key}`;
  415. }
  416. /** @this {Set} */
  417. function notIncludedIn(key) {
  418. return !this.has(key);
  419. }