Skip to content

Commit 32b0f07

Browse files
committed
feat: manual opt-ins and opt-outs
This improves the way we handle manual opt-out performend per site by the user by storing both manual opt-outs and opt-ins. This way we are able to tweak implicit default per website while respecting preexisting user choices. Also, in the future we may run opt-in metrics that compare lengths of both lists to gain better insight into user's behavior. Closes #921
1 parent c176fc4 commit 32b0f07

File tree

14 files changed

+109
-45
lines changed

14 files changed

+109
-45
lines changed

add-on/_locales/en/messages.json

+14-6
Original file line numberDiff line numberDiff line change
@@ -339,13 +339,21 @@
339339
"message": "Do not use if your IPFS node does not support *.ipfs.localhost. Redirecting to a path-based gateway breaks the origin-based security isolation of DNSLink websites, so make sure you understand the related risks.",
340340
"description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)"
341341
},
342-
"option_noIntegrationsHostnames_title": {
343-
"message": "Site Opt-Out List",
344-
"description": "An option title on the Preferences screen (option_noIntegrationsHostnames_title)"
342+
"option_disabledOn_title": {
343+
"message": "Manual Opt-Out List",
344+
"description": "An option title on the Preferences screen (option_disabledOn_title)"
345345
},
346-
"option_noIntegrationsHostnames_description": {
347-
"message": "Sites in this list (one hostname per line) will have all IPFS integrations disabled.",
348-
"description": "An option description on the Preferences screen (option_noRedirectHostnames_description)"
346+
"option_disabledOn_description": {
347+
"message": "Sites in this list (one hostname per line) will have IPFS integrations disabled.",
348+
"description": "An option description on the Preferences screen (option_disabledOn_description)"
349+
},
350+
"option_enabledOn_title": {
351+
"message": "Manual Opt-In List",
352+
"description": "An option title on the Preferences screen (option_enabledOn_title)"
353+
},
354+
"option_enabledOn_description": {
355+
"message": "Sites in this list (one hostname per line) will have IPFS integrations enabled.",
356+
"description": "An option description on the Preferences screen (option_enabledOn_description)"
349357
},
350358
"option_publicGatewayUrl_title": {
351359
"message": "Default Public Gateway",

add-on/src/lib/ipfs-companion.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ module.exports = async function init () {
4949

5050
try {
5151
log('init')
52-
await migrateOptions(browser.storage.local)
5352
await storeMissingOptions(await browser.storage.local.get(), optionDefaults, browser.storage.local)
53+
await migrateOptions(browser.storage.local, debug)
5454
const options = await browser.storage.local.get(optionDefaults)
5555
runtime = await createRuntimeChecks(browser)
5656
state = initState(options)
@@ -258,7 +258,7 @@ module.exports = async function init () {
258258
openViaWebUI: state.openViaWebUI,
259259
apiURLString: dropSlash(state.apiURLString),
260260
redirect: state.redirect,
261-
noIntegrationsHostnames: state.noIntegrationsHostnames,
261+
disabledOn: state.disabledOn,
262262
currentTab
263263
}
264264
try {
@@ -285,7 +285,7 @@ module.exports = async function init () {
285285
}
286286
info.currentDnslinkFqdn = dnslinkResolver.findDNSLinkHostname(url)
287287
info.currentFqdn = info.currentDnslinkFqdn || new URL(url).hostname
288-
info.currentTabIntegrationsOptOut = info.noIntegrationsHostnames && info.noIntegrationsHostnames.includes(info.currentFqdn)
288+
info.currentTabIntegrationsOptOut = info.disabledOn && info.disabledOn.includes(info.currentFqdn)
289289
info.isRedirectContext = info.currentFqdn && ipfsPathValidator.isRedirectPageActionsContext(url)
290290
}
291291
// Still here?
@@ -697,7 +697,8 @@ module.exports = async function init () {
697697
case 'preloadAtPublicGateway':
698698
case 'openViaWebUI':
699699
case 'useLatestWebUI':
700-
case 'noIntegrationsHostnames':
700+
case 'enabledOn':
701+
case 'disabledOn':
701702
case 'dnslinkRedirect':
702703
state[key] = change.newValue
703704
break

add-on/src/lib/ipfs-request.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ function createRequestModifier (getState, dnslinkResolver, ipfsPathValidator, ru
8989
if (fqdn.endsWith(optout) || (parentFqdn && parentFqdn.endsWith(optout))) return true
9090
return false
9191
}
92-
if (state.noIntegrationsHostnames.some(triggerOptOut)) {
92+
if (state.disabledOn.some(triggerOptOut)) {
9393
ignore(request.requestId)
9494
}
9595

add-on/src/lib/options.js

+31-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ exports.optionDefaults = Object.freeze({
1414
publicSubdomainGatewayUrl: 'https://dweb.link',
1515
useCustomGateway: true,
1616
useSubdomains: true,
17-
noIntegrationsHostnames: [],
17+
enabledOn: [], // hostnames with explicit integration opt-in
18+
disabledOn: [], // hostnames with explicit integration opt-out
1819
automaticMode: true,
1920
linkify: false,
2021
dnslinkPolicy: 'best-effort',
@@ -145,7 +146,10 @@ function localhostNameUrl (url) {
145146
return url.hostname.toLowerCase() === 'localhost'
146147
}
147148

148-
exports.migrateOptions = async (storage) => {
149+
exports.migrateOptions = async (storage, debug) => {
150+
const log = debug('ipfs-companion:migrations')
151+
log.error = debug('ipfs-companion:migrations:error')
152+
149153
// <= v2.4.4
150154
// DNSLINK: convert old on/off 'dnslink' flag to text-based 'dnslinkPolicy'
151155
const { dnslink } = await storage.get('dnslink')
@@ -157,6 +161,7 @@ exports.migrateOptions = async (storage) => {
157161
})
158162
await storage.remove('dnslink')
159163
}
164+
160165
// ~ v2.8.x + Brave
161166
// Upgrade js-ipfs to js-ipfs + chrome.sockets
162167
const { ipfsNodeType } = await storage.get('ipfsNodeType')
@@ -167,13 +172,15 @@ exports.migrateOptions = async (storage) => {
167172
ipfsNodeConfig: buildDefaultIpfsNodeConfig()
168173
})
169174
}
175+
170176
// ~ v2.9.x: migrating noRedirectHostnames → noIntegrationsHostnames
171177
// https://github.com/ipfs-shipyard/ipfs-companion/pull/830
172178
const { noRedirectHostnames } = await storage.get('noRedirectHostnames')
173179
if (noRedirectHostnames) {
174180
await storage.set({ noIntegrationsHostnames: noRedirectHostnames })
175181
await storage.remove('noRedirectHostnames')
176182
}
183+
177184
// ~v2.11: subdomain proxy at *.ipfs.localhost
178185
// migrate old default 127.0.0.1 to localhost hostname
179186
const { customGatewayUrl: gwUrl } = await storage.get('customGatewayUrl')
@@ -184,4 +191,26 @@ exports.migrateOptions = async (storage) => {
184191
await storage.set({ customGatewayUrl: newUrl })
185192
}
186193
}
194+
195+
{ // ~v2.15.x: migrating noIntregrationsHostnames → disabledOn
196+
const { disabledOn, noIntegrationsHostnames } = await storage.get(['disabledOn', 'noIntegrationsHostnames'])
197+
if (noIntegrationsHostnames) {
198+
log('migrating noIntregrationsHostnames → disabledOn')
199+
await storage.set({ disabledOn: disabledOn.concat(noIntegrationsHostnames) })
200+
await storage.remove('noIntegrationsHostnames')
201+
}
202+
}
203+
204+
{ // ~v2.15.x: opt-out some hostnames if user does not have excplicit rule already
205+
const { enabledOn, disabledOn } = await storage.get(['enabledOn', 'disabledOn'])
206+
for (const fqdn of [
207+
'proto.school', // https://github.com/ipfs-shipyard/ipfs-companion/issues/921
208+
'app.fleek.co' // TODO: confirm if ok
209+
]) {
210+
if (enabledOn.includes(fqdn) || disabledOn.includes(fqdn)) continue
211+
log(`adding '${fqdn}' to 'disabledOn' list`)
212+
disabledOn.push(fqdn)
213+
await storage.set({ disabledOn })
214+
}
215+
}
187216
}

add-on/src/lib/state.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function initState (options, overrides) {
3232
if (!state.active) return false
3333
try {
3434
const fqdn = new URL(url).hostname
35-
return !(state.noIntegrationsHostnames.find(host => fqdn.endsWith(host)))
35+
return !(state.disabledOn.find(host => fqdn.endsWith(host)))
3636
} catch (_) {
3737
return false
3838
}

add-on/src/options/forms/dnslink-form.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function dnslinkForm ({
3131
</dd>
3232
</dl>
3333
</label>
34-
<select id="dnslinkPolicy" name='dnslinkPolicy' class="self-center-ns bg-white" onchange=${onDnslinkPolicyChange}>
34+
<select id="dnslinkPolicy" name='dnslinkPolicy' class="self-center-ns bg-white navy" onchange=${onDnslinkPolicyChange}>
3535
<option
3636
value='false'
3737
selected=${String(dnslinkPolicy) === 'false'}>

add-on/src/options/forms/gateways-form.js

+26-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ function gatewaysForm ({
1616
customGatewayUrl,
1717
useCustomGateway,
1818
useSubdomains,
19-
noIntegrationsHostnames,
19+
disabledOn,
20+
enabledOn,
2021
publicGatewayUrl,
2122
publicSubdomainGatewayUrl,
2223
onOptionChange
@@ -26,7 +27,8 @@ function gatewaysForm ({
2627
const onUseSubdomainProxyChange = onOptionChange('useSubdomains')
2728
const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', guiURLString)
2829
const onPublicSubdomainGatewayUrlChange = onOptionChange('publicSubdomainGatewayUrl', guiURLString)
29-
const onNoIntegrationsHostnamesChange = onOptionChange('noIntegrationsHostnames', hostTextToArray)
30+
const onDisabledOnChange = onOptionChange('disabledOn', hostTextToArray)
31+
const onEnabledOnChange = onOptionChange('enabledOn', hostTextToArray)
3032
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
3133
const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded'
3234
const allowChangeOfCustomGateway = ipfsNodeType !== 'embedded:chromesockets'
@@ -132,19 +134,34 @@ function gatewaysForm ({
132134
` : null}
133135
${supportRedirectToCustomGateway ? html`
134136
<div class="flex-row-ns pb0-ns">
135-
<label for="noIntegrationsHostnames">
137+
<label for="disabledOn">
136138
<dl>
137-
<dt>${browser.i18n.getMessage('option_noIntegrationsHostnames_title')}</dt>
138-
<dd>${browser.i18n.getMessage('option_noIntegrationsHostnames_description')}</dd>
139+
<dt>${browser.i18n.getMessage('option_disabledOn_title')}</dt>
140+
<dd>${browser.i18n.getMessage('option_disabledOn_description')}</dd>
139141
</dl>
140142
</label>
141143
<textarea
142144
class="bg-white navy self-center-ns"
143-
id="noIntegrationsHostnames"
145+
id="disabledOn"
144146
spellcheck="false"
145-
onchange=${onNoIntegrationsHostnamesChange}
146-
rows="1"
147-
>${hostArrayToText(noIntegrationsHostnames)}</textarea>
147+
onchange=${onDisabledOnChange}
148+
rows="${disabledOn.length + 1}"
149+
>${hostArrayToText(disabledOn)}</textarea>
150+
</div>
151+
<div class="flex-row-ns pb0-ns">
152+
<label for="enabledOn">
153+
<dl>
154+
<dt>${browser.i18n.getMessage('option_enabledOn_title')}</dt>
155+
<dd>${browser.i18n.getMessage('option_enabledOn_description')}</dd>
156+
</dl>
157+
</label>
158+
<textarea
159+
class="bg-white navy self-center-ns"
160+
id="enabledOn"
161+
spellcheck="false"
162+
onchange=${onEnabledOnChange}
163+
rows="${enabledOn.length + 1}"
164+
>${hostArrayToText(enabledOn)}</textarea>
148165
</div>
149166
` : null}
150167

add-on/src/options/forms/ipfs-node-form.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
2626
</dd>
2727
</dl>
2828
</label>
29-
<select id="ipfsNodeType" name='ipfsNodeType' class="self-center-ns bg-white" onchange=${onIpfsNodeTypeChange}>
29+
<select id="ipfsNodeType" name='ipfsNodeType' class="self-center-ns bg-white navy" onchange=${onIpfsNodeTypeChange}>
3030
<option
3131
value='external'
3232
selected=${ipfsNodeType === 'external'}>
@@ -55,7 +55,12 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
5555
<dd>${browser.i18n.getMessage('option_ipfsNodeConfig_description')}</dd>
5656
</dl>
5757
</label>
58-
<textarea id="ipfsNodeConfig" rows="7" onchange=${onIpfsNodeConfigChange}>${ipfsNodeConfig}</textarea>
58+
<textarea
59+
class="bg-white navy self-center-ns"
60+
spellcheck="false"
61+
id="ipfsNodeConfig"
62+
rows="${(ipfsNodeConfig.match(/\n/g) || []).length + 1}"
63+
onchange=${onIpfsNodeConfigChange}>${ipfsNodeConfig}</textarea>
5964
</div>
6065
` : null}
6166
</fieldset>

add-on/src/options/page.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ module.exports = function optionsPage (state, emit) {
7171
useSubdomains: state.options.useSubdomains,
7272
publicGatewayUrl: state.options.publicGatewayUrl,
7373
publicSubdomainGatewayUrl: state.options.publicSubdomainGatewayUrl,
74-
noIntegrationsHostnames: state.options.noIntegrationsHostnames,
74+
disabledOn: state.options.disabledOn,
75+
enabledOn: state.options.enabledOn,
7576
onOptionChange
7677
})}
7778
${fileImportForm({

add-on/src/popup/browser-action/store.js

+14-11
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ module.exports = (state, emitter) => {
3434
currentTab: null,
3535
currentFqdn: null,
3636
currentDnslinkFqdn: null,
37-
noIntegrationsHostnames: []
37+
enabledOn: [],
38+
disabledOn: []
3839
})
3940

4041
let port
@@ -183,30 +184,32 @@ module.exports = (state, emitter) => {
183184
emitter.emit('render')
184185

185186
try {
186-
let noIntegrationsHostnames = state.noIntegrationsHostnames
187+
let { enabledOn, disabledOn, currentTab, currentDnslinkFqdn, currentFqdn } = state
187188
// if we are on /ipns/fqdn.tld/ then use hostname from DNSLink
188-
const fqdn = state.currentDnslinkFqdn || state.currentFqdn
189-
if (noIntegrationsHostnames.includes(fqdn)) {
190-
noIntegrationsHostnames = noIntegrationsHostnames.filter(host => !host.endsWith(fqdn))
189+
const fqdn = currentDnslinkFqdn || currentFqdn
190+
if (disabledOn.includes(fqdn)) {
191+
disabledOn = disabledOn.filter(host => !host.endsWith(fqdn))
192+
enabledOn.push(fqdn)
191193
} else {
192-
noIntegrationsHostnames.push(fqdn)
194+
enabledOn = enabledOn.filter(host => !host.endsWith(fqdn))
195+
disabledOn.push(fqdn)
193196
}
194197
// console.dir('toggleSiteIntegrations', state)
195-
await browser.storage.local.set({ noIntegrationsHostnames })
198+
await browser.storage.local.set({ disabledOn, enabledOn })
196199

197200
// Reload the current tab to apply updated redirect preference
198-
if (!state.currentDnslinkFqdn || !isIPFS.ipnsUrl(state.currentTab.url)) {
201+
if (!currentDnslinkFqdn || !isIPFS.ipnsUrl(currentTab.url)) {
199202
// No DNSLink, reload URL as-is
200-
await browser.tabs.reload(state.currentTab.id)
203+
await browser.tabs.reload(currentTab.id)
201204
} else {
202205
// DNSLinked websites require URL change
203206
// from http?://gateway.tld/ipns/{fqdn}/some/path OR
204207
// from http?://{fqdn}.ipns.gateway.tld/some/path
205208
// to http://{fqdn}/some/path
206209
// (defaulting to http: https websites will have HSTS or a redirect)
207-
const path = ipfsContentPath(state.currentTab.url, { keepURIParams: true })
210+
const path = ipfsContentPath(currentTab.url, { keepURIParams: true })
208211
const originalUrl = path.replace(/^.*\/ipns\//, 'http://')
209-
await browser.tabs.update(state.currentTab.id, {
212+
await browser.tabs.update(currentTab.id, {
210213
// FF only: loadReplace: true,
211214
url: originalUrl
212215
})

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"precommit": "run-s lint:standard",
4949
"prepush": "run-s clean build lint test",
5050
"chromium": "run-s bundle:chromium && web-ext run --no-reload --target chromium",
51-
"firefox": "run-s bundle:firefox && web-ext run --no-reload --url about:debugging",
51+
"firefox": "run-s bundle:firefox && web-ext run --no-reload --url about:debugging --verbose",
5252
"firefox:nightly": "cross-env PATH=\"./firefox:$PATH\" run-s get-firefox-nightly firefox",
5353
"firefox:beta:add": "faauv --update ci/firefox/update.json ",
5454
"get-firefox-nightly": "shx test -e ./firefox/firefox || get-firefox -b nightly -e",

test/functional/lib/ipfs-proxy/enable-command.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('lib/ipfs-proxy/enable-command', () => {
4444
it('should throw if ALL IPFS integrations are disabled for requested scope', async () => {
4545
const getState = () => initState(optionDefaults, {
4646
ipfsProxy: true,
47-
noIntegrationsHostnames: ['foo.tld']
47+
disabledOn: ['foo.tld']
4848
})
4949
const accessControl = new AccessControl(new Storage())
5050
const getScope = () => 'https://1.foo.tld/path/'

test/functional/lib/ipfs-proxy/pre-acl.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('lib/ipfs-proxy/pre-acl', () => {
3838
it('should throw if ALL IPFS integrations are disabled for requested scope', async () => {
3939
const getState = () => initState(optionDefaults, {
4040
ipfsProxy: true,
41-
noIntegrationsHostnames: ['foo.tld']
41+
disabledOn: ['foo.tld']
4242
})
4343
const accessControl = new AccessControl(new Storage())
4444
const getScope = () => 'https://2.foo.tld/bar/buzz/'

test/functional/lib/state.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,23 @@ describe('state.js', function () {
1818
expect(state.activeIntegrations(undefined)).to.equal(false)
1919
})
2020
it('should return true if host is not on the opt-out list', async function () {
21-
state.noIntegrationsHostnames = ['pl.wikipedia.org']
21+
state.disabledOn = ['pl.wikipedia.org']
2222
const url = 'https://en.wikipedia.org/wiki/Main_Page'
2323
expect(state.activeIntegrations(url)).to.equal(true)
2424
})
2525
it('should return false if host is not on the opt-out list but global toggle is off', async function () {
26-
state.noIntegrationsHostnames = ['pl.wikipedia.org']
26+
state.disabledOn = ['pl.wikipedia.org']
2727
state.active = false
2828
const url = 'https://en.wikipedia.org/wiki/Main_Page'
2929
expect(state.activeIntegrations(url)).to.equal(false)
3030
})
3131
it('should return false if host is on the opt-out list', async function () {
32-
state.noIntegrationsHostnames = ['example.com', 'pl.wikipedia.org']
32+
state.disabledOn = ['example.com', 'pl.wikipedia.org']
3333
const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna'
3434
expect(state.activeIntegrations(url)).to.equal(false)
3535
})
3636
it('should return false if parent host of a subdomain is on the opt-out list', async function () {
37-
state.noIntegrationsHostnames = ['wikipedia.org']
37+
state.disabledOn = ['wikipedia.org']
3838
const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna'
3939
expect(state.activeIntegrations(url)).to.equal(false)
4040
})

0 commit comments

Comments
 (0)