ソースを参照

feat: JSON format in the cookie block (#953)

Ahmad Kholid 2 年 前
コミット
01b13bbd07

+ 164 - 73
src/components/newtab/workflow/edit/EditCookie.vue

@@ -24,81 +24,110 @@
       >
         {{ t('workflow.blocks.cookie.types.getAll') }}
       </ui-checkbox>
-      <ui-input
-        :model-value="data.url"
-        class="mt-2 w-full"
-        type="url"
-        label="URL"
-        placeholder="https://example.com/"
-        @change="updateData({ url: $event })"
-      />
-      <ui-input
-        :model-value="data.name"
-        :label="`Name ${
-          data.type === 'get' && !data.getAll ? '' : '(optional)'
-        }`"
-        class="mt-2 w-full"
-        placeholder="site-cookie"
-        @change="updateData({ name: $event })"
-      />
-      <ui-input
-        v-if="data.type === 'set'"
-        :model-value="data.value"
-        label="Value (optional)"
-        class="mt-2 w-full"
-        placeholder="value"
-        @change="updateData({ value: $event })"
-      />
-      <ui-input
-        :model-value="data.path"
-        class="mt-2 w-full"
-        label="Path (optional)"
-        placeholder="/"
-        @change="updateData({ path: $event })"
-      />
-      <ui-input
-        v-if="isGetOrSet"
-        :model-value="data.domain"
-        class="mt-2 w-full"
-        label="Domain (optional)"
-        placeholder=".example.com"
-        @change="updateData({ domain: $event })"
-      />
-      <ui-input
-        v-if="data.type === 'set'"
-        :model-value="data.sameSite"
-        class="mt-2 w-full"
-        label="sameSite (optional)"
-        placeholder="lax"
-        @change="updateData({ sameSite: $event })"
-      />
-      <ui-input
-        v-if="data.type === 'set'"
-        :model-value="data.expirationDate"
-        class="mt-2 w-full"
-        label="expirationDate (seconds) (optional)"
-        placeholder="3600"
-        @change="updateData({ expirationDate: $event })"
-      />
-      <div
-        v-if="data.type === 'set' || (data.type === 'get' && data.getAll)"
-        class="mt-4"
+      <ui-checkbox
+        :model-value="data.useJson"
+        block
+        class="mt-1"
+        @change="updateData({ useJson: $event })"
       >
-        <ui-checkbox
-          v-if="data.type === 'set'"
-          :model-value="data.httpOnly"
-          class="mr-4"
-          @change="updateData({ httpOnly: $event })"
+        {{ t('workflow.blocks.cookie.useJson') }}
+      </ui-checkbox>
+      <template v-if="data.useJson">
+        <shared-codemirror
+          :model-value="data.jsonCode"
+          :extensions="codemirrorExts"
+          lang="json"
+          class="mt-4 cookie-editor"
+          @change="updateData({ jsonCode: $event })"
+        />
+        <a
+          :href="`https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/${
+            data.type === 'get' && data.getAll ? 'getAll' : data.type
+          }`"
+          rel="noopener"
+          class="underline mt-2 inline-block"
+          target="_blank"
         >
-          httpOnly
-        </ui-checkbox>
-        <ui-checkbox
-          :model-value="data.secure"
-          @change="updateData({ secure: $event })"
+          See all available properties
+        </a>
+      </template>
+      <template v-else>
+        <ui-input
+          :model-value="data.url"
+          class="mt-2 w-full"
+          type="url"
+          label="URL"
+          placeholder="https://example.com/"
+          @change="updateData({ url: $event })"
+        />
+        <ui-input
+          :model-value="data.name"
+          :label="`Name ${
+            data.type === 'get' && !data.getAll ? '' : '(optional)'
+          }`"
+          class="mt-2 w-full"
+          placeholder="site-cookie"
+          @change="updateData({ name: $event })"
+        />
+        <ui-input
+          v-if="data.type === 'set'"
+          :model-value="data.value"
+          label="Value (optional)"
+          class="mt-2 w-full"
+          placeholder="value"
+          @change="updateData({ value: $event })"
+        />
+        <ui-input
+          :model-value="data.path"
+          class="mt-2 w-full"
+          label="Path (optional)"
+          placeholder="/"
+          @change="updateData({ path: $event })"
+        />
+        <ui-input
+          v-if="isGetOrSet"
+          :model-value="data.domain"
+          class="mt-2 w-full"
+          label="Domain (optional)"
+          placeholder=".example.com"
+          @change="updateData({ domain: $event })"
+        />
+        <ui-input
+          v-if="data.type === 'set'"
+          :model-value="data.sameSite"
+          class="mt-2 w-full"
+          label="sameSite (optional)"
+          placeholder="lax"
+          @change="updateData({ sameSite: $event })"
+        />
+        <ui-input
+          v-if="data.type === 'set'"
+          :model-value="data.expirationDate"
+          class="mt-2 w-full"
+          label="expirationDate (seconds) (optional)"
+          placeholder="3600"
+          @change="updateData({ expirationDate: $event })"
+        />
+        <div
+          v-if="data.type === 'set' || (data.type === 'get' && data.getAll)"
+          class="mt-4"
         >
-          secure
-        </ui-checkbox>
-      </div>
+          <ui-checkbox
+            v-if="data.type === 'set'"
+            :model-value="data.httpOnly"
+            class="mr-4"
+            @change="updateData({ httpOnly: $event })"
+          >
+            httpOnly
+          </ui-checkbox>
+          <ui-checkbox
+            :model-value="data.secure"
+            @change="updateData({ secure: $event })"
+          >
+            secure
+          </ui-checkbox>
+        </div>
+      </template>
       <div v-if="data.type === 'get'" class="pt-4 border-t mt-4 cookie-data">
         <insert-workflow-data :data="data" variables @update="updateData" />
       </div>
@@ -114,11 +143,17 @@
   </div>
 </template>
 <script setup>
-import { computed } from 'vue';
+import { computed, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { autocompletion } from '@codemirror/autocomplete';
+import { syntaxTree } from '@codemirror/language';
 import { useHasPermissions } from '@/composable/hasPermissions';
 import InsertWorkflowData from './InsertWorkflowData.vue';
 
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
+
 const props = defineProps({
   data: {
     type: Object,
@@ -131,6 +166,16 @@ const { t } = useI18n();
 const permission = useHasPermissions(['cookies']);
 
 const types = ['get', 'set', 'remove'];
+const methodProps = {
+  name: { label: 'name', type: 'text' },
+  url: { label: 'url', type: 'text' },
+  path: { label: 'path', type: 'text' },
+  session: { label: 'session', type: 'text' },
+  secure: { label: 'secure', type: 'text' },
+  domain: { label: 'domain', type: 'text' },
+  sameSite: { label: 'sameSite', type: 'text' },
+  httpOnly: { label: 'httpOnly', type: 'text' },
+};
 
 const isGetOrSet = computed(
   () =>
@@ -138,12 +183,58 @@ const isGetOrSet = computed(
     props.data.type === 'set'
 );
 
+function cookieOptionsAutocomplete(context) {
+  const word = context.matchBefore(/\w*/);
+  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
+
+  if (
+    nodeBefore.name !== 'PropertyName' ||
+    (word.from === word.to && !context.explicit)
+  )
+    return null;
+
+  let options = [];
+
+  if (props.data.type === 'get') {
+    if (props.data.getAll) {
+      options = [
+        methodProps.domain,
+        methodProps.name,
+        methodProps.path,
+        methodProps.secure,
+        methodProps.url,
+      ];
+    } else {
+      options = [methodProps.name, methodProps.url];
+    }
+  } else if (props.data.type === 'set') {
+    options = Object.values(methodProps);
+  } else if (props.data.type === 'remove') {
+    options = [methodProps.name, methodProps.url];
+  }
+
+  return {
+    options,
+    from: word.from,
+  };
+}
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
+
+const codemirrorExts = [
+  autocompletion({
+    override: [cookieOptionsAutocomplete],
+  }),
+];
 </script>
 <style>
 .cookie-data .block-variable {
   margin-top: 0;
 }
+
+.cookie-editor .cm-tooltip-autocomplete {
+  margin-left: 0px !important;
+  margin-top: -5px !important;
+}
 </style>

+ 2 - 1
src/locales/en/blocks.json

@@ -112,7 +112,8 @@
           "set": "Set cookie",
           "remove": "Remove cookies",
           "getAll": "Get all cookies"
-        }
+        },
+        "useJson": "Use JSON format"
       },
       "note": {
         "name": "Note"

+ 11 - 0
src/newtab/utils/blocksValidation.js

@@ -272,6 +272,13 @@ export async function validateNotification() {
   return [];
 }
 
+export async function validateCookie() {
+  const hasPermission = await checkPermissions(['cookies']);
+  if (!hasPermission) return ["Don't have cookies permissions"];
+
+  return [];
+}
+
 export default {
   trigger: {
     ...defaultOptions,
@@ -386,4 +393,8 @@ export default {
     ...defaultOptions,
     func: validateInteractionBasic,
   },
+  cookie: {
+    ...defaultOptions,
+    func: validateCookie,
+  },
 };

+ 3 - 0
src/utils/shared.js

@@ -1278,11 +1278,14 @@ export const tasks = {
       'name',
       'url',
       'value',
+      'jsonCode',
     ],
     data: {
       disableBlock: false,
       description: '',
       type: 'get',
+      jsonCode: '{\n\n}',
+      useJson: false,
       getAll: false,
       domain: '',
       expirationDate: '',

+ 21 - 13
src/workflowEngine/blocksHandler/handlerCookie.js

@@ -1,4 +1,5 @@
 import browser from 'webextension-polyfill';
+import { parseJSON } from '@/utils/helper';
 
 function getValues(data, keys) {
   const values = {};
@@ -46,23 +47,30 @@ async function cookie({ data, id }) {
   let key = data.type;
   if (key === 'get' && data.getAll) key = 'getAll';
 
-  const values = getValues(data, keys[key]);
-  if (values.expirationDate) {
-    values.expirationDate = Date.now() / 1000 + +values.expirationDate;
-  }
-
   let result = null;
 
-  if (data.type === 'remove' && !data.name) {
-    const cookies = await browser.cookies.getAll({ url: data.url });
-    const removePromise = cookies.map(({ name }) =>
-      browser.cookies.remove({ name, url: data.url })
-    );
-    await Promise.allSettled(removePromise);
+  if (data.useJson) {
+    const obj = parseJSON(data.jsonCode, null);
+    if (!obj) throw new Error('Invalid JSON format');
 
-    result = cookies;
+    result = await browser.cookies[key](obj);
   } else {
-    result = await browser.cookies[key](values);
+    const values = getValues(data, keys[key]);
+    if (values.expirationDate) {
+      values.expirationDate = Date.now() / 1000 + +values.expirationDate;
+    }
+
+    if (data.type === 'remove' && !data.name) {
+      const cookies = await browser.cookies.getAll({ url: data.url });
+      const removePromise = cookies.map(({ name }) =>
+        browser.cookies.remove({ name, url: data.url })
+      );
+      await Promise.allSettled(removePromise);
+
+      result = cookies;
+    } else {
+      result = await browser.cookies[key](values);
+    }
   }
 
   if (data.type === 'get') {