mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
Merge pull request #7 from ksyasuda/feature/add-anilist-tracking
Add AniList Tracking
This commit is contained in:
37
src/main/anilist-url-guard.test.ts
Normal file
37
src/main/anilist-url-guard.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
isAllowedAnilistExternalUrl,
|
||||
isAllowedAnilistSetupNavigationUrl,
|
||||
} from "./anilist-url-guard";
|
||||
|
||||
test("allows only AniList https URLs for external opens", () => {
|
||||
assert.equal(isAllowedAnilistExternalUrl("https://anilist.co"), true);
|
||||
assert.equal(
|
||||
isAllowedAnilistExternalUrl("https://www.anilist.co/settings/developer"),
|
||||
true,
|
||||
);
|
||||
assert.equal(isAllowedAnilistExternalUrl("http://anilist.co"), false);
|
||||
assert.equal(isAllowedAnilistExternalUrl("https://example.com"), false);
|
||||
assert.equal(isAllowedAnilistExternalUrl("file:///tmp/test"), false);
|
||||
assert.equal(isAllowedAnilistExternalUrl("not a url"), false);
|
||||
});
|
||||
|
||||
test("allows only AniList https or data URLs for setup navigation", () => {
|
||||
assert.equal(
|
||||
isAllowedAnilistSetupNavigationUrl("https://anilist.co/api/v2/oauth/authorize"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedAnilistSetupNavigationUrl(
|
||||
"data:text/html;charset=utf-8,%3Chtml%3E%3C%2Fhtml%3E",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAllowedAnilistSetupNavigationUrl("https://example.com/redirect"),
|
||||
false,
|
||||
);
|
||||
assert.equal(isAllowedAnilistSetupNavigationUrl("javascript:alert(1)"), false);
|
||||
});
|
||||
25
src/main/anilist-url-guard.ts
Normal file
25
src/main/anilist-url-guard.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const ANILIST_ALLOWED_HOSTS = new Set(["anilist.co", "www.anilist.co"]);
|
||||
|
||||
export function isAllowedAnilistExternalUrl(rawUrl: string): boolean {
|
||||
try {
|
||||
const parsedUrl = new URL(rawUrl);
|
||||
return (
|
||||
parsedUrl.protocol === "https:" &&
|
||||
ANILIST_ALLOWED_HOSTS.has(parsedUrl.hostname.toLowerCase())
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAllowedAnilistSetupNavigationUrl(rawUrl: string): boolean {
|
||||
if (isAllowedAnilistExternalUrl(rawUrl)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const parsedUrl = new URL(rawUrl);
|
||||
return parsedUrl.protocol === "data:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,27 @@ import type { RuntimeOptionsManager } from "../runtime-options";
|
||||
import type { MecabTokenizer } from "../mecab-tokenizer";
|
||||
import type { BaseWindowTracker } from "../window-trackers";
|
||||
|
||||
export interface AnilistSecretResolutionState {
|
||||
status: "not_checked" | "resolved" | "error";
|
||||
source: "none" | "literal" | "stored";
|
||||
message: string | null;
|
||||
resolvedAt: number | null;
|
||||
errorAt: number | null;
|
||||
}
|
||||
|
||||
export interface AnilistRetryQueueState {
|
||||
pending: number;
|
||||
ready: number;
|
||||
deadLetter: number;
|
||||
lastAttemptAt: number | null;
|
||||
lastError: string | null;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
yomitanExt: Extension | null;
|
||||
yomitanSettingsWindow: BrowserWindow | null;
|
||||
yomitanParserWindow: BrowserWindow | null;
|
||||
anilistSetupWindow: BrowserWindow | null;
|
||||
yomitanParserReadyPromise: Promise<void> | null;
|
||||
yomitanParserInitPromise: Promise<boolean> | null;
|
||||
mpvClient: MpvIpcClient | null;
|
||||
@@ -33,6 +50,7 @@ export interface AppState {
|
||||
currentMediaPath: string | null;
|
||||
currentMediaTitle: string | null;
|
||||
pendingSubtitlePosition: SubtitlePosition | null;
|
||||
anilistClientSecretState: AnilistSecretResolutionState;
|
||||
mecabTokenizer: MecabTokenizer | null;
|
||||
keybindings: Keybinding[];
|
||||
subtitleTimingTracker: SubtitleTimingTracker | null;
|
||||
@@ -57,6 +75,8 @@ export interface AppState {
|
||||
texthookerOnlyMode: boolean;
|
||||
jlptLevelLookup: (term: string) => JlptLevel | null;
|
||||
frequencyRankLookup: FrequencyDictionaryLookup;
|
||||
anilistSetupPageOpened: boolean;
|
||||
anilistRetryQueueState: AnilistRetryQueueState;
|
||||
}
|
||||
|
||||
export interface AppStateInitialValues {
|
||||
@@ -81,6 +101,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
||||
yomitanExt: null,
|
||||
yomitanSettingsWindow: null,
|
||||
yomitanParserWindow: null,
|
||||
anilistSetupWindow: null,
|
||||
yomitanParserReadyPromise: null,
|
||||
yomitanParserInitPromise: null,
|
||||
mpvClient: null,
|
||||
@@ -92,6 +113,13 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
||||
currentMediaPath: null,
|
||||
currentMediaTitle: null,
|
||||
pendingSubtitlePosition: null,
|
||||
anilistClientSecretState: {
|
||||
status: "not_checked",
|
||||
source: "none",
|
||||
message: null,
|
||||
resolvedAt: null,
|
||||
errorAt: null,
|
||||
},
|
||||
mecabTokenizer: null,
|
||||
keybindings: [],
|
||||
subtitleTimingTracker: null,
|
||||
@@ -118,6 +146,14 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
||||
texthookerOnlyMode: values.texthookerOnlyMode ?? false,
|
||||
jlptLevelLookup: () => null,
|
||||
frequencyRankLookup: () => null,
|
||||
anilistSetupPageOpened: false,
|
||||
anilistRetryQueueState: {
|
||||
pending: 0,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
lastAttemptAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user