Skip to content

Commit 2c07d6b

Browse files
authored
⚡️ 更完整的permission检查,更好的userScript权限提示 (#1251)
1 parent 40c99fb commit 2c07d6b

File tree

6 files changed

+147
-81
lines changed

6 files changed

+147
-81
lines changed

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";
@@ -173,22 +174,8 @@ export class RuntimeService {
173174
}
174175
}
175176

176-
showNoDeveloperModeWarning() {
177-
// 判断是否首次
178-
this.localStorageDAO.get("firstShowDeveloperMode").then((res) => {
179-
if (!res) {
180-
this.localStorageDAO.save({
181-
key: "firstShowDeveloperMode",
182-
value: true,
183-
});
184-
// 打开页面
185-
initLocalesPromise.then(() => {
186-
chrome.tabs.create({
187-
url: `${DocumentationSite}${localePath}/docs/use/open-dev/`,
188-
});
189-
});
190-
}
191-
});
177+
async showUserscriptActivationGuide() {
178+
const storageKey = "firstShowDeveloperMode";
192179
chrome.action.setBadgeBackgroundColor({
193180
color: "#ff8c00",
194181
});
@@ -217,6 +204,35 @@ export class RuntimeService {
217204
});
218205
}
219206
});
207+
208+
const currentInstalledBrowser = getBrowserInstalledVersion();
209+
const lastInstalledBrowser = (await this.localStorageDAO.get(storageKey))?.value as string | boolean | undefined;
210+
// 判断是否安装后的首次,或是浏览器升级后的首次
211+
if (currentInstalledBrowser === lastInstalledBrowser) return; // 非首次则不弹出页面
212+
213+
const savePromise = this.localStorageDAO.save({
214+
key: storageKey,
215+
value: currentInstalledBrowser,
216+
});
217+
await Promise.allSettled([initLocalesPromise, this.initReady, savePromise]); // 等一下语言加载和 isUserScriptsAvailable 检查之类的
218+
219+
const userscript_enabled: boolean = this.isUserScriptsAvailable;
220+
const permission = await isPermissionOk("userScripts");
221+
const browserType = getBrowserType();
222+
const guard =
223+
browserType.chrome & BrowserType.guardedByDeveloperMode
224+
? "developerMode"
225+
: browserType.chrome & BrowserType.guardedByAllowScript
226+
? "allowScript"
227+
: "none";
228+
229+
// 打开页面
230+
const path = `${DocumentationSite}${localePath}/docs/use/open-dev/`;
231+
let search = `?userscript_enabled=${userscript_enabled}&userscript_permission=${permission}&userscript_guard=${guard}`;
232+
if (browserType.chrome & BrowserType.Edge) search += "&browser=edge";
233+
else if (browserType.chrome & BrowserType.Chrome) search += "&browser=chrome";
234+
const hash = `${guard === "developerMode" ? "#enable-developer-mode" : guard === "allowScript" ? "#allow-user-scripts" : ""}`;
235+
chrome.tabs.create({ url: `${path}${search}${hash}` });
220236
}
221237

222238
async getInjectJsCode() {
@@ -581,7 +597,7 @@ export class RuntimeService {
581597
// 检查是否开启了开发者模式
582598
if (!this.isUserScriptsAvailable) {
583599
// 未开启加上警告引导
584-
this.showNoDeveloperModeWarning();
600+
this.showUserscriptActivationGuide();
585601
let cid: ReturnType<typeof setInterval> | number;
586602
cid = setInterval(async () => {
587603
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 } from "@App/pkg/utils/utils";
34+
import { formatBytes, isPermissionOk } from "@App/pkg/utils/utils";
3535
import { ScriptIcons } from "../options/routes/utils";
3636
import { bytesDecode, detectEncoding } from "@App/pkg/utils/encoding";
3737
import { prettyUrl } from "@App/pkg/utils/url-utils";
@@ -463,9 +463,8 @@ function App() {
463463

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

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 valueType(val: unknown) {
@@ -251,17 +251,25 @@ export function getBrowserVersion(): number {
251251

252252
// 判断是否为Edge浏览器
253253
export function isEdge(): boolean {
254-
return navigator.userAgent.includes("Edg/");
254+
return (
255+
// @ts-ignore; For Extension (Page/Worker), we can check UserSubscriptionState (hidden feature in Edge)
256+
typeof chrome.runtime.UserSubscriptionState === "object" ||
257+
// Fallback to userAgent check
258+
navigator.userAgent.includes("Edg/")
259+
);
255260
}
256261

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

266274
export function getBrowserType() {
267275
const o = {
@@ -270,35 +278,77 @@ export function getBrowserType() {
270278
chrome: 0, // Chrome, Chromium, Brave, Edge
271279
unknown: 0,
272280
chromeVersion: 0,
281+
device: 0,
273282
};
274283
if (isFirefox()) {
284+
// Firefox, Zen
275285
o.firefox = 1;
276286
} else {
277287
//@ts-ignore
278288
const isWebkitBased = typeof webkitIndexedDB === "object";
279289
if (isWebkitBased) {
290+
// Safari, Orion
280291
o.webkit = 1;
281292
} else {
282-
//@ts-ignore
283-
const isChromeBased = typeof webkitRequestAnimationFrame === "function";
293+
const isChromeBased =
294+
typeof requestAnimationFrame === "function"
295+
? // @ts-ignore. For Page only
296+
typeof webkitRequestAnimationFrame === "function"
297+
: // @ts-ignore. Available in Worker (Chrome 74+ Edge 79+)
298+
typeof BackgroundFetchRecord === "function";
284299
if (isChromeBased) {
285300
const isEdgeBrowser = isEdge();
286301
const chromeVersion = getBrowserVersion();
287302
o.chrome |= isEdgeBrowser ? BrowserType.Edge : BrowserType.Chrome;
288-
o.chrome |= chromeVersion < 120 ? BrowserType.chromeA : 0; // Chrome 120 以下
289-
o.chrome |= chromeVersion < 138 ? BrowserType.chromeB : BrowserType.chromeC; // Chrome 121 ~ 137 / 138 以上
290-
if (isEdgeBrowser) {
291-
o.chrome |= chromeVersion >= 144 ? BrowserType.edgeA : 0; // Edge 144 以上
303+
// 由小至大
304+
if (chromeVersion < 120) {
305+
o.chrome |= BrowserType.noUserScriptsAPI;
306+
} else {
307+
// 120+
308+
if (isEdgeBrowser ? chromeVersion < 144 : chromeVersion < 138) {
309+
o.chrome |= BrowserType.guardedByDeveloperMode;
310+
} else {
311+
// Edge 144+ / Chrome 138+
312+
o.chrome |= BrowserType.guardedByAllowScript;
313+
// 如日后再变化,在这里再加条件式
314+
}
292315
}
293316
o.chromeVersion = chromeVersion;
294317
} else {
295318
o.unknown = 1;
296319
}
297320
}
298321
}
322+
// BrowserType.Mouse 未能在 Worker 使用
323+
o.device |= typeof matchMedia === "function" && !matchMedia("(hover: none)").matches ? BrowserType.Mouse : 0;
324+
o.device |= navigator.maxTouchPoints > 0 ? BrowserType.Touch : 0;
299325
return o;
300326
}
301327

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