Skip to content

Commit cdfb48f

Browse files
committed
更完整的permission检查,更好的userScript权限提示
1 parent 524a47c commit cdfb48f

6 files changed

Lines changed: 147 additions & 81 deletions

File tree

src/app/service/service_worker/runtime.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
obtainBlackList,
2424
sourceMapTo,
2525
} from "@App/pkg/utils/utils";
26+
import { BrowserType, getBrowserInstalledVersion, getBrowserType, isPermissionOk } from "@App/pkg/utils/utils";
2627
import { cacheInstance } from "@App/app/cache";
2728
import { UrlMatch } from "@App/pkg/utils/match";
2829
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
@@ -162,22 +163,8 @@ export class RuntimeService {
162163
}
163164
}
164165

165-
showNoDeveloperModeWarning() {
166-
// 判断是否首次
167-
this.localStorageDAO.get("firstShowDeveloperMode").then((res) => {
168-
if (!res) {
169-
this.localStorageDAO.save({
170-
key: "firstShowDeveloperMode",
171-
value: true,
172-
});
173-
// 打开页面
174-
initLocalesPromise.then(() => {
175-
chrome.tabs.create({
176-
url: `${DocumentationSite}${localePath}/docs/use/open-dev/`,
177-
});
178-
});
179-
}
180-
});
166+
async showUserscriptActivationGuide() {
167+
const storageKey = "firstShowDeveloperMode";
181168
chrome.action.setBadgeBackgroundColor({
182169
color: "#ff8c00",
183170
});
@@ -206,6 +193,35 @@ export class RuntimeService {
206193
});
207194
}
208195
});
196+
197+
const currentInstalledBrowser = getBrowserInstalledVersion();
198+
const lastInstalledBrowser = (await this.localStorageDAO.get(storageKey))?.value as string | boolean | undefined;
199+
// 判断是否安装后的首次,或是浏览器升级后的首次
200+
if (currentInstalledBrowser === lastInstalledBrowser) return; // 非首次则不弹出页面
201+
202+
const savePromise = this.localStorageDAO.save({
203+
key: storageKey,
204+
value: currentInstalledBrowser,
205+
});
206+
await Promise.allSettled([initLocalesPromise, this.initReady, savePromise]); // 等一下语言加载和 isUserScriptsAvailable 检查之类的
207+
208+
const userscript_enabled: boolean = this.isUserScriptsAvailable;
209+
const permission = await isPermissionOk("userScripts");
210+
const browserType = getBrowserType();
211+
const guard =
212+
browserType.chrome & BrowserType.guardedByDeveloperMode
213+
? "developerMode"
214+
: browserType.chrome & BrowserType.guardedByAllowScript
215+
? "allowScript"
216+
: "none";
217+
218+
// 打开页面
219+
const path = `${DocumentationSite}${localePath}/docs/use/open-dev/`;
220+
let search = `?userscript_enabled=${userscript_enabled}&userscript_permission=${permission}&userscript_guard=${guard}`;
221+
if (browserType.chrome & BrowserType.Edge) search += "&browser=edge";
222+
else if (browserType.chrome & BrowserType.Chrome) search += "&browser=chrome";
223+
const hash = `${guard === "developerMode" ? "#enable-developer-mode" : guard === "allowScript" ? "#allow-user-scripts" : ""}`;
224+
chrome.tabs.create({ url: `${path}${search}${hash}` });
209225
}
210226

