Просмотр исходного кода

fix: modern browsers don't support ftp

+ refactor `ua` and move into bg
tophf 2 лет назад
Родитель
Сommit
60f1907c45

+ 4 - 1
src/background/utils/init.js

@@ -8,5 +8,8 @@ export const addOwnCommands = obj => {
 };
 
 export let resolveInit;
-export let init = new Promise(r => (resolveInit = r));
+export let init = new Promise(r => {
+  resolveInit = () => Promise.all(init.deps).then(r);
+});
+init.deps = [];
 init.then(() => (init = null));

+ 2 - 2
src/background/utils/notifications.js

@@ -1,6 +1,6 @@
 import { i18n, defaultImage, sendTabCmd, trueJoin } from '@/common';
-import ua from '@/common/ua';
 import { addPublicCommands } from './init';
+import { CHROME } from './ua';
 
 const openers = {};
 const removeNotification = id => browser.notifications.clear(id);
@@ -16,7 +16,7 @@ addPublicCommands({
       ...!IS_FIREFOX && {
         requireInteraction: !!onclick,
       },
-      ...ua.chrome >= 70 && {
+      ...CHROME >= 70 && {
         silent,
       }
     });

+ 1 - 1
src/background/utils/preinject.js

@@ -4,7 +4,6 @@ import {
 } from '@/common/consts';
 import initCache from '@/common/cache';
 import { forEachEntry, forEachValue, mapEntry, objectSet } from '@/common/object';
-import ua from '@/common/ua';
 import { CACHE_KEYS, getScriptsByURL, PROMISE, REQ_KEYS, VALUE_IDS } from './db';
 import { setBadge } from './icon';
 import { addOwnCommands, addPublicCommands } from './init';
@@ -18,6 +17,7 @@ import {
 import { clearStorageCache, onStorageChanged } from './storage-cache';
 import { getFrameDocId, getFrameDocIdAsObj, tabsOnRemoved } from './tabs';
 import { addValueOpener, clearValueOpener, reifyValueOpener } from './values';
+import { ua } from './ua';
 
 let isApplied;
 let injectInto;

+ 2 - 2
src/background/utils/requests-core.js

@@ -1,6 +1,6 @@
 import { buffer2string, getUniqId, isEmpty, noop } from '@/common';
 import { forEachEntry } from '@/common/object';
-import ua from '@/common/ua';
+import { CHROME } from './ua';
 
 let encoder;
 
@@ -137,6 +137,6 @@ function string2byteString(str) {
 
 // Chrome 74-91 needs an extraHeaders listener at tab load start, https://crbug.com/1074282
 // We're attaching a no-op in non-blocking mode so it's very lightweight and fast.
-if (ua.chrome >= 74 && ua.chrome <= 91) {
+if (CHROME >= 74 && CHROME <= 91) {
   browser.webRequest.onBeforeSendHeaders.addListener(noop, API_FILTER, EXTRA_HEADERS);
 }

+ 2 - 2
src/background/utils/requests.js

@@ -1,13 +1,13 @@
 import { blob2base64, getFullUrl, sendTabCmd, string2uint8array } from '@/common';
 import { CHARSET_UTF8, FORM_URLENCODED } from '@/common/consts';
 import { forEachEntry, forEachValue, objectPick } from '@/common/object';
-import ua from '@/common/ua';
 import cache from './cache';
 import { addPublicCommands, commands } from './init';
 import {
   FORBIDDEN_HEADER_RE, VM_VERIFY, requests, toggleHeaderInjector, verify,
 } from './requests-core';
 import { getFrameDocIdAsObj, getFrameDocIdFromSrc } from './tabs';
+import { FIREFOX } from './ua';
 
 addPublicCommands({
   /**
@@ -241,7 +241,7 @@ async function httpRequest(opts, events, src, cb) {
     const cookies = (await browser.cookies.getAll({
       url,
       storeId: req.storeId,
-      ...ua.firefox >= 59 && { firstPartyDomain: null },
+      ...FIREFOX >= 59 && { firstPartyDomain: null },
     })).filter(c => c.session || c.expirationDate > now); // FF reports expired cookies!
     if (cookies.length) {
       vmHeaders.push({

+ 3 - 3
src/background/utils/tab-redirector.js

@@ -1,9 +1,9 @@
 import { browserWindows, request, noop, i18n, getUniqId } from '@/common';
-import ua from '@/common/ua';
 import cache from './cache';
 import { addPublicCommands, commands } from './init';
 import { parseMeta, isUserScript } from './script';
 import { getTabUrl, tabsOnUpdated } from './tabs';
+import { FIREFOX } from './ua';
 
 const CONFIRM_URL_BASE = `${extensionRoot}confirm/index.html#`;
 
@@ -26,7 +26,7 @@ addPublicCommands({
       || cache.has(`autoclose:${tabId}`)
       || /^(chrome:\/\/(newtab|startpage)\/|about:(home|newtab))$/.test(from));
     /** @namespace VM.ConfirmCache */
-    cache.put(`confirm-${confirmKey}`, { incognito, url, from, tabId, ff: ua.firefox });
+    cache.put(`confirm-${confirmKey}`, { incognito, url, from, tabId, ff: FIREFOX });
     const confirmUrl = CONFIRM_URL_BASE + confirmKey;
     const { windowId } = canReplaceCurTab
       ? await browser.tabs.update(tabId, { url: confirmUrl })
@@ -89,7 +89,7 @@ browser.tabs.onCreated.addListener((tab) => {
   const { id, title, url } = tab;
   /* Determining if this tab can be auto-closed (replaced, actually).
      FF>=68 allows reading file: URL only in the tab's content script so the tab must stay open. */
-  if ((!url.startsWith('file:') || ua.firefox < 68)
+  if ((!url.startsWith('file:') || FIREFOX < 68)
       && /\.user\.js([?#]|$)/.test(getTabUrl(tab))) {
     cache.put(`autoclose:${id}`, true, 10e3);
   }

+ 13 - 7
src/background/utils/tabs.js

@@ -1,8 +1,8 @@
 import { browserWindows, getActiveTab, noop, sendTabCmd, getFullUrl } from '@/common';
-import ua from '@/common/ua';
 import { addOwnCommands, addPublicCommands, commands } from './init';
 import { getOption } from './options';
 import { testScript } from './tester';
+import { CHROME, FIREFOX } from './ua';
 
 const openers = {};
 const openerTabIdSupported = !IS_FIREFOX // supported in Chrome
@@ -103,7 +103,7 @@ addPublicCommands({
         && getOption('editorWindow')
         /* cookieStoreId in windows.create() is supported since FF64 https://bugzil.la/1393570
          * and a workaround is too convoluted to add it for such an ancient version */
-        && (!storeId || ua.firefox >= 64)) {
+        && (!storeId || FIREFOX >= 64)) {
       const wndOpts = {
         url,
         incognito: canOpenIncognito && incognito,
@@ -159,11 +159,17 @@ tabsOnRemoved.addListener((id) => {
   }
 });
 
-if (!IS_FIREFOX) {
-  chrome.extension.isAllowedFileSchemeAccess(ok => {
-    if (!ok) injectableRe = /^(ht|f)tps?:/;
-  });
-}
+(async () => {
+  // FF68+ can't fetch file:// from extension context but it runs content scripts in file:// tabs
+  const fileScheme = IS_FIREFOX
+    || await new Promise(r => chrome.extension.isAllowedFileSchemeAccess(r));
+  // Since users in FF can override UA we detect FF 90 via feature
+  if (IS_FIREFOX && [].at || CHROME >= 88) {
+    injectableRe = fileScheme ? /^(https?|file):/ : /^https?:/;
+  } else if (!fileScheme) {
+    injectableRe = /^(ht|f)tps?:/;
+  }
+})();
 
 export async function forEachTab(callback) {
   const tabs = await browser.tabs.query({});

+ 55 - 0
src/background/utils/ua.js

@@ -0,0 +1,55 @@
+import { addOwnCommands, init } from './init';
+
+const info = (() => {
+  const uad = navigator.userAgentData;
+  let brand, ver, full;
+  if (uad) {
+    full = uad.getHighEntropyValues(['uaFullVersion']);
+    [brand, ver] = uad.brands.map(({ brand: b, version }) => `${
+      /Not[^a-z]*A[^a-z]*Brand/i.test(b) ? '4' :
+        b === 'Chromium' ? '3' + b :
+          b === 'Google Chrome' ? '2' + b :
+            '1' + b
+    }\n${version}`).sort()[0]?.slice(1).split('\n') || [];
+  } else {
+    ver = navigator.userAgent.match(/\s(?:Chrom(?:e|ium)|Firefox)\/(\d+[.0-9]*)|$/i)[1];
+  }
+  return {
+    [IS_FIREFOX ? 'FF' : 'CH']: parseFloat(ver) || 1,
+    brand,
+    full,
+    ver,
+  };
+})();
+
+/** @type {VMScriptGMInfoPlatform} */
+export const ua = {};
+/** @type {number} This value can be trusted because the only way to spoof it in Chrome/ium
+ * is to manually open devtools for the background page in device emulation mode. */
+export const CHROME = info.CH;
+/** @type {number} DANGER! Until init is done the only sure thing about this value
+ * is whether it's truthy, because UA can be overridden by about:config */
+export let FIREFOX = info.FF || +IS_FIREFOX;
+
+addOwnCommands({
+  UA: () => ua,
+});
+
+init.deps.push(
+  Promise.all([
+    browser.runtime.getPlatformInfo(),
+    browser.runtime.getBrowserInfo?.(),
+    info.full, // Getting the real version in new Chrome that simplifies its UA as ###.0.0.0
+  ]).then(([
+    { os, arch },
+    { name, version } = {},
+    { uaFullVersion } = {},
+  ]) => {
+    ua.arch = arch;
+    ua.os = os;
+    ua.brand = ua.browserBrand = info.brand || '';
+    ua.name = ua.browserName = name?.toLowerCase() || 'chrome';
+    ua.version = ua.browserVersion = uaFullVersion || version || info.ver;
+    if (FIREFOX) FIREFOX = parseFloat(version);
+  })
+);

+ 1 - 0
src/common/safe-globals.js

@@ -37,3 +37,4 @@ export const TAB_RECYCLE = 'recycleBin';
 export const BROWSER_ACTION = 'browser_action';
 export const kDocumentId = 'documentId';
 export const kFrameId = 'frameId';
+export const INJECT = 'inject';

+ 0 - 56
src/common/ua.js

@@ -1,56 +0,0 @@
-// UA can be overridden by about:config in FF or devtools in Chrome
-// so we'll test for window.chrome.app which is only defined in Chrome
-// and for browser.runtime.getBrowserInfo in Firefox 51+
-
-/** @type {VMUserAgent} */
-const ua = {};
-const kUaFullVersion = 'uaFullVersion'; // for new Chrome which simplifies UA version as #.0.0.0
-const uaData = navigator.userAgentData;
-export default ua;
-
-// using non-enumerable properties that won't be sent to content scripts via GetInjected
-Object.defineProperties(ua, {
-  ...IS_FIREFOX ? {
-    firefox: {
-      value: matchNavUA(), // will be replaced with the real version number in ready()
-      writable: true,
-    },
-  } : {
-    chrome: {
-      value: uaData && parseFloat(uaData.brands[0]?.version) || matchNavUA(true),
-    },
-  },
-  ready: {
-    value: Promise.all([
-      browser.runtime.getPlatformInfo(),
-      browser.runtime.getBrowserInfo?.(),
-      uaData?.getHighEntropyValues([kUaFullVersion]),
-    ]).then(([
-      { os, arch },
-      { name, version } = {},
-      { brands, [kUaFullVersion]: fullVer } = {},
-    ]) => {
-      Object.assign(ua, {
-        arch,
-        os,
-        browserBrand: brands?.map(({ brand: b }) => (
-          /Not[^a-z]*A[^a-z]*Brand/i.test(b) ? '4' :
-            b === 'Chromium' ? '3' + b :
-              b === 'Google Chrome' ? '2' + b :
-                '1' + b
-        )).sort()[0]?.slice(1) || '',
-        browserName: name?.toLowerCase() || 'chrome',
-        browserVersion: fullVer || version || matchNavUA(true, true),
-      });
-      if (IS_FIREFOX) {
-        ua.firefox = parseFloat(version) || 0;
-      }
-    }),
-  },
-});
-
-function matchNavUA(asChrome, asString) {
-  const re = new RegExp(`\\s${asChrome ? 'Chrom(e|ium)' : 'Firefox'}/(\\d+[.0-9]*)|$`, 'i');
-  const ver = navigator.userAgent.match(re).pop();
-  return asString ? ver : parseFloat(ver);
-}

+ 1 - 2
src/confirm/views/app.vue

@@ -115,7 +115,6 @@ import { loadScriptIcon } from '@/common/load-script-icon';
 import { deepEqual, objectPick } from '@/common/object';
 import options from '@/common/options';
 import { route } from '@/common/router';
-import ua from '@/common/ua';
 
 const KEEP_INFO_DELAY = 5000;
 const RETRY_DELAY = 3000;
@@ -400,7 +399,7 @@ export default {
         this.installed = true;
         if (isOk ? this.isLocal && this.$refs.track.value : isBtnTrack) {
           this.message = i18n('trackEditsNote')
-            + (ua.firefox >= 68 ? ' ' + i18n('installOptionTrackTooltip') : '');
+            + (this.info.ff >= 68 ? ' ' + i18n('installOptionTrackTooltip') : '');
           this.trackLocalFile();
         } else if (btnId === '+edit') {
           location.href = extensionOptionsPage + ROUTE_SCRIPTS + '/' + update.props.id;

+ 4 - 3
src/options/views/tab-settings/vm-export.vue

@@ -37,7 +37,6 @@ import { getScriptName, sendCmdDirectly } from '@/common';
 import { formatDate, DATE_FMT } from '@/common/date';
 import { objectGet } from '@/common/object';
 import options from '@/common/options';
-import ua from '@/common/ua';
 import SettingCheck from '@/common/ui/setting-check';
 import SettingText from '@/common/ui/setting-text';
 import { downloadBlob } from '@/common/download';
@@ -49,6 +48,7 @@ import { store } from '../../utils';
  * - Firefox does not support multiline <select>
  */
 if (IS_FIREFOX) store.ffDownload = {};
+let ua;
 
 export default {
   components: {
@@ -69,6 +69,7 @@ export default {
     async handleExport() {
       try {
         this.exporting = true;
+        if (IS_FIREFOX && !ua) ua = await sendCmdDirectly('UA');
         download(await exportData(), this.getFileName());
       } finally {
         this.exporting = false;
@@ -86,8 +87,8 @@ function download(blob, fileName) {
    * v56 in Windows https://bugzil.la/1357486
    * v61 in MacOS https://bugzil.la/1385403
    * v63 in Linux https://bugzil.la/1357487 */
-  const FF = IS_FIREFOX;
-  // eslint-disable-next-line no-nested-ternary
+  // TODO: remove when strict_min_version >= 63
+  const FF = IS_FIREFOX && parseFloat(ua.version);
   if (FF && (ua.os === 'win' ? FF < 56 : ua.os === 'mac' ? FF < 61 : FF < 63)) {
     const reader = new FileReader();
     reader.onload = () => {

+ 0 - 9
src/types.d.ts

@@ -332,15 +332,6 @@ declare type VMStorageFetch = (
   check?: (...args) => void // throws on error
 ) => Promise<void>
 
-declare interface VMUserAgent extends VMScriptGMInfoPlatform {
-  /** Chrome/ium version number */
-  chrome: number | typeof NaN;
-  /** derived from UA string initially, a real number when `ready` */
-  firefox: number | typeof NaN;
-  /** resolves when `browser` API returns real versions */
-  ready: Promise<void>;
-}
-
 /** Augmented by handleCommandMessage in messages from the content script */
 declare interface VMMessageSender extends chrome.runtime.MessageSender {
   top?: VMTopRenderMode;

+ 6 - 0
test/mock/polyfill.js

@@ -16,6 +16,12 @@ global.browser = {
       icons: { 16: '' },
       options_ui: {},
     }),
+    getPlatformInfo: async () => ({}),
+  },
+  tabs: {
+    onRemoved: { addListener: () => {} },
+    onReplaced: { addListener: () => {} },
+    onUpdated: { addListener: () => {} },
   },
 };
 if (!window.Response) window.Response = { prototype: {} };