Skip to content

Commit

Permalink
feat(mv3): ✨ Introduces Redirect Rule Management (#1240)
Browse files Browse the repository at this point in the history
* feat: exporting rules ending regex

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* feat: ✨ Adding Rule Management UI

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* feat: ✨ hooking up with background worker.

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* fix: lint

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* fix: 🎨 button styling

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

* fix(mv3): 💄 Making UI a bit better

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>

---------

Signed-off-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com>
  • Loading branch information
whizzzkid authored Jul 28, 2023
1 parent 2d8528e commit fe0e159
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 21 deletions.
16 changes: 16 additions & 0 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,10 @@
"message": "Experiments",
"description": "A section header on the Preferences screen (option_header_experiments)"
},
"option_header_redirect_rules": {
"message": "Redirect Rules",
"description": "A section header on the Preferences screen (option_header_redirect_rules)"
},
"option_header_reset": {
"message": "Reset Preferences",
"description": "A section header on the Preferences screen (option_header_reset)"
Expand Down Expand Up @@ -507,6 +511,18 @@
"message": "Automatically preload assets imported to IPFS via asynchronous HTTP HEAD requests to a public gateway.",
"description": "An option description on the Preferences screen (option_preloadAtPublicGateway_description)"
},
"option_redirect_rules_reset_all": {
"message": "Reset All Redirect Rules",
"description": "A button label on the Preferences screen (option_redirect_rules_reset_all)"
},
"option_redirect_rules_row_origin": {
"message": "Origin",
"description": "A table header on the Preferences screen (option_redirect_rules_row_origin)"
},
"option_redirect_rules_row_target": {
"message": "Target",
"description": "A table header on the Preferences screen (option_redirect_rules_row_target)"
},
"option_logNamespaces_title": {
"message": "Log Namespaces",
"description": "An option title for tweaking log level (option_logNamespaces_title)"
Expand Down
71 changes: 52 additions & 19 deletions add-on/src/lib/redirect-handler/blockOrObserve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { CompanionState } from '../../types/companion.js'
const log = debug('ipfs-companion:redirect-handler:blockOrObserve')
log.error = debug('ipfs-companion:redirect-handler:blockOrObserve:error')

export const GLOBAL_STATE_CHANGE = 'GLOBAL_STATE_CHANGE'
export const GLOBAL_STATE_OPTION_CHANGE = 'GLOBAL_STATE_OPTION_CHANGE'
export const DELETE_RULE_REQUEST = 'DELETE_RULE_REQUEST'
export const DELETE_RULE_REQUEST_SUCCESS = 'DELETE_RULE_REQUEST_SUCCESS'
export const RULE_REGEX_ENDING = '((?:[^\\.]|$).*)$'

interface regexFilterMap {
id: number
regexSubstitution: string
Expand All @@ -17,16 +23,16 @@ interface redirectHandlerInput {
redirectUrl: string
}

type messageToSelfType = typeof GLOBAL_STATE_CHANGE | typeof GLOBAL_STATE_OPTION_CHANGE | typeof DELETE_RULE_REQUEST
interface messageToSelf {
type: typeof GLOBAL_STATE_CHANGE | typeof GLOBAL_STATE_OPTION_CHANGE
type: messageToSelfType
value?: string | Record<string, unknown>
}

// We need to check if the browser supports the declarativeNetRequest API.
// TODO: replace with check for `Blocking` in `chrome.webRequest.OnBeforeRequestOptions`
// which is currently a bug https://bugs.chromium.org/p/chromium/issues/detail?id=1427952
export const supportsBlock = !(browser.declarativeNetRequest?.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES === 5000)
export const GLOBAL_STATE_CHANGE = 'GLOBAL_STATE_CHANGE'
export const GLOBAL_STATE_OPTION_CHANGE = 'GLOBAL_STATE_OPTION_CHANGE'

/**
* Notify self about state change.
Expand All @@ -44,16 +50,20 @@ export async function notifyOptionChange (): Promise<void> {
return await sendMessageToSelf(GLOBAL_STATE_OPTION_CHANGE)
}

export async function notifyDeleteRule (id: number): Promise<void> {
return await sendMessageToSelf(DELETE_RULE_REQUEST, id)
}

/**
* Sends message to self to notify about change.
*
* @param msg
*/
async function sendMessageToSelf (msg: typeof GLOBAL_STATE_CHANGE | typeof GLOBAL_STATE_OPTION_CHANGE): Promise<void> {
async function sendMessageToSelf (msg: messageToSelfType, value?: any): Promise<void> {
// this check ensures we don't send messages to ourselves if blocking mode is enabled.
if (!supportsBlock) {
const message: messageToSelf = { type: msg }
await browser.runtime.sendMessage(message)
const message: messageToSelf = { type: msg, value }
await browser.runtime.sendMessage({ message })
}
}

Expand Down Expand Up @@ -119,16 +129,16 @@ function constructRegexFilter ({ originUrl, redirectUrl }: redirectHandlerInput)
// We need to escape the characters that are allowed in the URL, but not in the regex.
const regexFilterFirst = escapeURLRegex(originUrl.slice(0, originUrl.length - commonIdx + 1))
// We need to match the rest of the URL, so we can use a wildcard.
const regexEnding = '((?:[^\\.]|$).*)$'
let regexFilter = `^${regexFilterFirst}${regexEnding}`.replace(/https?/ig, 'https?')
const RULE_REGEX_ENDING = '((?:[^\\.]|$).*)$'
let regexFilter = `^${regexFilterFirst}${RULE_REGEX_ENDING}`.replace(/https?/ig, 'https?')

// This method does not parse:
// originUrl: "https://awesome.ipfs.io/"
// redirectUrl: "http://localhost:8081/ipns/awesome.ipfs.io/"
// that ends up with capturing all urls which we do not want.
if (regexFilter === `^https?\\:\\/${regexEnding}`) {
if (regexFilter === `^https?\\:\\/${RULE_REGEX_ENDING}`) {
const subdomain = new URL(originUrl).hostname
regexFilter = `^https?\\:\\/\\/${escapeURLRegex(subdomain)}${regexEnding}`
regexFilter = `^https?\\:\\/\\/${escapeURLRegex(subdomain)}${RULE_REGEX_ENDING}`
regexSubstitution = regexSubstitution.replace('\\1', `/${subdomain}\\1`)
}

Expand Down Expand Up @@ -171,18 +181,25 @@ export async function cleanupRules (resetInMemory: boolean = false): Promise<voi
}
}

/**
* Clean up a rule by ID.
*
* @param id number
*/
async function cleanupRuleById (id: number): Promise<void> {
const [{ condition: { regexFilter } }] = await browser.declarativeNetRequest.getDynamicRules({ ruleIds: [id] })
savedRegexFilters.delete(regexFilter as string)
await browser.declarativeNetRequest.updateDynamicRules({ addRules: [], removeRuleIds: [id] })
}

/**
* This function sets up the listeners for the extension.
* @param {function} handlerFn
*/
function setupListeners (handlerFn: () => Promise<void>): void {
browser.runtime.onMessage.addListener(async ({ type }: messageToSelf): Promise<void> => {
if (type === GLOBAL_STATE_CHANGE) {
await handlerFn()
}
if (type === GLOBAL_STATE_OPTION_CHANGE) {
await cleanupRules(true)
await handlerFn()
function setupListeners (handlers: Record<messageToSelfType, (value: any) => Promise<void>>): void {
browser.runtime.onMessage.addListener(async ({ message: { type, value } }: { message: messageToSelf }): Promise<void> => {
if (type in handlers) {
await handlers[type](value)
}
})
}
Expand Down Expand Up @@ -354,7 +371,23 @@ export function addRuleToDynamicRuleSetGenerator (
)
}

setupListeners(async (): Promise<void> => await reconcileRulesAndRemoveOld(getState()))
setupListeners({
[GLOBAL_STATE_CHANGE]: async (): Promise<void> => {
await reconcileRulesAndRemoveOld(getState())
},
[GLOBAL_STATE_OPTION_CHANGE]: async (): Promise<void> => {
await cleanupRules(true)
await reconcileRulesAndRemoveOld(getState())
},
[DELETE_RULE_REQUEST]: async (value: number): Promise<void> => {
if (value != null) {
await cleanupRuleById(value)
await browser.runtime.sendMessage({ type: DELETE_RULE_REQUEST_SUCCESS })
} else {
await cleanupRules(true)
}
}
})
// call to reconcile rules and remove old ones.
await reconcileRulesAndRemoveOld(state)
}
Expand Down
79 changes: 79 additions & 0 deletions add-on/src/options/forms/redirect-rule-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import browser from 'webextension-polyfill'

/**
*
* @param {(event: string, value?: any) => void} emit
* @returns
*/
function ruleItem (emit) {
/**
* Renders Rule Item
*
* @param {{
* id: string
* origin: string
* target: string
* }} param0
* @returns
*/
return function ({ id, origin, target }) {
return html`
<div class="flex flex-row-ns pb0-ns">
<dl class="flex-grow-1">
<dt>
<span class="b">${browser.i18n.getMessage('option_redirect_rules_row_origin')}:</span> ${origin}
</dt>
<dt>
<span class="b">${browser.i18n.getMessage('option_redirect_rules_row_target')}:</span> ${target}
</dt>
</dl>
<div class="rule-delete">
<button class="f6 ph3 pv2 mt0 mb0 bg-transparent b--none red" onclick=${() => emit('redirectRuleDeleteRequest', id)}>X</button>
</div>
</div>
`
}
}

/**
*
* @param {{
* emit: (event: string, value?: any) => void,
* redirectRules: {
* id: string
* origin: string
* target: string
* }[]
* }} param0
* @returns
*/
export default function redirectRuleForm ({ emit, redirectRules }) {
return html`
<form>
<fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">
<h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_redirect_rules')}</h2>
<div class="flex-row-ns pb0-ns">
<label for="deleteAllRules">
<dl>
<dt>
<div class="self-right-ns">
Found ${redirectRules?.length ?? 0} rules
</div>
</dt>
</dl>
</label>
<div class="self-center-ns">
<button id="deleteAllRules" class="Button transition-all sans-serif v-mid fw5 nowrap lh-copy bn br1 pa2 pointer focus-outline white bg-red white" onclick=${() => emit('redirectRuleDeleteRequest')}>${browser.i18n.getMessage('option_redirect_rules_reset_all')}</button>
</div>
</div>
<div style="max-height: 250px; overflow-y: auto">
${redirectRules ? redirectRules.map(ruleItem(emit)) : html`<div>Loading...</div>`}
</div>
</fieldset>
</form>
`
}
7 changes: 6 additions & 1 deletion add-on/src/options/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,9 @@ input:invalid {
input.brave {
background-color: #f7f8fa;
}

div.rule-delete {
display: flex;
flex-direction: row;
justify-content: flex-end;
max-width: 50px;
}
8 changes: 8 additions & 0 deletions add-on/src/options/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import apiForm from './forms/api-form.js'
import experimentsForm from './forms/experiments-form.js'
import telemetryForm from './forms/telemetry-form.js'
import resetForm from './forms/reset-form.js'
import redirectRuleForm from './forms/redirect-rule-form.js'
import { supportsBlock } from '../lib/redirect-handler/blockOrObserve.js'

// Render the options page:
// Passed current app `state` from the store and `emit`, a function to create
Expand Down Expand Up @@ -112,6 +114,12 @@ export default function optionsPage (state, emit) {
})}
${resetForm({
onOptionsReset
})}
${supportsBlock
? ''
: redirectRuleForm({
redirectRules: state.redirectRules,
emit
})}
</div>
`
Expand Down
23 changes: 22 additions & 1 deletion add-on/src/options/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@

import browser from 'webextension-polyfill'
import { optionDefaults } from '../lib/options.js'
import { notifyOptionChange, notifyStateChange } from '../lib/redirect-handler/blockOrObserve.js'
import { DELETE_RULE_REQUEST_SUCCESS, RULE_REGEX_ENDING, notifyDeleteRule, notifyOptionChange, notifyStateChange } from '../lib/redirect-handler/blockOrObserve.js'
import createRuntimeChecks from '../lib/runtime-checks.js'

// The store contains and mutates the state for the app
export default function optionStore (state, emitter) {
state.options = optionDefaults

const fetchRedirectRules = async () => {
const existingRedirectRules = await browser.declarativeNetRequest.getDynamicRules()
state.redirectRules = existingRedirectRules.map(rule => ({
id: rule.id,
origin: rule.condition.regexFilter?.replace(RULE_REGEX_ENDING, '(.*)').replaceAll('\\', ''),
target: rule.action.redirect?.regexSubstitution?.replace('\\1', '<resource-path>')
}))
emitter.emit('render')
}

const updateStateOptions = async () => {
const runtime = await createRuntimeChecks(browser)
state.withNodeFromBrave = runtime.brave && await runtime.brave.getIPFSEnabled()
Expand All @@ -23,9 +33,20 @@ export default function optionStore (state, emitter) {
emitter.on('DOMContentLoaded', async () => {
browser.runtime.sendMessage({ telemetry: { trackView: 'options' } })
updateStateOptions()
fetchRedirectRules()
browser.storage.onChanged.addListener(updateStateOptions)
})

emitter.on('redirectRuleDeleteRequest', async (id) => {
console.log('delete rule request', id)
browser.runtime.onMessage.addListener(({ type }) => {
if (type === DELETE_RULE_REQUEST_SUCCESS) {
emitter.emit('render')
}
})
notifyDeleteRule(id)
})

emitter.on('optionChange', async ({ key, value }) => {
browser.storage.local.set({ [key]: value })
if (key === 'active') {
Expand Down

0 comments on commit fe0e159

Please sign in to comment.