211227
async getInjectJsCode() {
@@ -570,7 +586,7 @@ export class RuntimeService {
570586
// 检查是否开启了开发者模式
571587
if (!this.isUserScriptsAvailable) {
572588
// 未开启加上警告引导
573-
this.showNoDeveloperModeWarning();
589+
this.showUserscriptActivationGuide();
574590
let cid: ReturnType<typeof setInterval> | number;
575591
cid = setInterval(async () => {
576592
if (!this.isUserScriptsAvailable) {

src/pages/components/PopupWarnings/index.tsx

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Alert, Button } from "@arco-design/web-react";
22
import { useEffect, useMemo, useState } from "react";
33
import { useTranslation } from "react-i18next";
4-
import { checkUserScriptsAvailable, getBrowserType, BrowserType } from "@App/pkg/utils/utils";
4+
import { checkUserScriptsAvailable, getBrowserType, BrowserType, isPermissionOk } from "@App/pkg/utils/utils";
55
import edgeMobileQrCode from "@App/assets/images/edge_mobile_qrcode.png";
66

77
interface PopupWarningsProps {
@@ -39,16 +39,28 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) {
3939

4040
const browser = browserType.chrome & BrowserType.Edge ? "edge" : "chrome";
4141

42-
const warningMessageHTML = browserType.firefox
43-
? t("develop_mode_guide", { browser: "firefox" })
44-
: browserType.chrome
45-
? browserType.chrome & BrowserType.chromeA
42+
let warningMessageHTML;
43+
44+
if (browserType.firefox) {
45+
// firefox
46+
warningMessageHTML = t("develop_mode_guide", { browser: "firefox" });
47+
} else if (browserType.chrome) {
48+
// chrome
49+
warningMessageHTML =
50+
browserType.chrome & BrowserType.noUserScriptsAPI
4651
? t("lower_version_browser_guide")
47-
: (browserType.chrome & BrowserType.chromeC && browserType.chrome & BrowserType.Chrome) ||
48-
browserType.chrome & BrowserType.edgeA
49-
? t("allow_user_script_guide", { browser }) // Edge 144+ 后使用`允许用户脚本`控制
50-
: t("develop_mode_guide", { browser }) // Edge浏览器目前没有允许用户脚本选项,开启开发者模式即可
51-
: "UNKNOWN";
52+
: // 120+
53+
browserType.chrome & BrowserType.guardedByDeveloperMode
54+
? t("develop_mode_guide", { browser }) // Edge浏览器目前没有允许用户脚本选项,开启开发者模式即可
55+
: // Edge 144+ / Chrome 138+
56+
browserType.chrome & BrowserType.guardedByAllowScript
57+
? t("allow_user_script_guide", { browser }) // Edge 144+ 后使用`允许用户脚本`控制
58+
: // 用于日后扩充更新版本
59+
"UNKNOWN";
60+
} else {
61+
// other browsers
62+
warningMessageHTML = "UNKNOWN";
63+
}
5264

5365
return warningMessageHTML;
5466
}, [isUserScriptsAvailableState, t]);
@@ -62,28 +74,14 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) {
6274

6375
// 权限要求详见:https://github.com/mdn/webextensions-examples/blob/main/userScripts-mv3/options.mjs
6476
useEffect(() => {
65-
//@ts-ignore
66-
if (chrome.permissions?.contains && chrome.permissions?.request) {
67-
chrome.permissions.contains(
68-
{
69-
permissions: ["userScripts"],
70-
},
71-
function (permissionOK) {
72-
const lastError = chrome.runtime.lastError;
73-
if (lastError) {
74-
console.error("chrome.runtime.lastError in chrome.permissions.contains:", lastError.message);
75-
// runtime 错误的话不显示按钮
76-
return;
77-
}
78-
if (permissionOK === false) {
79-
// 假设browser能支持 `chrome.permissions.contains` 及在 callback返回一个false值的话,
80-
// chrome.permissions.request 应该可以执行
81-
// 因此在这裡显示按钮
82-
setShowRequestButton(true);
83-
}
84-
}
85-
);
86-
}
77+
isPermissionOk("userScripts").then((permissionOK) => {
78+
if (permissionOK === false) {
79+
// 假设browser能支持 `chrome.permissions.contains` 及在 callback返回一个false值的话,
80+
// chrome.permissions.request 应该可以执行
81+
// 因此在这里显示按钮
82+
setShowRequestButton(true);
83+
}
84+
});
8785
}, []);
8886

8987
const handleRequestPermission = () => {
@@ -103,8 +101,8 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) {
103101
if (granted) {
104102
setPermissionReqResult("✅");
105103
// UserScripts API相关的初始化:
106-
// userScripts.LISTEN_CONNECTIONS 進行 Server 通讯初始化
107-
// onUserScriptAPIGrantAdded 進行 腳本注冊
104+
// userScripts.LISTEN_CONNECTIONS 进行 Server 通讯初始化
105+
// onUserScriptAPIGrantAdded 进行 脚本注册
108106
updateIsUserScriptsAvailableState();
109107
} else {
110108
setPermissionReqResult("❎");

src/pages/components/RuntimeSetting/index.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import FileSystemParams from "../FileSystemParams";
55
import { systemConfig } from "@App/pages/store/global";
66
import type { FileSystemType } from "@Packages/filesystem/factory";
77
import FileSystemFactory from "@Packages/filesystem/factory";
8+
import { isPermissionOk } from "@App/pkg/utils/utils";
89

910
const CollapseItem = Collapse.Item;
1011

@@ -24,11 +25,8 @@ const RuntimeSetting: React.FC = () => {
2425
setFilesystemType(res.filesystem);
2526
setFilesystemParam(res.params[res.filesystem] || {});
2627
});
27-
chrome.permissions.contains({ permissions: ["background"] }, (result) => {
28-
if (chrome.runtime.lastError) {
29-
console.error(chrome.runtime.lastError);
30-
return;
31-
}
28+
isPermissionOk("background").then((result) => {
29+
if (result === null) return; // 无法要求 background permission
3230
setEnableBackgroundState(result);
3331
});
3432
}, []);

src/pages/install/App.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer";
3131
import { useSearchParams } from "react-router-dom";
3232
import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key";
3333
import { cacheInstance } from "@App/app/cache";
34-
import { formatBytes, prettyUrl } from "@App/pkg/utils/utils";
34+
import { formatBytes, isPermissionOk, prettyUrl } from "@App/pkg/utils/utils";
3535
import { ScriptIcons } from "../options/routes/utils";
3636
import { bytesDecode, detectEncoding } from "@App/pkg/utils/encoding";
3737

@@ -462,9 +462,8 @@ function App() {
462462

463463
if (hasShown !== "true") {
464464
// 检查是否已经有后台权限
465-
if (!(await chrome.permissions.contains({ permissions: ["background"] }))) {
466-
return true;
467-
}
465+
const permission = await isPermissionOk("background");
466+
if (permission === false) return true; // optional permission "background" 需要显示后台运行提示
468467
}
469468
return false;
470469
};

src/pkg/utils/utils.ts

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export const deferred = <T = void>(): Deferred<T> => {
4040
};
4141

4242
export function isFirefox() {
43-
//@ts-ignore
44-
return typeof mozInnerScreenX !== "undefined";
43+
// @ts-ignore. For both Page & Worker
44+
return typeof mozInnerScreenX !== "undefined" || typeof navigator.mozGetUserMedia === "function";
4545
}
4646

4747
export function InfoNotification(title: string, msg: string) {
@@ -256,17 +256,25 @@ export function getBrowserVersion(): number {
256256

257257
// 判断是否为Edge浏览器
258258
export function isEdge(): boolean {
259-
return navigator.userAgent.includes("Edg/");
259+
return (
260+
// @ts-ignore; For Extension (Page/Worker), we can check UserSubscriptionState (hidden feature in Edge)
261+
typeof chrome.runtime.UserSubscriptionState === "object" ||
262+
// Fallback to userAgent check
263+
navigator.userAgent.includes("Edg/")
264+
);
260265
}
261266

262-
export enum BrowserType {
263-
Edge = 2,
264-
Chrome = 1,
265-
chromeA = 4, // ~ 120
266-
chromeB = 8, // 121 ~ 137
267-
chromeC = 16, // 138 ~
268-
edgeA = 32, // Edge 144~
269-
}
267+
export const BrowserType = {
268+
Edge: 2,
269+
Chrome: 1,
270+
noUserScriptsAPI: 64,
271+
guardedByDeveloperMode: 128,
272+
guardedByAllowScript: 256,
273+
Mouse: 1, // Desktop, Laptop. Tablet ??
274+
Touch: 2, // Touchscreen Laptop, Mobile, Tablet
275+
} as const;
276+
277+
export type BrowserType = ValueOf<typeof BrowserType>;
270278

271279
export function getBrowserType() {
272280
const o = {
@@ -275,35 +283,77 @@ export function getBrowserType() {
275283
chrome: 0, // Chrome, Chromium, Brave, Edge
276284
unknown: 0,
277285
chromeVersion: 0,
286+
device: 0,
278287
};
279288
if (isFirefox()) {
289+
// Firefox, Zen
280290
o.firefox = 1;
281291
} else {
282292
//@ts-ignore
283293
const isWebkitBased = typeof webkitIndexedDB === "object";
284294
if (isWebkitBased) {
295+
// Safari, Orion
285296
o.webkit = 1;
286297
} else {
287-
//@ts-ignore
288-
const isChromeBased = typeof webkitRequestAnimationFrame === "function";
298+
const isChromeBased =
299+
typeof requestAnimationFrame === "function"
300+
? // @ts-ignore. For Page only
301+
typeof webkitRequestAnimationFrame === "function"
302+
: // @ts-ignore. Available in Worker (Chrome 74+ Edge 79+)
303+
typeof BackgroundFetchRecord === "function";
289304
if (isChromeBased) {
290305
const isEdgeBrowser = isEdge();
291306
const chromeVersion = getBrowserVersion();
292307
o.chrome |= isEdgeBrowser ? BrowserType.Edge : BrowserType.Chrome;
293-
o.chrome |= chromeVersion < 120 ? BrowserType.chromeA : 0; // Chrome 120 以下
294-
o.chrome |= chromeVersion < 138 ? BrowserType.chromeB : BrowserType.chromeC; // Chrome 121 ~ 137 / 138 以上
295-
if (isEdgeBrowser) {
296-
o.chrome |= chromeVersion >= 144 ? BrowserType.edgeA : 0; // Edge 144 以上
308+
// 由小至大
309+
if (chromeVersion < 120) {
310+
o.chrome |= BrowserType.noUserScriptsAPI;
311+
} else {
312+
// 120+
313+
if (isEdgeBrowser ? chromeVersion < 144 : chromeVersion < 138) {
314+
o.chrome |= BrowserType.guardedByDeveloperMode;
315+
} else {
316+
// Edge 144+ / Chrome 138+
317+
o.chrome |= BrowserType.guardedByAllowScript;
318+
// 如日后再变化,在这里再加条件式
319+
}
297320
}
298321
o.chromeVersion = chromeVersion;
299322
} else {
300323
o.unknown = 1;
301324
}
302325
}
303326
}
327+
// BrowserType.Mouse 未能在 Worker 使用
328+
o.device |= typeof matchMedia === "function" && !matchMedia("(hover: none)").matches ? BrowserType.Mouse : 0;
329+
o.device |= navigator.maxTouchPoints > 0 ? BrowserType.Touch : 0;
304330
return o;
305331
}
306332

333+
export const isPermissionOk = async (
334+
manifestPermission: chrome.runtime.ManifestOptionalPermissions & chrome.runtime.ManifestPermissions
335+
): Promise<boolean | null> => {
336+
// 兼容 Firefox - 避免因为检查 permission 时,该permission不存在于 optional permission 而报错
337+
const manifest = chrome.runtime.getManifest();
338+
if (manifest.optional_permissions?.includes(manifestPermission)) {
339+
try {
340+
return await chrome.permissions.contains({ permissions: [manifestPermission] });
341+
} catch {
342+
// ignored
343+
}
344+
} else if (manifest.permissions?.includes(manifestPermission)) {
345+
// mainfest 而列明有该permission, 不用检查
346+
return true;
347+
}
348+
return null;
349+
};
350+
351+
export const getBrowserInstalledVersion = () => {
352+
// unique for each browser update.
353+
// Usage: Detect whether the browser is upgraded.
354+
return btoa([...navigator.userAgent.matchAll(/[\d._]+/g)].map((e) => e[0]).join(";"));
355+
};
356+
307357
export const makeBlobURL = <T extends { blob: Blob; persistence: boolean }>(
308358
params: T,
309359
fallbackFn?: (params: T) => string | Promise<string>
@@ -513,7 +563,7 @@ export const normalizeResponseHeaders = (headersString: string) => {
513563
// 遵循 ISO 8601, 一月四日为Week 1,星期一为新一周
514564
// 能应对每年开始和结束(不会因为踏入新一年而重新计算)
515565
// 见 https://wikipedia.org/wiki/ISO_week_date
516-
// 中文說明 https://juejin.cn/post/6921245139855736846
566+
// 中文说明 https://juejin.cn/post/6921245139855736846
517567
export const getISOWeek = (date: Date): number => {
518568
// 使用传入日期的年月日创建 UTC 日期对象,忽略本地时间部分,避免时区影响
519569
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));

tests/pages/popup/App.test.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,15 @@ vi.mock("@App/pkg/utils/utils", () => ({
7272
title: "Example",
7373
}),
7474
BrowserType: {
75-
Chrome: "chrome",
76-
Firefox: "firefox",
77-
Edge: "edge",
75+
Edge: 2,
76+
Chrome: 1,
77+
noUserScriptsAPI: 64,
78+
guardedByDeveloperMode: 128,
79+
guardedByAllowScript: 256,
80+
Mouse: 1,
81+
Touch: 2,
7882
},
83+
isPermissionOk: vi.fn(async (_s: string) => true),
7984
}));
8085

8186
vi.mock("@App/locales/locales", () => ({

0 commit comments

Comments
 (0)