injected.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. !function(){
  2. // Avoid running repeatedly due to new document.documentElement
  3. if (window.VM) return;
  4. window.VM = 1;
  5. var _ = {
  6. getUniqId: function () {
  7. return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
  8. },
  9. sendMessage: function (data) {
  10. return new Promise(function (resolve, reject) {
  11. chrome.runtime.sendMessage(data, function (res) {
  12. res && res.error ? reject(res.error) : resolve(res && res.data);
  13. });
  14. });
  15. },
  16. includes: function (arr, item) {
  17. var length = arr.length;
  18. for (var i = 0; i < length; i ++)
  19. if (arr[i] === item) return true;
  20. return false;
  21. },
  22. forEach: function (arr, func, context) {
  23. var length = arr.length;
  24. for (var i = 0; i < length; i ++)
  25. if (func.call(context, arr[i], i, arr) === false) break;
  26. },
  27. };
  28. /**
  29. * http://www.webtoolkit.info/javascript-utf8.html
  30. */
  31. function utf8decode (utftext) {
  32. var string = "";
  33. var i = 0;
  34. var c = 0, c1 = 0, c2 = 0, c3 = 0;
  35. while ( i < utftext.length ) {
  36. c = utftext.charCodeAt(i);
  37. if (c < 128) {string += String.fromCharCode(c);i++;}
  38. else if((c > 191) && (c < 224)) {
  39. c2 = utftext.charCodeAt(i+1);
  40. string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
  41. i += 2;
  42. } else {
  43. c2 = utftext.charCodeAt(i+1);
  44. c3 = utftext.charCodeAt(i+2);
  45. string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
  46. i += 3;
  47. }
  48. }
  49. return string;
  50. }
  51. function getPopup(){
  52. // XXX: only scripts run in top level window are counted
  53. if(top === window)
  54. _.sendMessage({
  55. cmd: 'SetPopup',
  56. data: {
  57. ids: ids,
  58. menus: menus,
  59. },
  60. });
  61. }
  62. var badge = {
  63. number: 0,
  64. ready: false,
  65. willSet: false,
  66. };
  67. function getBadge(){
  68. badge.willSet = true;
  69. setBadge();
  70. }
  71. function setBadge(){
  72. if (badge.ready && badge.willSet) {
  73. // XXX: only scripts run in top level window are counted
  74. if (top === window)
  75. _.sendMessage({cmd: 'SetBadge', data: badge.number});
  76. }
  77. }
  78. /**
  79. * @desc Wrap methods to prevent unexpected modifications.
  80. */
  81. function getWrapper() {
  82. // http://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects
  83. // http://developer.mozilla.org/docs/Web/API/Window
  84. var comm = this;
  85. var wrapper = {};
  86. // `eval` should be called directly so that it is run in current scope
  87. wrapper.eval = eval;
  88. // Wrap methods
  89. comm.forEach([
  90. // 'uneval',
  91. 'isFinite',
  92. 'isNaN',
  93. 'parseFloat',
  94. 'parseInt',
  95. 'decodeURI',
  96. 'decodeURIComponent',
  97. 'encodeURI',
  98. 'encodeURIComponent',
  99. 'addEventListener',
  100. 'alert',
  101. 'atob',
  102. 'blur',
  103. 'btoa',
  104. 'clearInterval',
  105. 'clearTimeout',
  106. 'close',
  107. 'confirm',
  108. 'dispatchEvent',
  109. 'find',
  110. 'focus',
  111. 'getComputedStyle',
  112. 'getSelection',
  113. 'matchMedia',
  114. 'moveBy',
  115. 'moveTo',
  116. 'open',
  117. 'openDialog',
  118. 'postMessage',
  119. 'print',
  120. 'prompt',
  121. 'removeEventListener',
  122. 'resizeBy',
  123. 'resizeTo',
  124. 'scroll',
  125. 'scrollBy',
  126. 'scrollByLines',
  127. 'scrollByPages',
  128. 'scrollTo',
  129. 'setInterval',
  130. 'setTimeout',
  131. 'stop',
  132. ], function (name) {
  133. var method = window[name];
  134. wrapper[name] = function () {
  135. return method.apply(window, arguments);
  136. };
  137. });
  138. // Wrap properties
  139. comm.forEach(comm.props, function (name) {
  140. if (wrapper[name]) return;
  141. var modified = false;
  142. var value;
  143. Object.defineProperty(wrapper, name, {
  144. get: function () {
  145. if (!modified) value = window[name];
  146. return value === window ? wrapper : value;
  147. },
  148. set: function (val) {
  149. modified = true;
  150. value = val;
  151. },
  152. });
  153. });
  154. return wrapper;
  155. }
  156. // Communicator
  157. var comm = {
  158. vmid: 'VM_' + _.getUniqId(),
  159. state: 0,
  160. utf8decode: utf8decode,
  161. getUniqId: _.getUniqId,
  162. // Array functions
  163. // to avoid using prototype functions
  164. // since they may be changed by page scripts
  165. includes: _.includes,
  166. forEach: _.forEach,
  167. props: Object.getOwnPropertyNames(window),
  168. init: function(srcId, destId) {
  169. var comm = this;
  170. comm.sid = comm.vmid + srcId;
  171. comm.did = comm.vmid + destId;
  172. document.addEventListener(comm.sid, comm['handle' + srcId].bind(comm), false);
  173. comm.load = comm.checkLoad = function(){};
  174. },
  175. post: function(data) {
  176. var e = new CustomEvent(this.did, {detail: data});
  177. document.dispatchEvent(e);
  178. },
  179. handleR: function(e) {
  180. var obj = e.detail;
  181. var comm = this;
  182. var maps = {
  183. LoadScript: comm.loadScript.bind(comm),
  184. Command: function (data) {
  185. var func = comm.command[data];
  186. if(func) func();
  187. },
  188. GotRequestId: function (id) {
  189. comm.qrequests.shift().start(id);
  190. },
  191. HttpRequested: function (r) {
  192. var req = comm.requests[r.id];
  193. if (req) req.callback(r);
  194. },
  195. UpdateValues: function (data) {
  196. var values = comm.values;
  197. if (values && values[data.uri]) values[data.uri] = data.values;
  198. },
  199. // advanced inject
  200. Injected: function (id) {
  201. var obj = comm.ainject[id];
  202. var func = window['VM_' + id];
  203. delete window['VM_' + id];
  204. delete comm.ainject[id];
  205. if (obj && func) comm.runCode(obj[0], func, obj[1]);
  206. },
  207. };
  208. var func = maps[obj.cmd];
  209. if (func) func(obj.data);
  210. },
  211. runCode: function(name, func, wrapper) {
  212. try{
  213. func.call(wrapper.window, wrapper);
  214. }catch(e){
  215. var msg = 'Error running script: ' + name + '\n' + e;
  216. if(e.message) msg += '\n' + e.message;
  217. console.error(msg);
  218. }
  219. },
  220. initRequest: function() {
  221. // request functions
  222. function reqAbort(){
  223. comm.post({cmd: 'AbortRequest', data: this.id});
  224. }
  225. // request object functions
  226. function callback(req){
  227. var t = this;
  228. var cb = t.details['on' + req.type];
  229. if (cb) {
  230. if(req.data.response) {
  231. if(!t.data.length) {
  232. if(req.resType) { // blob or arraybuffer
  233. var m = req.data.response.match(/^data:(.*?);base64,(.*)$/);
  234. if (!m) req.data.response = null;
  235. else {
  236. var b = window.atob(m[2]);
  237. if(t.details.responseType == 'blob')
  238. t.data.push(new Blob([b], {type: m[1]}));
  239. else { // arraybuffer
  240. m = new Uint8Array(b.length);
  241. for(var i = 0; i < b.length; i ++) m[i] = b.charCodeAt(i);
  242. t.data.push(m.buffer);
  243. }
  244. }
  245. } else if(t.details.responseType == 'json') // json
  246. t.data.push(JSON.parse(req.data.response));
  247. else // text
  248. t.data.push(req.data.response);
  249. }
  250. req.data.response = t.data[0];
  251. }
  252. cb(req.data);
  253. }
  254. if (req.type == 'loadend') delete comm.requests[t.id];
  255. }
  256. function start(id) {
  257. var t = this;
  258. var data = {
  259. id: id,
  260. method: t.details.method,
  261. url: t.details.url,
  262. data: t.details.data,
  263. //async: !t.details.synchronous,
  264. user: t.details.user,
  265. password: t.details.password,
  266. headers: t.details.headers,
  267. overrideMimeType: t.details.overrideMimeType,
  268. };
  269. t.id = id;
  270. comm.requests[id] = t;
  271. if(comm.includes(['arraybuffer', 'blob'], t.details.responseType))
  272. data.responseType = 'blob';
  273. comm.post({cmd: 'HttpRequest', data: data});
  274. }
  275. function getFullUrl(url) {
  276. var a = document.createElement('a');
  277. a.setAttribute('href', url);
  278. return a.href;
  279. }
  280. var comm = this;
  281. comm.requests = {};
  282. comm.qrequests = [];
  283. comm.Request = function(details) {
  284. var t = {
  285. details: details,
  286. callback: callback,
  287. start: start,
  288. req: {
  289. abort: reqAbort,
  290. },
  291. data: [],
  292. };
  293. details.url = getFullUrl(details.url);
  294. comm.qrequests.push(t);
  295. comm.post({cmd:'GetRequestId'});
  296. return t.req;
  297. };
  298. },
  299. getWrapper: getWrapper,
  300. wrapGM: function(script, cache) {
  301. function getValues() {
  302. return comm.values[script.uri];
  303. }
  304. function propertyToString() {
  305. return '[Violentmonkey property]';
  306. }
  307. function addProperty(name, prop, obj) {
  308. if('value' in prop) prop.writable = false;
  309. prop.configurable = false;
  310. Object.defineProperty(obj, name, prop);
  311. if (typeof obj[name] == 'function')
  312. obj[name].toString = propertyToString;
  313. }
  314. function saveValues() {
  315. comm.post({
  316. cmd: 'SetValue',
  317. data: {
  318. uri: script.uri,
  319. values: getValues(),
  320. },
  321. });
  322. }
  323. // Add GM functions
  324. // Reference: http://wiki.greasespot.net/Greasemonkey_Manual:API
  325. var comm = this;
  326. var gm = {};
  327. var grant = script.meta.grant || [];
  328. var urls = {};
  329. if (!grant.length || grant.length == 1 && grant[0] == 'none') {
  330. // @grant none
  331. grant.pop();
  332. } else {
  333. gm['window'] = comm.getWrapper();
  334. }
  335. if(!comm.includes(grant, 'unsafeWindow')) grant.push('unsafeWindow');
  336. if(!comm.includes(grant, 'GM_info')) grant.push('GM_info');
  337. var resources = script.meta.resources || {};
  338. var gm_funcs = {
  339. unsafeWindow: {value: window},
  340. GM_info: {
  341. get: function () {
  342. var m = script.code.match(/\/\/\s+==UserScript==\s+([\s\S]*?)\/\/\s+==\/UserScript==\s/);
  343. var data = {
  344. description: script.meta.description || '',
  345. excludes: script.meta.exclude.concat(),
  346. includes: script.meta.include.concat(),
  347. matches: script.meta.match.concat(),
  348. name: script.meta.name || '',
  349. namespace: script.meta.namespace || '',
  350. resources: {},
  351. 'run-at': script.meta['run-at'] || '',
  352. unwrap: false,
  353. version: script.meta.version || '',
  354. };
  355. var obj = {};
  356. addProperty('scriptMetaStr', {value: m ? m[1] : ''}, obj);
  357. // whether update is allowed
  358. addProperty('scriptWillUpdate', {value: !!script.update}, obj);
  359. // Violentmonkey specific data
  360. addProperty('version', {value: comm.version}, obj);
  361. addProperty('scriptHandler', {value: 'Violentmonkey'}, obj);
  362. // script object
  363. addProperty('script', {value:{}}, obj);
  364. var i;
  365. for(i in data) {
  366. addProperty(i, {value: data[i]}, obj.script);
  367. }
  368. for(i in script.meta.resources)
  369. addProperty(i, {value: script.meta.resources[i]}, obj.script.resources);
  370. return obj;
  371. },
  372. },
  373. GM_deleteValue: {
  374. value: function (key) {
  375. var values = getValues();
  376. delete values[key];
  377. saveValues();
  378. },
  379. },
  380. GM_getValue: {
  381. value: function(key, val) {
  382. var values = getValues();
  383. var v = values[key];
  384. if (v) {
  385. var type = v[0];
  386. v = v.slice(1);
  387. switch(type) {
  388. case 'n':
  389. val = Number(v);
  390. break;
  391. case 'b':
  392. val = v == 'true';
  393. break;
  394. case 'o':
  395. try {
  396. val = JSON.parse(v);
  397. } catch(e) {
  398. console.warn(e);
  399. }
  400. break;
  401. default:
  402. val = v;
  403. }
  404. }
  405. return val;
  406. },
  407. },
  408. GM_listValues: {
  409. value: function () {
  410. return Object.getOwnPropertyNames(getValues());
  411. },
  412. },
  413. GM_setValue: {
  414. value: function (key, val) {
  415. var type = (typeof val)[0];
  416. switch(type) {
  417. case 'o':
  418. val = type + JSON.stringify(val);
  419. break;
  420. default:
  421. val = type + val;
  422. }
  423. var values = getValues();
  424. values[key] = val;
  425. saveValues();
  426. },
  427. },
  428. GM_getResourceText: {
  429. value: function (name) {
  430. for(var i in resources) if (name == i) {
  431. var text = cache[resources[i]];
  432. if (text) text = comm.utf8decode(window.atob(text));
  433. return text;
  434. }
  435. },
  436. },
  437. GM_getResourceURL: {
  438. value: function (name) {
  439. for(var i in resources) if (name == i) {
  440. i = resources[i];
  441. var url = urls[i];
  442. if(!url) {
  443. var cc = cache[i];
  444. if(cc) {
  445. cc = window.atob(cc);
  446. var b = new Uint8Array(cc.length);
  447. for(var j = 0; j < cc.length; j ++)
  448. b[j] = cc.charCodeAt(j);
  449. b = new Blob([b]);
  450. urls[i] = url = URL.createObjectURL(b);
  451. }
  452. }
  453. }
  454. return url;
  455. }
  456. },
  457. GM_addStyle: {
  458. value: function (css) {
  459. if (document.head) {
  460. var style = document.createElement('style');
  461. style.innerHTML = css;
  462. document.head.appendChild(style);
  463. return style;
  464. }
  465. },
  466. },
  467. GM_log: {
  468. /* eslint-disable no-console */
  469. value: function (data) {console.log(data);},
  470. /* eslint-enable no-console */
  471. },
  472. GM_openInTab: {
  473. value: function (url) {
  474. comm.post({cmd: 'NewTab', data: url});
  475. },
  476. },
  477. GM_registerMenuCommand: {
  478. value: function (cap, func, acc) {
  479. comm.command[cap] = func;
  480. comm.post({cmd: 'RegisterMenu', data: [cap, acc]});
  481. },
  482. },
  483. GM_xmlhttpRequest: {
  484. value: function (details) {
  485. if(!comm.Request) comm.initRequest();
  486. return comm.Request(details);
  487. },
  488. },
  489. };
  490. comm.forEach(grant, function (name) {
  491. var prop = gm_funcs[name];
  492. if(prop) addProperty(name, prop, gm);
  493. });
  494. return gm;
  495. },
  496. loadScript: function (data) {
  497. function buildCode(script) {
  498. var require = script.meta.require || [];
  499. var wrapper = comm.wrapGM(script, data.cache);
  500. var code = [];
  501. var part;
  502. comm.forEach(Object.getOwnPropertyNames(wrapper), function(name) {
  503. code.push(name + '=this["' + name + '"]=g["' + name + '"]');
  504. });
  505. if (code.length)
  506. code = ['var ' + code.join(',') + ';delete g;with(this)!function(){'];
  507. else
  508. code = [];
  509. for(var i = 0; i < require.length; i ++)
  510. if((part = data.require[require[i]])) code.push(part);
  511. // wrap code to make 'use strict' work
  512. code.push('!function(){' + script.code + '\n}.call(this)');
  513. code.push('}.call(this);');
  514. code = code.join('\n');
  515. var name = script.custom.name || script.meta.name || script.id;
  516. if (data.injectMode == 1) {
  517. // advanced injection
  518. var id = comm.getUniqId();
  519. comm.ainject[id] = [name, wrapper];
  520. comm.post({cmd: 'Inject', data: [id, code]});
  521. } else {
  522. // normal injection
  523. try {
  524. var func = new Function('g', code);
  525. } catch(e) {
  526. console.error('Syntax error in script: ' + name + '\n' + e.message);
  527. return;
  528. }
  529. comm.runCode(name, func, wrapper);
  530. }
  531. }
  532. function run(list) {
  533. while (list.length) buildCode(list.shift());
  534. }
  535. var comm = this;
  536. var start = [];
  537. var idle = [];
  538. var end = [];
  539. comm.command = {};
  540. comm.ainject = {};
  541. comm.version = data.version;
  542. comm.values = {};
  543. // reset load and checkLoad
  544. comm.load = function() {
  545. run(end);
  546. setTimeout(function() {
  547. run(idle);
  548. }, 0);
  549. };
  550. comm.checkLoad = function() {
  551. if (!comm.state && comm.includes(['interactive', 'complete'], document.readyState))
  552. comm.state = 1;
  553. if (comm.state) comm.load();
  554. };
  555. comm.forEach(data.scripts, function(script) {
  556. comm.values[script.uri] = data.values[script.uri] || {};
  557. var list;
  558. if(script && script.enabled) {
  559. switch (script.custom['run-at'] || script.meta['run-at']) {
  560. case 'document-start':
  561. list = start;
  562. break;
  563. case 'document-idle':
  564. list = idle;
  565. break;
  566. default:
  567. list = end;
  568. }
  569. list.push(script);
  570. }
  571. });
  572. run(start);
  573. comm.checkLoad();
  574. },
  575. };
  576. var menus = [];
  577. var ids = [];
  578. function injectScript(data) {
  579. // data: [id, code]
  580. var func = function(id, did, cb) {
  581. Object.defineProperty(window, 'VM_' + id, {
  582. value: cb,
  583. configurable: true,
  584. });
  585. var e = new CustomEvent(did, {detail: {cmd: 'Injected', data: id}});
  586. document.dispatchEvent(e);
  587. };
  588. inject('!' + func.toString() + '(' + JSON.stringify(data[0]) + ',' + JSON.stringify(comm.did) + ',function(g){' + data[1] + '})');
  589. }
  590. function newTab(url) {
  591. window.open(url);
  592. }
  593. function handleC(e) {
  594. var req = e.detail;
  595. if (!req) {
  596. console.error('[Violentmonkey] Invalid data! There might be unsupported data format.');
  597. return;
  598. }
  599. var maps = {
  600. SetValue: function(data) {
  601. _.sendMessage({cmd: 'SetValue', data: data});
  602. },
  603. RegisterMenu: function(data) {
  604. if (window.top === window) menus.push(data);
  605. getPopup();
  606. },
  607. GetRequestId: getRequestId,
  608. HttpRequest: httpRequest,
  609. AbortRequest: abortRequest,
  610. Inject: injectScript,
  611. NewTab: newTab,
  612. };
  613. var func = maps[req.cmd];
  614. if (func) func(req.data);
  615. }
  616. // Messages
  617. chrome.runtime.onMessage.addListener(function (req, src) {
  618. var maps = {
  619. Command: function (data) {
  620. comm.post({cmd: 'Command', data: data});
  621. },
  622. GetPopup: getPopup,
  623. GetBadge: getBadge,
  624. HttpRequested: httpRequested,
  625. UpdateValues: function (data) {
  626. comm.post({cmd: 'UpdateValues', data: data});
  627. },
  628. };
  629. var func = maps[req.cmd];
  630. if (func) func(req.data, src);
  631. });
  632. // Requests
  633. var requests = {};
  634. function getRequestId() {
  635. _.sendMessage({cmd: 'GetRequestId'}).then(function (id) {
  636. requests[id] = 1;
  637. comm.post({cmd: 'GotRequestId', data: id});
  638. });
  639. }
  640. function httpRequest(details) {
  641. _.sendMessage({cmd: 'HttpRequest', data: details});
  642. }
  643. function httpRequested(data) {
  644. if(requests[data.id]) {
  645. if(data.type == 'loadend') delete requests[data.id];
  646. comm.post({cmd: 'HttpRequested', data: data});
  647. }
  648. }
  649. function abortRequest(id) {
  650. _.sendMessage({cmd: 'AbortRequest', data: id});
  651. }
  652. function objEncode(obj) {
  653. var list = [];
  654. for(var i in obj) {
  655. if(!obj.hasOwnProperty(i)) continue;
  656. if(typeof obj[i] == 'function')
  657. list.push(i + ':' + obj[i].toString());
  658. else
  659. list.push(i + ':' + JSON.stringify(obj[i]));
  660. }
  661. return '{' + list.join(',') + '}';
  662. }
  663. function inject(code) {
  664. var script = document.createElement('script')
  665. var doc = document.body || document.documentElement;
  666. script.innerHTML = code;
  667. doc.appendChild(script);
  668. doc.removeChild(script);
  669. }
  670. function loadScript(data) {
  671. data.scripts.forEach(function(script) {
  672. ids.push(script.id);
  673. if (script.enabled) badge.number ++;
  674. });
  675. comm.post({cmd: 'LoadScript', data: data});
  676. badge.ready = true;
  677. getPopup();
  678. setBadge();
  679. }
  680. function initCommunicator() {
  681. var C = 'C';
  682. var R = 'R';
  683. inject(
  684. '!function(c,R,C){c.init(R,C);document.addEventListener("DOMContentLoaded",function(e){c.state=1;c.load();},false);c.checkLoad();}(' +
  685. objEncode(comm) + ',"' + R + '","' + C + '")'
  686. );
  687. comm.handleC = handleC;
  688. comm.init(C, R);
  689. _.sendMessage({cmd: 'GetInjected', data: location.href}).then(loadScript);
  690. }
  691. initCommunicator();
  692. }();