Skip to content

Commit 86f5fcf

Browse files
committed
feat(options): UI for editing redirect opt-outs
This adds a textarea for editing per-site opt-outs. Array is converted into multi-line text. Entries that are not valid FQDNs are dropped. The list is sorted lexicographically.
1 parent 1f939d6 commit 86f5fcf

File tree

9 files changed

+96
-24
lines changed

9 files changed

+96
-24
lines changed

add-on/_locales/en/messages.json

+8
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,14 @@
263263
"message": "Redirect requests for IPFS resources to the Custom gateway",
264264
"description": "An option description on the Preferences screen (option_useCustomGateway_description)"
265265
},
266+
"option_noRedirectHostnames_title": {
267+
"message": "Redirect Opt-Outs",
268+
"description": "An option title on the Preferences screen (option_noRedirectHostnames_title)"
269+
},
270+
"option_noRedirectHostnames_description": {
271+
"message": "List of websites that should not be redirected to the Custom Gateway (includes subresources from other domains). One hostname per line.",
272+
"description": "An option description on the Preferences screen (option_noRedirectHostnames_description)"
273+
},
266274
"option_publicGatewayUrl_title": {
267275
"message": "Default Public Gateway",
268276
"description": "An option title on the Preferences screen (option_publicGatewayUrl_title)"

add-on/src/lib/options.js

+21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const isFQDN = require('is-fqdn')
4+
35
exports.optionDefaults = Object.freeze({
46
active: true, // global ON/OFF switch, overrides everything else
57
ipfsNodeType: 'external', // or 'embedded'
@@ -65,6 +67,25 @@ function normalizeGatewayURL (url) {
6567
exports.normalizeGatewayURL = normalizeGatewayURL
6668
exports.safeURL = (url) => new URL(normalizeGatewayURL(url))
6769

70+
// convert JS array to multiline textarea
71+
function hostArrayToText (array) {
72+
array = array.map(host => host.trim().toLowerCase())
73+
array = [...new Set(array)] // dedup
74+
array = array.filter(Boolean).filter(isFQDN)
75+
array.sort()
76+
return array.join('\n')
77+
}
78+
// convert JS array to multiline textarea
79+
function hostTextToArray (text) {
80+
let array = text.split('\n').map(host => host.trim().toLowerCase())
81+
array = [...new Set(array)] // dedup
82+
array = array.filter(Boolean).filter(isFQDN)
83+
array.sort()
84+
return array
85+
}
86+
exports.hostArrayToText = hostArrayToText
87+
exports.hostTextToArray = hostTextToArray
88+
6889
exports.migrateOptions = async (storage) => {
6990
// <= v2.4.4
7091
// DNSLINK: convert old on/off 'dnslink' flag to text-based 'dnslinkPolicy'

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

+40-21
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
const browser = require('webextension-polyfill')
55
const html = require('choo/html')
6-
const { normalizeGatewayURL } = require('../../lib/options')
6+
const { normalizeGatewayURL, hostTextToArray, hostArrayToText } = require('../../lib/options')
77

88
// Warn about mixed content issues when changing the gateway
99
// https://github.com/ipfs-shipyard/ipfs-companion/issues/648
@@ -13,19 +13,40 @@ function gatewaysForm ({
1313
ipfsNodeType,
1414
customGatewayUrl,
1515
useCustomGateway,
16+
noRedirectHostnames,
1617
publicGatewayUrl,
1718
onOptionChange
1819
}) {
1920
const onCustomGatewayUrlChange = onOptionChange('customGatewayUrl', normalizeGatewayURL)
2021
const onUseCustomGatewayChange = onOptionChange('useCustomGateway')
2122
const onPublicGatewayUrlChange = onOptionChange('publicGatewayUrl', normalizeGatewayURL)
23+
const onNoRedirectHostnamesChange = onOptionChange('noRedirectHostnames', hostTextToArray)
2224
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
25+
const supportRedirectToCustomGateway = ipfsNodeType === 'external'
2326

2427
return html`
2528
<form>
2629
<fieldset>
2730
<legend>${browser.i18n.getMessage('option_header_gateways')}</legend>
28-
${ipfsNodeType === 'external' ? html`
31+
<div>
32+
<label for="publicGatewayUrl">
33+
<dl>
34+
<dt>${browser.i18n.getMessage('option_publicGatewayUrl_title')}</dt>
35+
<dd>${browser.i18n.getMessage('option_publicGatewayUrl_description')}</dd>
36+
</dl>
37+
</label>
38+
<input
39+
id="publicGatewayUrl"
40+
type="url"
41+
inputmode="url"
42+
required
43+
pattern="^https?://[^/]+/?$"
44+
spellcheck="false"
45+
title="Enter URL without any sub-path"
46+
onchange=${onPublicGatewayUrlChange}
47+
value=${publicGatewayUrl} />
48+
</div>
49+
${supportRedirectToCustomGateway ? html`
2950
<div>
3051
<label for="customGatewayUrl">
3152
<dl>
@@ -48,7 +69,7 @@ function gatewaysForm ({
4869
4970
</div>
5071
` : null}
51-
${ipfsNodeType === 'external' ? html`
72+
${supportRedirectToCustomGateway ? html`
5273
<div>
5374
<label for="useCustomGateway">
5475
<dl>
@@ -63,24 +84,22 @@ function gatewaysForm ({
6384
checked=${useCustomGateway} />
6485
</div>
6586
` : null}
66-
<div>
67-
<label for="publicGatewayUrl">
68-
<dl>
69-
<dt>${browser.i18n.getMessage('option_publicGatewayUrl_title')}</dt>
70-
<dd>${browser.i18n.getMessage('option_publicGatewayUrl_description')}</dd>
71-
</dl>
72-
</label>
73-
<input
74-
id="publicGatewayUrl"
75-
type="url"
76-
inputmode="url"
77-
required
78-
pattern="^https?://[^/]+/?$"
79-
spellcheck="false"
80-
title="Enter URL without any sub-path"
81-
onchange=${onPublicGatewayUrlChange}
82-
value=${publicGatewayUrl} />
83-
</div>
87+
${supportRedirectToCustomGateway ? html`
88+
<div>
89+
<label for="noRedirectHostnames">
90+
<dl>
91+
<dt>${browser.i18n.getMessage('option_noRedirectHostnames_title')}</dt>
92+
<dd>${browser.i18n.getMessage('option_noRedirectHostnames_description')}</dd>
93+
</dl>
94+
</label>
95+
<textarea
96+
id="noRedirectHostnames"
97+
spellcheck="false"
98+
onchange=${onNoRedirectHostnamesChange}
99+
rows="4"
100+
>${hostArrayToText(noRedirectHostnames)}</textarea>
101+
</div>
102+
` : null}
84103
</fieldset>
85104
</form>
86105
`

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function ipfsNodeForm ({ ipfsNodeType, ipfsNodeConfig, onOptionChange }) {
4646
<dd>${browser.i18n.getMessage('option_ipfsNodeConfig_description')}</dd>
4747
</dl>
4848
</label>
49-
<textarea id="ipfsNodeConfig" rows="4" onchange=${onIpfsNodeConfigChange}>${ipfsNodeConfig}</textarea>
49+
<textarea id="ipfsNodeConfig" rows="7" onchange=${onIpfsNodeConfigChange}>${ipfsNodeConfig}</textarea>
5050
</div>
5151
` : null}
5252
</fieldset>

add-on/src/options/page.js

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ module.exports = function optionsPage (state, emit) {
6060
customGatewayUrl: state.options.customGatewayUrl,
6161
useCustomGateway: state.options.useCustomGateway,
6262
publicGatewayUrl: state.options.publicGatewayUrl,
63+
noRedirectHostnames: state.options.noRedirectHostnames,
6364
onOptionChange
6465
})}
6566
${state.options.ipfsNodeType === 'external' ? apiForm({

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ module.exports = (state, emitter) => {
184184
} else {
185185
noRedirectHostnames.push(fqdn)
186186
}
187-
console.dir('toggleSiteRedirect', state)
187+
// console.dir('toggleSiteRedirect', state)
188188
await browser.storage.local.set({ noRedirectHostnames })
189189

190190
// Reload the current tab to apply updated redirect preference

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"ipfs-http-response": "0.2.2",
119119
"ipfs-postmsg-proxy": "3.1.1",
120120
"ipfsx": "0.17.0",
121+
"is-fqdn": "1.0.1",
121122
"is-ipfs": "0.4.8",
122123
"is-svg": "3.0.0",
123124
"lru-cache": "5.1.1",

test/functional/lib/options.test.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use strict'
22
const { describe, it, beforeEach, after } = require('mocha')
3+
const { expect } = require('chai')
34
const sinon = require('sinon')
45
const browser = require('sinon-chrome')
5-
const { storeMissingOptions, optionDefaults } = require('../../../add-on/src/lib/options')
6+
const { storeMissingOptions, optionDefaults, hostTextToArray, hostArrayToText } = require('../../../add-on/src/lib/options')
67

78
describe('storeMissingOptions()', function () {
89
beforeEach(() => {
@@ -66,3 +67,19 @@ describe('storeMissingOptions()', function () {
6667
browser.flush()
6768
})
6869
})
70+
71+
describe('hostTextToArray()', function () {
72+
it('should sort, dedup hostnames, drop non-FQDNs and produce an array', () => {
73+
const text = `zombo.com\n two.com \n totally not a FQDN \none.pl \nTWO.com\n\n`
74+
const array = ['one.pl', 'two.com', 'zombo.com']
75+
expect(hostTextToArray(text)).to.be.an('array').to.have.ordered.members(array)
76+
})
77+
})
78+
79+
describe('hostArrayToText()', function () {
80+
it('should sort, deduplicate, drop non-FQDNs and produce multiline string', () => {
81+
const array = ['zombo.com ', 'two.com ', 'ONE.pl ', 'one.pl', 'totall not a FQDN', 'zombo.com']
82+
const text = `one.pl\ntwo.com\nzombo.com`
83+
expect(hostArrayToText(array)).to.be.a('string').equal(text)
84+
})
85+
})

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -6664,6 +6664,11 @@ is-finite@^1.0.0:
66646664
dependencies:
66656665
number-is-nan "^1.0.0"
66666666

6667+
6668+
version "1.0.1"
6669+
resolved "https://registry.yarnpkg.com/is-fqdn/-/is-fqdn-1.0.1.tgz#f3ed9cd5a20238449ae510e10d81258dafca9b70"
6670+
integrity sha1-8+2c1aICOESa5RDhDYElja/Km3A=
6671+
66676672
is-fullwidth-code-point@^1.0.0:
66686673
version "1.0.0"
66696674
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"

0 commit comments

Comments
 (0)