Ver Fonte

feat: backgroun context in javascript code block

Ahmad Kholid há 2 anos atrás
pai
commit
fc8a5ec689

+ 1 - 1
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -56,7 +56,7 @@
       shortcut['action:search'].readable
     })`"
     prepend-icon="riSearch2Line"
-    class="px-4 mt-4 mb-2"
+    class="px-4 mt-4 mb-2 w-full"
   />
   <div class="scroll bg-scroll px-4 flex-1 relative overflow-auto">
     <workflow-block-list

+ 31 - 15
src/components/newtab/workflow/edit/EditJavascriptCode.vue

@@ -16,6 +16,20 @@
       class="mb-2 w-full"
       @change="updateData({ timeout: +$event })"
     />
+    <ui-select
+      :model-value="data.context"
+      :label="t('workflow.blocks.javascript-code.context.name')"
+      class="mb-2 w-full"
+      @change="updateData({ context: $event })"
+    >
+      <option
+        v-for="item in ['website', 'background']"
+        :key="item"
+        :value="item"
+      >
+        {{ t(`workflow.blocks.javascript-code.context.items.${item}`) }}
+      </option>
+    </ui-select>
     <p class="text-sm ml-1 text-gray-600 dark:text-gray-200">
       {{ t('workflow.blocks.javascript-code.name') }}
     </p>
@@ -25,20 +39,22 @@
       @click="state.showCodeModal = true"
       v-text="data.code"
     />
-    <ui-checkbox
-      :model-value="data.everyNewTab"
-      class="mt-2"
-      @change="updateData({ everyNewTab: $event })"
-    >
-      {{ t('workflow.blocks.javascript-code.everyNewTab') }}
-    </ui-checkbox>
-    <ui-checkbox
-      :model-value="data.runBeforeLoad"
-      class="mt-2"
-      @change="updateData({ runBeforeLoad: $event })"
-    >
-      Run before page loaded
-    </ui-checkbox>
+    <template v-if="data.context !== 'background'">
+      <ui-checkbox
+        :model-value="data.everyNewTab"
+        class="mt-2"
+        @change="updateData({ everyNewTab: $event })"
+      >
+        {{ t('workflow.blocks.javascript-code.everyNewTab') }}
+      </ui-checkbox>
+      <ui-checkbox
+        :model-value="data.runBeforeLoad"
+        class="mt-2"
+        @change="updateData({ runBeforeLoad: $event })"
+      >
+        Run before page loaded
+      </ui-checkbox>
+    </template>
     <ui-modal v-model="state.showCodeModal" content-class="max-w-3xl">
       <template #header>
         <ui-tabs v-model="state.activeTab" class="border-none">
@@ -110,7 +126,7 @@
               class="flex-1 mr-4"
             />
             <ui-checkbox
-              v-if="!data.everyNewTab"
+              v-if="!data.everyNewTab || data.context !== 'website'"
               v-model="state.preloadScripts[index].removeAfterExec"
             >
               {{ t('workflow.blocks.javascript-code.removeAfterExec') }}

+ 7 - 0
src/locales/en/blocks.json

@@ -597,6 +597,13 @@
         "availabeFuncs": "Available functions:",
         "removeAfterExec": "Remove after block executed",
         "everyNewTab": "Execute every new tab",
+        "context": {
+          "name": "Execution context",
+          "items": {
+            "website": "Active tab",
+            "background": "Background"
+          }
+        },
         "modal": {
           "tabs": {
             "code": "JavaScript code",

+ 7 - 1
src/manifest.chrome.json

@@ -67,5 +67,11 @@
       ],
       "matches": ["<all_urls>"]
     }
-  ]
+  ],
+  "sandbox": {
+    "pages": ["/sandbox.html"]
+  },
+  "content_security_policy": {
+    "sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';"
+  }
 }

+ 1 - 0
src/newtab/index.html

@@ -7,5 +7,6 @@
 
   <body>
     <div id="app"></div>
+    <iframe src="/sandbox.html" id="sandbox" class="hidden"></iframe>
   </body>
 </html>

+ 91 - 60
src/newtab/utils/workflowEngine/blocksHandler/handlerJavascriptCode.js

@@ -1,5 +1,6 @@
 import { customAlphabet } from 'nanoid/non-secure';
 import browser from 'webextension-polyfill';
+import cloneDeep from 'lodash.clonedeep';
 import { automaRefDataStr, waitTabLoaded } from '../helper';
 
 const nanoid = customAlphabet('1234567890abcdef', 5);
@@ -25,68 +26,28 @@ function automaResetTimeout() {
 
   return str;
 }
+function executeInSandbox(data) {
+  return new Promise((resolve) => {
+    const messageId = nanoid();
 
-export async function javascriptCode({ outputs, data, ...block }, { refData }) {
-  const nextBlockId = this.getBlockConnections(block.id);
-
-  if (data.everyNewTab) {
-    const isScriptExist = this.preloadScripts.some(({ id }) => id === block.id);
-    if (!isScriptExist) this.preloadScripts.push({ ...block, data });
-    if (!this.activeTab.id) return { data: '', nextBlockId };
-  } else if (!this.activeTab.id) {
-    throw new Error('no-tab');
-  }
-
-  const payload = {
-    ...block,
-    data,
-    refData: { variables: {} },
-    frameSelector: this.frameSelector,
-  };
-  if (data.code.includes('automaRefData')) {
-    payload.refData = { ...refData, secrets: {} };
-  }
-
-  const preloadScriptsPromise = await Promise.allSettled(
-    data.preloadScripts.map(async (script) => {
-      const { protocol } = new URL(script.src);
-      const isValidUrl = /https?/.test(protocol);
-      if (!isValidUrl) return null;
-
-      const response = await fetch(script.src);
-      if (!response.ok) throw new Error(response.statusText);
-
-      const result = await response.text();
-
-      return {
-        script: result,
-        id: `automa-script-${nanoid()}`,
-        removeAfterExec: script.removeAfterExec,
-      };
-    })
-  );
-  const preloadScripts = preloadScriptsPromise.reduce((acc, item) => {
-    if (item.status === 'fulfilled') acc.push(item.value);
+    const iframeEl = document.querySelector('#sandbox');
+    iframeEl.contentWindow.postMessage({ id: messageId, ...data }, '*');
 
-    return acc;
-  }, []);
+    const messageListener = ({ data: messageData }) => {
+      if (messageData?.type !== 'sandbox' || messageData?.id !== messageId)
+        return;
 
-  const automaScript = data.everyNewTab
-    ? ''
-    : getAutomaScript(payload.refData, data.everyNewTab);
+      resolve(messageData.result);
+    };
 
-  await waitTabLoaded({
-    tabId: this.activeTab.id,
-    ms: this.settings?.tabLoadTimeout ?? 30000,
+    window.addEventListener('message', messageListener, { once: true });
   });
-
+}
+async function executeInWebpage(args, target) {
   const [{ result }] = await browser.scripting.executeScript({
+    args,
+    target,
     world: 'MAIN',
-    args: [payload, preloadScripts, automaScript],
-    target: {
-      tabId: this.activeTab.id,
-      frameIds: [this.activeTab.frameId || 0],
-    },
     func: ($blockData, $preloadScripts, $automaScript) => {
       return new Promise((resolve, reject) => {
         try {
@@ -155,7 +116,7 @@ export async function javascriptCode({ outputs, data, ...block }, { refData }) {
             let onResetTimeout;
 
             /* eslint-disable-next-line */
-            function cleanUp(detail) {
+            function cleanUp() {
               script.remove();
               preloadScriptsEl.forEach((item) => {
                 if (item.removeAfterExec) item.script.remove();
@@ -171,7 +132,10 @@ export async function javascriptCode({ outputs, data, ...block }, { refData }) {
                 '__automa-next-block__',
                 onNextBlock
               );
+            }
 
+            onNextBlock = ({ detail }) => {
+              cleanUp(detail || {});
               resolve({
                 columns: {
                   data: detail?.data,
@@ -179,10 +143,6 @@ export async function javascriptCode({ outputs, data, ...block }, { refData }) {
                 },
                 variables: detail?.refData?.variables,
               });
-            }
-
-            onNextBlock = ({ detail }) => {
-              cleanUp(detail || {});
             };
             onResetTimeout = () => {
               clearTimeout(timeout);
@@ -211,6 +171,77 @@ export async function javascriptCode({ outputs, data, ...block }, { refData }) {
     },
   });
 
+  return result;
+}
+
+export async function javascriptCode({ outputs, data, ...block }, { refData }) {
+  const nextBlockId = this.getBlockConnections(block.id);
+
+  if (data.everyNewTab) {
+    const isScriptExist = this.preloadScripts.some(({ id }) => id === block.id);
+    if (!isScriptExist) this.preloadScripts.push({ ...block, data });
+    if (!this.activeTab.id) return { data: '', nextBlockId };
+  } else if (!this.activeTab.id && data.context !== 'background') {
+    throw new Error('no-tab');
+  }
+
+  const payload = {
+    ...block,
+    data,
+    refData: { variables: {} },
+    frameSelector: this.frameSelector,
+  };
+  if (data.code.includes('automaRefData')) {
+    payload.refData = { ...refData, secrets: {} };
+  }
+
+  const preloadScriptsPromise = await Promise.allSettled(
+    data.preloadScripts.map(async (script) => {
+      const { protocol } = new URL(script.src);
+      const isValidUrl = /https?/.test(protocol);
+      if (!isValidUrl) return null;
+
+      const response = await fetch(script.src);
+      if (!response.ok) throw new Error(response.statusText);
+
+      const result = await response.text();
+
+      return {
+        script: result,
+        id: `automa-script-${nanoid()}`,
+        removeAfterExec: script.removeAfterExec,
+      };
+    })
+  );
+  const preloadScripts = preloadScriptsPromise.reduce((acc, item) => {
+    if (item.status === 'fulfilled') acc.push(item.value);
+
+    return acc;
+  }, []);
+
+  const automaScript =
+    data.everyNewTab || data.context === 'background'
+      ? ''
+      : getAutomaScript(payload.refData, data.everyNewTab);
+
+  if (data.context !== 'background') {
+    await waitTabLoaded({
+      tabId: this.activeTab.id,
+      ms: this.settings?.tabLoadTimeout ?? 30000,
+    });
+  }
+
+  const result = await (data.context === 'background'
+    ? executeInSandbox({
+        preloadScripts,
+        refData: payload.refData,
+        blockData: cloneDeep(payload.data),
+      })
+    : executeInWebpage([payload, preloadScripts, automaScript], {
+        tabId: this.activeTab.id,
+        frameIds: [this.activeTab.frameId || 0],
+      }));
+
   if (result) {
     if (result.columns.data?.$error) {
       throw new Error(result.columns.data.message);

+ 2 - 0
src/newtab/utils/workflowEngine/blocksHandler/handlerNewTab.js

@@ -109,6 +109,8 @@ async function newTab({ id, data }) {
     });
   }
 
+  await browser.windows.update(tab.windowId, { focused: true });
+
   return {
     data: data.url,
     nextBlockId: this.getBlockConnections(id),

+ 4 - 0
src/newtab/utils/workflowEngine/blocksHandler/handlerSwitchTab.js

@@ -92,6 +92,10 @@ export default async function ({ data, id }) {
     await Promise.allSettled(preloadScripts);
   }
 
+  if (activeTab) {
+    await browser.windows.update(tab.windowId, { focused: true });
+  }
+
   return {
     nextBlockId,
     data: tab.url,

+ 10 - 0
src/sandbox/index.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>Sandbox</title>
+</head>
+<body>
+</body>
+</html>

+ 106 - 0
src/sandbox/index.js

@@ -0,0 +1,106 @@
+import objectPath from 'object-path';
+
+window.$getNestedProperties = objectPath.get;
+
+function onMessage({ data }) {
+  if (!data.id) return;
+
+  let timeout;
+  const scriptId = `script${data.id}`;
+  const propertyName = `automa${data.id}`;
+
+  const isScriptExists = document.querySelector(`#${scriptId}`);
+  if (isScriptExists) {
+    window.top.postMessage(
+      {
+        id: data.id,
+        type: 'sandbox',
+        result: {
+          columns: {},
+          variables: {},
+        },
+      },
+      '*'
+    );
+
+    return;
+  }
+
+  const preloadScripts = data.preloadScripts.map((item) => {
+    const scriptEl = document.createElement('script');
+    scriptEl.textContent = item.script;
+
+    document.body.appendChild(scriptEl);
+
+    return scriptEl;
+  });
+
+  if (!data.blockData.code.includes('automaNextBlock')) {
+    data.blockData.code += `\n automaNextBlock()`;
+  }
+
+  const script = document.createElement('script');
+  script.id = scriptId;
+  script.textContent = `
+  	(() => {
+  		function automaRefData(keyword, path = '') {
+			  return window.$getNestedProperties(${propertyName}.refData, keyword + '.' + path);
+			}
+  		function automaSetVariable(name, value) {
+			  ${propertyName}.refData.variables[name] = value;
+			}
+  		function automaNextBlock(data = {}, insert = true) {
+  			${propertyName}.nextBlock({ data, insert });
+  		}
+  		function automaResetTimeout() {
+  			${propertyName}.resetTimeout();
+  		}
+
+  		try {
+  			${data.blockData.code}
+  		} catch (error) {
+  			console.error(error);
+  			automaNextBlock({ $error: true, message: error.message });
+  		}
+  	})();
+  `;
+
+  function cleanUp() {
+    script.remove();
+    preloadScripts.forEach((preloadScript) => {
+      preloadScript.remove();
+    });
+
+    delete window[propertyName];
+  }
+
+  window[propertyName] = {
+    refData: data.refData,
+    nextBlock: (result) => {
+      cleanUp();
+      window.top.postMessage(
+        {
+          id: data.id,
+          type: 'sandbox',
+          result: {
+            variables: data?.refData?.variables,
+            columns: {
+              data: result?.data,
+              insert: result?.insert,
+            },
+          },
+        },
+        '*'
+      );
+    },
+    resetTimeout: () => {
+      clearTimeout(timeout);
+      timeout = setTimeout(cleanUp, data.blockData.timeout);
+    },
+  };
+
+  timeout = setTimeout(cleanUp, data.blockData.timeout);
+  document.body.appendChild(script);
+}
+
+window.addEventListener('message', onMessage);

