handlerJavascriptCode.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import { customAlphabet } from 'nanoid/non-secure';
  2. import browser from 'webextension-polyfill';
  3. import cloneDeep from 'lodash.clonedeep';
  4. import { parseJSON, isObject } from '@/utils/helper';
  5. import {
  6. jsContentHandler,
  7. automaFetchClient,
  8. jsContentHandlerEval,
  9. } from '../utils/javascriptBlockUtil';
  10. import {
  11. waitTabLoaded,
  12. messageSandbox,
  13. automaRefDataStr,
  14. checkCSPAndInject,
  15. } from '../helper';
  16. const nanoid = customAlphabet('1234567890abcdef', 5);
  17. function getAutomaScript({ varName, refData, everyNewTab, isEval = false }) {
  18. let str = `
  19. const ${varName} = ${JSON.stringify(refData)};
  20. ${automaRefDataStr(varName)}
  21. function automaSetVariable(name, value) {
  22. const variables = ${varName}.variables;
  23. if (!variables) ${varName}.variables = {}
  24. ${varName}.variables[name] = value;
  25. }
  26. function automaNextBlock(data, insert = true) {
  27. if (${isEval}) {
  28. $automaResolve({
  29. columns: {
  30. data,
  31. insert,
  32. },
  33. variables: ${varName}.variables,
  34. });
  35. } else{
  36. document.body.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: { data, insert, refData: ${varName} } }));
  37. }
  38. }
  39. function automaResetTimeout() {
  40. if (${isEval}) {
  41. clearTimeout($automaTimeout);
  42. $automaTimeout = setTimeout(() => {
  43. resolve();
  44. }, $automaTimeoutMs);
  45. } else {
  46. document.body.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));
  47. }
  48. }
  49. function automaFetch(type, resource) {
  50. return (${automaFetchClient.toString()})('${varName}', { type, resource });
  51. }
  52. `;
  53. if (everyNewTab) str = automaRefDataStr(varName);
  54. return str;
  55. }
  56. async function executeInWebpage(args, target, worker) {
  57. if (!target.tabId) {
  58. throw new Error('no-tab');
  59. }
  60. if (worker.engine.isMV2) {
  61. args[0] = cloneDeep(args[0]);
  62. const result = await worker._sendMessageToTab({
  63. label: 'javascript-code',
  64. data: args,
  65. });
  66. return result;
  67. }
  68. const { debugMode } = worker.engine.workflow.settings;
  69. const cspResult = await checkCSPAndInject({ target, debugMode }, () => {
  70. const { 0: blockData, 1: preloadScripts, 3: varName } = args;
  71. const automaScript = getAutomaScript({
  72. varName,
  73. isEval: true,
  74. refData: blockData.refData,
  75. everyNewTab: blockData.data.everyNewTab,
  76. });
  77. const jsCode = jsContentHandlerEval({
  78. blockData,
  79. automaScript,
  80. preloadScripts,
  81. });
  82. return jsCode;
  83. });
  84. if (cspResult.isBlocked) return cspResult.value;
  85. const [{ result }] = await browser.scripting.executeScript({
  86. args,
  87. target,
  88. world: 'MAIN',
  89. func: jsContentHandler,
  90. });
  91. if (typeof result?.columns?.data === 'string') {
  92. result.columns.data = parseJSON(result.columns.data, {});
  93. }
  94. return result;
  95. }
  96. export async function javascriptCode({ outputs, data, ...block }, { refData }) {
  97. let nextBlockId = this.getBlockConnections(block.id);
  98. if (data.everyNewTab) {
  99. const isScriptExist = this.preloadScripts.some(({ id }) => id === block.id);
  100. if (!isScriptExist)
  101. this.preloadScripts.push({ id: block.id, data: cloneDeep(data) });
  102. if (!this.activeTab.id) return { data: '', nextBlockId };
  103. } else if (!this.activeTab.id && data.context !== 'background') {
  104. throw new Error('no-tab');
  105. }
  106. const payload = {
  107. ...block,
  108. data,
  109. refData: { variables: {} },
  110. frameSelector: this.frameSelector,
  111. };
  112. if (data.code.includes('automaRefData')) {
  113. const newRefData = {};
  114. Object.keys(refData).forEach((keyword) => {
  115. if (!data.code.includes(keyword)) return;
  116. newRefData[keyword] = refData[keyword];
  117. });
  118. payload.refData = { ...newRefData, secrets: {} };
  119. }
  120. const preloadScriptsPromise = await Promise.allSettled(
  121. data.preloadScripts.map(async (script) => {
  122. const { protocol } = new URL(script.src);
  123. const isValidUrl = /https?/.test(protocol);
  124. if (!isValidUrl) return null;
  125. const response = await fetch(script.src);
  126. if (!response.ok) throw new Error(response.statusText);
  127. const result = await response.text();
  128. return {
  129. script: result,
  130. id: `automa-script-${nanoid()}`,
  131. removeAfterExec: script.removeAfterExec,
  132. };
  133. })
  134. );
  135. const preloadScripts = preloadScriptsPromise.reduce((acc, item) => {
  136. if (item.status === 'fulfilled') acc.push(item.value);
  137. return acc;
  138. }, []);
  139. const instanceId = `automa${nanoid()}`;
  140. const automaScript =
  141. data.everyNewTab && (!data.context || data.context !== 'background')
  142. ? ''
  143. : getAutomaScript({
  144. varName: instanceId,
  145. refData: payload.refData,
  146. everyNewTab: data.everyNewTab,
  147. });
  148. if (data.context !== 'background') {
  149. await waitTabLoaded({
  150. tabId: this.activeTab.id,
  151. ms: this.settings?.tabLoadTimeout ?? 30000,
  152. });
  153. }
  154. const inSandbox =
  155. (this.engine.isMV2 || this.engine.isPopup) &&
  156. BROWSER_TYPE !== 'firefox' &&
  157. data.context === 'background';
  158. const result = await (inSandbox
  159. ? messageSandbox('javascriptBlock', {
  160. instanceId,
  161. preloadScripts,
  162. refData: payload.refData,
  163. blockData: cloneDeep(payload.data),
  164. })
  165. : executeInWebpage(
  166. [payload, preloadScripts, automaScript, instanceId],
  167. {
  168. tabId: this.activeTab.id,
  169. frameIds: [this.activeTab.frameId || 0],
  170. },
  171. this
  172. ));
  173. if (result) {
  174. if (result.columns.data?.$error) {
  175. throw new Error(result.columns.data.message);
  176. }
  177. if (result.variables) {
  178. await Promise.allSettled(
  179. Object.keys(result.variables).map(async (varName) => {
  180. await this.setVariable(varName, result.variables[varName]);
  181. })
  182. );
  183. }
  184. let insert = true;
  185. let replaceTable = false;
  186. if (isObject(result.columns.insert)) {
  187. const {
  188. insert: insertData,
  189. nextBlockId: inputNextBlockId,
  190. replaceTable: replaceTableParam,
  191. } = result.columns.insert;
  192. replaceTable = Boolean(replaceTableParam);
  193. insert = typeof insertData === 'boolean' ? insertData : true;
  194. if (inputNextBlockId) {
  195. let customNextBlockId = this.getBlockConnections(inputNextBlockId);
  196. const nextBlock = this.engine.blocks[inputNextBlockId];
  197. if (!customNextBlockId && nextBlock) {
  198. customNextBlockId = [
  199. {
  200. id: inputNextBlockId,
  201. blockId: inputNextBlockId,
  202. connections: new Map([]),
  203. },
  204. ];
  205. }
  206. if (!customNextBlockId)
  207. throw new Error(`Can't find block with "${inputNextBlockId}" id`);
  208. nextBlockId = customNextBlockId;
  209. }
  210. } else {
  211. insert = result.columns.insert;
  212. }
  213. const columnData = result.columns.data;
  214. if (insert && columnData) {
  215. const columnDataObj =
  216. typeof columnData === 'string'
  217. ? parseJSON(columnData, null)
  218. : columnData;
  219. if (columnDataObj) {
  220. const params = Array.isArray(columnDataObj)
  221. ? columnDataObj
  222. : [columnDataObj];
  223. if (replaceTable) {
  224. this.engine.referenceData.table = [];
  225. Object.keys(this.engine.columns).forEach((key) => {
  226. this.engine.columns[key].index = 0;
  227. });
  228. }
  229. this.addDataToColumn(params);
  230. }
  231. }
  232. }
  233. return {
  234. nextBlockId,
  235. data: result?.columns.data || {},
  236. };
  237. }
  238. export default javascriptCode;