Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/background/handlers/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { loadData } from '../../shared/api/storage';
import {
isCheckUrlMessage,
isContinueActiveTabMessage,
isGetBlockedPageStateMessage,
isGetDataMessage,
isGoBackActiveTabMessage,
Expand Down Expand Up @@ -40,6 +41,11 @@ export function handleMessage(
return true;
}

if (isContinueActiveTabMessage(message)) {
void handleContinueActiveTab(sendResponse);
return true;
}

if (isGetBlockedPageStateMessage(message)) {
void handleGetBlockedPageState(message.blockId, sender, sendResponse);
return true;
Expand All @@ -65,23 +71,27 @@ async function handleGoBackActiveTab(sendResponse: (response: unknown) => void):
sendResponse({ restored: await getTabController().goBackFromActiveTab() });
}

async function handleContinueActiveTab(sendResponse: (response: unknown) => void): Promise<void> {
sendResponse({ continued: await getTabController().continueFromActiveTab() });
}

async function handleGetBlockedPageState(
blockId: string | undefined,
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
): Promise<void> {
if (blockId) {
sendResponse(await getTabController().getBlockedPageStateByBlockId(blockId));
const senderTabId = sender.tab?.id;
if (typeof senderTabId === 'number') {
sendResponse(await getTabController().getFreshBlockedPageState(senderTabId, sender.tab?.url));
return;
}

const senderTabId = sender.tab?.id;
if (typeof senderTabId !== 'number') {
sendResponse({ status: 'unavailable' });
if (blockId) {
sendResponse(await getTabController().getBlockedPageStateByBlockId(blockId));
return;
}

sendResponse(await getTabController().getFreshBlockedPageState(senderTabId, sender.tab?.url));
sendResponse({ status: 'unavailable' });
}

function isInternalSender(sender: chrome.runtime.MessageSender): boolean {
Expand Down
115 changes: 102 additions & 13 deletions src/background/tabController.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
clearBlockedTabState,
clearWarningBypassState,
getBlockedPageState,
getBlockedTabState,
getLastAllowedUrl,
getWarningBypassState,
setBlockedPageState,
setBlockedTabState,
setLastAllowedUrl,
setWarningBypassState,
} from '../shared/api/session';
import { getActiveTab, queryTabs, updateTabUrl } from '../shared/api/tabs';
import { getExtensionUrl } from '../shared/api/runtime';
Expand Down Expand Up @@ -72,6 +75,14 @@ class TabController {
const decision = rules.engine.evaluate(url);

if (decision.action === 'block') {
if (
decision.blockType === 'warning' &&
(await this.isWarningBypassed(tabId, url, decision))
) {
await this.allowTab(tabId, url, { preserveWarningBypass: true });
return;
}

await this.blockTab(tabId, url, decision, rules.data);
return;
}
Expand All @@ -92,13 +103,17 @@ class TabController {

const rules = await this.getRules();
const decision = rules.engine.evaluate(resolvedTarget.targetUrl);
if (decision.action === 'block') {
const bypassed =
decision.action === 'block' &&
decision.blockType === 'warning' &&
(await this.isWarningBypassed(tabId, resolvedTarget.targetUrl, decision));
if (decision.action === 'block' && !bypassed) {
await this.ensureBlockedState(tabId, resolvedTarget.targetUrl, decision, rules.data);
return false;
}

await updateTabUrl(tabId, resolvedTarget.targetUrl);
await this.allowTab(tabId, resolvedTarget.targetUrl);
await this.allowTab(tabId, resolvedTarget.targetUrl, { preserveWarningBypass: bypassed });
return true;
}

Expand All @@ -123,13 +138,9 @@ class TabController {
tabId: number,
blockedPageUrl?: string
): Promise<GetBlockedPageStateResponse> {
const blockId = parseBlockedPageBlockId(blockedPageUrl);
if (blockId) {
return this.getBlockedPageStateByBlockId(blockId);
}

if (!Number.isInteger(tabId)) {
return { status: 'unavailable' };
const blockId = parseBlockedPageBlockId(blockedPageUrl);
return blockId ? this.getBlockedPageStateByBlockId(blockId) : { status: 'unavailable' };
}

const resolvedTarget = await this.resolveBlockedTarget(tabId, blockedPageUrl);
Expand Down Expand Up @@ -200,6 +211,35 @@ class TabController {
return true;
}

async continueFromActiveTab(): Promise<boolean> {
const activeTab = await getActiveTab();
if (!activeTab?.id) {
return false;
}

const resolvedTarget = await this.resolveBlockedTarget(activeTab.id, activeTab.url);
if (!resolvedTarget) {
return false;
}

const rules = await this.getRules();
const decision = rules.engine.evaluate(resolvedTarget.targetUrl);
if (decision.action !== 'block' || decision.blockType !== 'warning') {
return false;
}

await Promise.all([
setWarningBypassState(activeTab.id, {
filterId: decision.filterId,
urlKey: getWarningBypassUrlKey(resolvedTarget.targetUrl),
}),
clearBlockedTabState(activeTab.id),
setLastAllowedUrl(activeTab.id, resolvedTarget.targetUrl),
]);
await updateTabUrl(activeTab.id, resolvedTarget.targetUrl);
return true;
}

private async reconcileTab(tabId: number, url: string): Promise<void> {
const blockedPageUrl = getExtensionUrl(PAGES.BLOCKED);
if (url.startsWith(blockedPageUrl)) {
Expand All @@ -222,10 +262,14 @@ class TabController {

const rules = await this.getRules();
const decision = rules.engine.evaluate(resolvedTarget.targetUrl);
if (decision.action !== 'block') {
const bypassed =
decision.action === 'block' &&
decision.blockType === 'warning' &&
(await this.isWarningBypassed(tabId, resolvedTarget.targetUrl, decision));
if (decision.action !== 'block' || bypassed) {
if (resolvedTarget.hasSessionState) {
await updateTabUrl(tabId, resolvedTarget.targetUrl);
await this.allowTab(tabId, resolvedTarget.targetUrl);
await this.allowTab(tabId, resolvedTarget.targetUrl, { preserveWarningBypass: bypassed });
}
return;
}
Expand All @@ -251,8 +295,24 @@ class TabController {
await updateTabUrl(tabId, getBlockedPageUrl(state.tabState.blockId));
}

private async allowTab(tabId: number, url: string): Promise<void> {
await Promise.all([clearBlockedTabState(tabId), setLastAllowedUrl(tabId, url)]);
private async allowTab(
tabId: number,
url: string,
options?: { readonly preserveWarningBypass?: boolean }
): Promise<void> {
const operations: Promise<void>[] = [
clearBlockedTabState(tabId),
setLastAllowedUrl(tabId, url),
];

if (!options?.preserveWarningBypass) {
const warningBypass = await getWarningBypassState(tabId);
if (warningBypass && warningBypass.urlKey !== getWarningBypassUrlKey(url)) {
operations.push(clearWarningBypassState(tabId));
}
}

await Promise.all(operations);
}

private async setBlockedState(
Expand Down Expand Up @@ -324,7 +384,11 @@ class TabController {
): Promise<BlockedStateResult | undefined> {
const rules = currentRules ?? (await this.getRules());
const decision = rules.engine.evaluate(targetUrl);
if (decision.action !== 'block') {
if (
decision.action !== 'block' ||
(decision.blockType === 'warning' &&
(await this.isWarningBypassed(tabId, targetUrl, decision)))
) {
await clearBlockedTabState(tabId);
return undefined;
}
Expand Down Expand Up @@ -366,6 +430,22 @@ class TabController {
private async getRules(): Promise<CurrentRules> {
return this.rulesProvider.loadCurrentRules();
}

private async isWarningBypassed(
tabId: number,
targetUrl: string,
decision: Extract<FilterDecision, { action: 'block' }>
): Promise<boolean> {
if (decision.blockType !== 'warning') {
return false;
}

const warningBypass = await getWarningBypassState(tabId);
return (
warningBypass?.filterId === decision.filterId &&
warningBypass.urlKey === getWarningBypassUrlKey(targetUrl)
);
}
}

function parseBlockedPageBlockId(tabUrl: string | undefined): string | null {
Expand Down Expand Up @@ -422,6 +502,15 @@ function isSameBlockedState(
);
}

function getWarningBypassUrlKey(targetUrl: string): string {
try {
const parsed = new URL(targetUrl);
return parsed.origin !== 'null' ? parsed.origin : targetUrl;
} catch {
return targetUrl;
}
}

function createFilterSnapshot(
filter: Filter | undefined,
fallbackFilterId: string
Expand Down
1 change: 1 addition & 0 deletions src/blocked/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ <h1>Page Blocked</h1>
</dl>
</section>
<div class="actions">
<button class="button" id="continue" type="button" hidden>Continue</button>
<button class="button" id="go-back" type="button">Go Back</button>
<button class="button secondary" id="open-options" type="button">Manage Filters</button>
</div>
Expand Down
51 changes: 47 additions & 4 deletions src/blocked/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { openOptionsPage } from '../shared/api/runtime';
import {
MessageType,
type BlockedPageState,
type ContinueActiveTabResponse,
type FilterMatchMode,
type GetBlockedPageStateResponse,
type GoBackActiveTabResponse,
Expand All @@ -23,9 +24,7 @@ interface BlockedPageViewModel {
* Initialize blocked page
*/
async function init(): Promise<void> {
const state = await getBlockedPageState();
renderBlockedUrl(state);
renderResponsibleFilter(state);
await renderPage();

const goBackButton = getElementByIdOrNull('go-back');
goBackButton?.addEventListener('click', () => {
Expand All @@ -34,13 +33,35 @@ async function init(): Promise<void> {
});
});

const continueButton = getElementByIdOrNull('continue');
continueButton?.addEventListener('click', () => {
void handleContinue().catch((error: unknown) => {
console.error('Failed to continue past warning:', error);
});
});

// Set up options button
const openOptionsButton = getElementByIdOrNull('open-options');
openOptionsButton?.addEventListener('click', () => {
openOptionsPage().catch((error: unknown) => {
console.error('Failed to open options page:', error);
});
});

chrome.storage.onChanged.addListener((_changes, areaName) => {
if (areaName !== 'sync' && areaName !== 'session') {
return;
}

void renderPage();
});
}

async function renderPage(): Promise<void> {
const state = await getBlockedPageState();
renderBlockedUrl(state);
renderResponsibleFilter(state);
renderActions(state);
}

async function getBlockedPageState(): Promise<BlockedPageViewModel> {
Expand Down Expand Up @@ -129,6 +150,16 @@ async function handleGoBack(): Promise<void> {
}
}

async function handleContinue(): Promise<void> {
const response = (await chrome.runtime.sendMessage({
type: MessageType.CONTINUE_ACTIVE_TAB,
})) as ContinueActiveTabResponse;

if (!response.continued) {
console.warn('[Teichos] No warning bypass is available for this tab.');
}
}

function renderBlockedUrl(state: BlockedPageViewModel): void {
const blockedUrlElement = getElementByIdOrNull('blocked-url');
if (blockedUrlElement) {
Expand All @@ -138,7 +169,12 @@ function renderBlockedUrl(state: BlockedPageViewModel): void {

function renderResponsibleFilter(state: BlockedPageViewModel): void {
const detailSection = getElementByIdOrNull<HTMLElement>('responsible-filter');
if (!detailSection || !state.state) {
if (!detailSection) {
return;
}

if (!state.state) {
detailSection.hidden = true;
return;
}

Expand All @@ -154,6 +190,13 @@ function renderResponsibleFilter(state: BlockedPageViewModel): void {
detailSection.hidden = false;
}

function renderActions(state: BlockedPageViewModel): void {
const continueButton = getElementByIdOrNull<HTMLButtonElement>('continue');
if (continueButton) {
continueButton.hidden = state.state?.blockType !== 'warning';
}
}

function setText(elementId: string, value: string): void {
const element = getElementByIdOrNull(elementId);
if (element) {
Expand Down
19 changes: 17 additions & 2 deletions src/options/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,16 @@ <h2>Groups</h2>
<div class="group-content">
<div class="global-settings-panel">
<p class="global-settings-note">
Export your current Teichos configuration, or import a previous backup to replace the
current settings.
Choose the default block behavior for new and inherited filters, or export/import your
current Teichos configuration.
</p>
<div class="form-row">
<label for="global-block-type">Block Type</label>
<select id="global-block-type" class="input">
<option value="block">Block Page</option>
<option value="warning">Warning</option>
</select>
</div>
<div class="global-settings-actions">
<button class="button secondary" id="export-settings-btn" type="button">
Export Settings
Expand Down Expand Up @@ -377,6 +384,14 @@ <h2 id="filter-modal-title">Add Filter</h2>
<option value="regex">Regular Expression</option>
</select>
</div>
<div class="form-row">
<label for="filter-block-type">Block Type</label>
<select id="filter-block-type" class="input">
<option value="default" selected>Default</option>
<option value="block">Block Page</option>
<option value="warning">Warning</option>
</select>
</div>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="filter-enabled" checked />
Expand Down
Loading
Loading