+ 1 - 0
src/utils/shared.js

@@ -531,6 +531,7 @@ export const tasks = {
       disableBlock: false,
       description: '',
       timeout: 20000,
+      context: 'website',
       code: 'console.log("Hello world!");\nautomaNextBlock()',
       preloadScripts: [],
       everyNewTab: false,

+ 7 - 0
webpack.config.js

@@ -40,6 +40,7 @@ if (fileSystem.existsSync(secretsPath)) {
 const options = {
   mode: process.env.NODE_ENV || 'development',
   entry: {
+    sandbox: path.join(__dirname, 'src', 'sandbox', 'index.js'),
     newtab: path.join(__dirname, 'src', 'newtab', 'index.js'),
     popup: path.join(__dirname, 'src', 'popup', 'index.js'),
     params: path.join(__dirname, 'src', 'params', 'index.js'),
@@ -193,6 +194,12 @@ const options = {
       chunks: ['newtab'],
       cache: false,
     }),
+    new HtmlWebpackPlugin({
+      template: path.join(__dirname, 'src', 'sandbox', 'index.html'),
+      filename: 'sandbox.html',
+      chunks: ['sandbox'],
+      cache: false,
+    }),
     new HtmlWebpackPlugin({
       template: path.join(__dirname, 'src', 'popup', 'index.html'),
       filename: 'popup.html',