text:
'use strict'
import { describe, it, beforeEach, after } from 'mocha'
import { expect } from 'chai'
import sinon from 'sinon'
import browser from 'sinon-chrome'
import { storeMissingOptions, optionDefaults, isHostname, hostTextToArray, hostArrayToText } from '../../../add-on/src/lib/options.js'
import { URL } from 'url'

describe('storeMissingOptions()', function () {
  beforeEach(() => {
    global.URL = URL
    browser.runtime.id = 'testid'
    browser.flush()
  })

  it('should save all defaults during first run when no value is present in storage', done => {
    browser.storage.local.get.returns(Promise.resolve({})) // simulates empty user storage (clean install)
    browser.storage.local.set.returns(Promise.resolve({}))
    const read = Object.assign({}, optionDefaults)
    storeMissingOptions(read, optionDefaults, browser.storage.local)
      .then(changes => {
        sinon.assert.calledWith(browser.storage.local.set, changes)
        expect(changes).to.be.deep.equal(optionDefaults)
        done()
      })
      .catch(error => { done(error) })
  })
  it('should not touch non-default value present in storage', done => {
    const read = Object.assign({}, optionDefaults)
    const userModifiedKey = 'linkify'
    read[userModifiedKey] = !optionDefaults[userModifiedKey] // simulate custom option set by user
    browser.storage.local.get.returns(Promise.resolve(read)) // simulate existing user data read from storage
    browser.storage.local.set.returns(Promise.resolve({}))
    storeMissingOptions(read, optionDefaults, browser.storage.local)
      .then(changes => {
        sinon.assert.calledWith(browser.storage.local.set, changes)
        expect(changes).to.be.deep.equal({})
        done()
      })
      .catch(error => { done(error) })
  })

  it('should initialize default value for key missing from both read options and storage', done => {
    const read = Object.assign({}, optionDefaults)
    const userMissingKey = 'customGatewayUrl'
    delete read[userMissingKey] // simulate the key not being in storage (eg. after extension update)
    const expectedChanges = {}
    expectedChanges[userMissingKey] = optionDefaults[userMissingKey]
    browser.storage.local.get.returns(Promise.resolve(read)) // simulate existing user data read from storage
    browser.storage.local.set.returns(Promise.resolve({}))
    storeMissingOptions(read, optionDefaults, browser.storage.local)
      .then(changes => {
        sinon.assert.calledWith(browser.storage.local.set, changes)
        expect(changes).to.be.deep.equal(expectedChanges)
        done()
      })
      .catch(error => { done(error) })
  })

  after(() => {
    browser.flush()
  })
})

describe('isHostname()', function () {
  it('should return false for invalid URL.hostname', () => {
    expect(isHostname('random text with whitespaces')).to.equal(false)
  })
  it('should return true for a valid URL.hostname (FQDN)', () => {
    expect(isHostname('example.com')).to.equal(true)
  })
  it('should return true for a valid URL.hostname (ipv4)', () => {
    expect(isHostname('192.168.1.1')).to.equal(true)
  })
  it('should return true for valid URL.hostname (ipv6 in brackets)', () => {
    expect(isHostname('[fe80::bb67:770c:8a97:1]')).to.equal(true)
  })
  it('should return false for invalid URL.hostname (ipv6 without brackets)', () => {
    expect(isHostname('fe80::bb67:770c:8a97:1')).to.equal(false)
  })
  it('should return false for ipv6 with a missing bracket', () => {
    expect(
      isHostname('[fe80::bb67:770c:8a97:1') ||
      isHostname('fe80::bb67:770c:8a97:1]')
    ).to.equal(false)
  })
  it('should return false for ipv6 with malformed brackets', () => {
    expect(isHostname('[fe80::bb67:770c:8a97]:1]')).to.equal(false)
  })
})

describe('hostTextToArray()', function () {
  it('should sort, dedup hostnames, drop non-FQDNs and produce an array', () => {
    const text = 'zombo.com\n TwO.com \n 192.168.1.1:58080 \n192.168.1.2\n[fe80::bb67:770c:8a97:1]:58080\nfe80::bb67:770c:8a97:2\n[fe80::bb67:770c:8a97:3]\n totally not a FQDN \none.pl \nTWO.com\n\n'
    const array = ['192.168.1.1', '192.168.1.2', '[fe80::bb67:770c:8a97:1]', '[fe80::bb67:770c:8a97:2]', '[fe80::bb67:770c:8a97:3]', 'one.pl', 'two.com', 'zombo.com']
    expect(hostTextToArray(text)).to.be.an('array').to.have.ordered.members(array)
  })
})

describe('hostArrayToText()', function () {
  it('should sort, deduplicate, drop non-FQDNs and produce multiline string', () => {
    const array = ['zombo.com ', 'two.com  ', 'ONE.pl ', 'one.pl', 'totall not a FQDN', 'zombo.com']
    const text = 'one.pl\ntwo.com\nzombo.com'
    expect(hostArrayToText(array)).to.be.a('string').equal(text)
  })
})


text:
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import browser from 'sinon-chrome'
import AbortController from 'abort-controller'
import { URL } from 'url'
import { optionDefaults } from '../../../add-on/src/lib/options.js'
browser.runtime.id = 'testid'
global.browser = browser
global.AbortController = AbortController
global.chrome = browser
global.navigator = {
  clipboard: {
    writeText: () => {}
  }
}
// We need to do this because global is not mapped otherwise, we need to stub browser and chrome runtime
// so that the webextension-polyfill does not complain about the test runner not being a browser instance.
const init = async () => (await import('../../../add-on/src/lib/ipfs-companion.js')).default()

describe('lib/ipfs-companion.js', function () {
  describe('init', function () {
    before(function () {
      global.localStorage = global.localStorage || {}
      global.URL = global.URL || URL
      global.screen = { width: 1024, height: 720 }

      browser.runtime.getManifest.returns({ version: '0.0.0' }) // on-installed.js
    })

    it('should query local storage for options with hardcoded defaults for fallback', async function () {
      this.timeout(10000)
      browser.storage.local.get.resolves(optionDefaults)
      browser.storage.local.set.resolves()
      const ipfsCompanion = await init()
      expect(browser.storage.local.get.calledWith(optionDefaults)).to.equal(true)
      return await ipfsCompanion.destroy()
    })

    after(function () {
      browser.flush()
    })
  })

  describe.skip('onStorageChange()', function () {
    before(function () {
      global.window = {}
      global.browser = browser
      global.URL = URL
    })

    it('should update ipfs API instance on IPFS API URL change', async function () {
      browser.storage.local.get.resolves(optionDefaults)
      browser.storage.local.set.resolves()
      browser.browserAction.setBadgeBackgroundColor.resolves()
      browser.browserAction.setBadgeText.resolves()
      browser.browserAction.setIcon.resolves()
      browser.tabs.query.resolves([{ id: 'TEST' }])
      browser.contextMenus.update.resolves()
      browser.idle.queryState.resolves('active')

      const ipfsCompanion = await init()

      const oldIpfsApiUrl = 'http://127.0.0.1:5001'
      const newIpfsApiUrl = 'http://1.2.3.4:8080'
      const changes = { ipfsApiUrl: { oldValue: oldIpfsApiUrl, newValue: newIpfsApiUrl } }
      const area = 'local'
      const ipfs = global.window.ipfs
      browser.storage.onChanged.dispatch(changes, area)
      expect(ipfs).to.not.equal(window.ipfs)
      return ipfsCompanion.destroy()
    })

    after(function () {
      browser.flush()
    })
  })
})

repo_name: ipfs/ipfs-companion

repo_language: JavaScript

mime_type: text/x-diff


text:
'use strict'
import { describe, it, before, beforeEach, after } from 'mocha'
import { expect, assert } from 'chai'
import { URL } from 'url' // URL implementation with support for .origin attribute
import browser from 'sinon-chrome'
import { initState } from '../../../add-on/src/lib/state.js'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
import { createRequestModifier } from '../../../add-on/src/lib/ipfs-request.js'
import createDNSLinkResolver from '../../../add-on/src/lib/dnslink.js'
import { createIpfsPathValidator } from '../../../add-on/src/lib/ipfs-path.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'
import { braveNodeType } from '../../../add-on/src/lib/ipfs-client/brave.js'
import { spoofDnsTxtRecord } from './dnslink.test.js'

describe('modifyRequest processing', function () {
  let state, getState, dnslinkResolver, ipfsPathValidator, modifyRequest, runtime

  before(function () {
    global.URL = URL
    global.browser = browser
    browser.runtime.id = 'testid'
  })

  beforeEach(async function () {
    browser.runtime.getURL.flush()
    state = initState(optionDefaults)
    getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDNSLinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  // Additional handling is required for redirected IPFS subresources on regular HTTPS pages
  // (eg. image embedded from public gateway on HTTPS website)
  describe('a subresource request on HTTPS website', function () {
    const cid = 'QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR'
    it('should be routed to "127.0.0.1" gw in Chromium if type is image', function () {
      runtime.isFirefox = false
      const request = {
        method: 'GET',
        type: 'image',
        url: `https://ipfs.io/ipfs/${cid}`,
        initiator: 'https://some-website.example.com' // Chromium
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
    })
    it('should be routed to "localhost" gw in Chromium if not a subresource', function () {
      runtime.isFirefox = false
      const request = {
        method: 'GET',
        type: 'main_frame',
        url: `https://ipfs.io/ipfs/${cid}`,
        initiator: 'https://some-website.example.com' // Chromium
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://localhost:8080/ipfs/${cid}`)
    })
    it('should be routed to "127.0.0.1" gw to avoid mixed content warning in Firefox', function () {
      runtime.isFirefox = true
      const request = {
        method: 'GET',
        type: 'image',
        url: `https://ipfs.io/ipfs/${cid}`,
        originUrl: 'https://some-website.example.com/some/page.html' // FF only
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://127.0.0.1:8080/ipfs/${cid}`)
    })
    it('should be routed to "localhost" gw in Firefox if not a subresource', function () {
      runtime.isFirefox = true
      const request = {
        method: 'GET',
        type: 'main_frame',
        url: `https://ipfs.io/ipfs/${cid}`,
        originUrl: 'https://some-website.example.com/some/page.html' // FF only
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://localhost:8080/ipfs/${cid}`)
    })
  })

  describe('a request to <apiURL>/api/v0/add with stream-channels=true', function () {
    const expectHeader = { name: 'Expect', value: '100-continue' }
    it('should apply the "Expect: 100-continue" fix for https://github.com/ipfs/go-ipfs/issues/5168 ', function () {
      const request = {
        method: 'POST',
        requestHeaders: [
          { name: 'Content-Type', value: 'multipart/form-data; boundary=--------------------------015695779813832455524979' }
        ],
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/add?progress=true&wrapWithDirectory=true&pin=true&wrap-with-directory=true&stream-channels=true`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders).to.deep.include(expectHeader)
    })
  })

  // The Origin header set by browser for requests coming from within a browser
  // extension has been a mess for years, and by now we simply have zero trust
  // in stability of this header. Instead, we use WebExtension's webRequest API
  // to tell if a request comes from our browser extension and manually set
  // Origin to look like a request coming from the same Origin as IPFS API.
  //
  // The full context can be found in ipfs-request.js, where isCompanionRequest
  // check is executed.
  describe('Origin header in a request to <apiURL>/api/v0/', function () {
    it('set to API if request comes from Companion in Firefox <85', async function () {
      // Context: Firefox 65 started setting this header
      // set vendor-specific Origin for WebExtension context
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originalOriginHeader = { name: 'Origin', value: 'moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [originalOriginHeader],
        originUrl: 'moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/path/to/background.html', // FF specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('set to API if request comes from Companion in Firefox 85', async function () {
      // Context: https://github.com/ipfs-shipyard/ipfs-companion/issues/955#issuecomment-753413988
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originalOriginHeader = { name: 'Origin', value: 'null' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [originalOriginHeader],
        originUrl: 'moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/path/to/background.html', // FF specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('set to API if request comes from Companion in Chromium <72', async function () {
      // set vendor-specific Origin for WebExtension context
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const bogusOriginHeader = { name: 'Origin', value: 'null' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [bogusOriginHeader],
        initiator: 'chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('set to API if request comes from Companion in Chromium 72', async function () {
      // Context: Chromium 72 started setting this header to chrome-extension:// URI
      // set vendor-specific Origin for WebExtension context
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const bogusOriginHeader = { name: 'Origin', value: 'chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch' }
      const apiOriginHeader = { name: 'Origin', value: getState().apiURL.origin }
      const request = {
        requestHeaders: [bogusOriginHeader],
        initiator: 'chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(apiOriginHeader)
    })

    it('keep Origin as-is if request does not come from Companion (Chromium)', async function () {
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const expectedOriginHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const request = {
        requestHeaders: [originHeader],
        initiator: 'https://some.website.example.com', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })

    it('keep Origin as-is if request does not come from Companion (Firefox)', async function () {
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const originHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const expectedOriginHeader = { name: 'Origin', value: 'https://some.website.example.com' }
      const request = {
        requestHeaders: [originHeader],
        originUrl: 'https://some.website.example.com/some/path.html', // Firefox specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })

    it('keep the "Origin: null" if request does not come from Companion (Chromium)', async function () {
      // Presence of Origin header is important as it protects API from XSS via sandboxed iframe
      // NOTE: Chromium <72 was setting this header in requests sent by browser extension,
      // but they fixed it since then, and we switched to reading origin via webRequest API,
      // which is independent from the HTTP header.
      browser.runtime.getURL.withArgs('/').returns('chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const nullOriginHeader = { name: 'Origin', value: 'null' }
      const expectedOriginHeader = { name: 'Origin', value: 'null' }
      const request = {
        requestHeaders: [nullOriginHeader],
        initiator: 'https://random.website.example.com', // Chromium specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })

    it('keep the "Origin: null" if request does not come from Companion (Firefox)', async function () {
      // Presence of Origin header is important as it protects API from XSS via sandboxed iframe
      browser.runtime.getURL.withArgs('/').returns('moz-extension://0f334731-19e3-42f8-85e2-03dbf50026df/')
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const nullOriginHeader = { name: 'Origin', value: 'null' }
      const expectedOriginHeader = { name: 'Origin', value: 'null' }
      const request = {
        requestHeaders: [nullOriginHeader],
        originUrl: 'https://random.website.example.com/some/path.html', // Firefox specific WebExtension API
        type: 'xmlhttprequest',
        url: `${state.apiURLString}api/v0/id`
      }
      modifyRequest.onBeforeRequest(request) // executes before onBeforeSendHeaders, may mutate state
      expect(modifyRequest.onBeforeSendHeaders(request).requestHeaders)
        .to.deep.include(expectedOriginHeader)
    })
  })

  // We've moved blog to blog.ipfs.io but links to ipfs.io/blog/*
  // are still around due to the way Discourse integration was done for comments.
  // https://github.com/ipfs/blog/issues/360
  describe('a failed main_frame request to /ipns/ipfs.io/blog', function () {
    it('should be updated to /ipns/blog.ipfs.io', async function () {
      const brokenDNSLinkUrl = 'http://example.com/ipns/ipfs.io/blog/some-post'
      const fixedDNSLinkUrl = 'http://example.com/ipns/blog.ipfs.io/some-post'
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const request = {
        tabId: 42404,
        statusCode: 404,
        type: 'main_frame',
        url: brokenDNSLinkUrl
      }
      browser.tabs.update.flush()
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: fixedDNSLinkUrl }).notCalled)
      modifyRequest.onCompleted(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: fixedDNSLinkUrl }).calledOnce)
      browser.tabs.update.flush()
    })
  })

  // Brave seems to ignore redirect to ipfs:// and ipns://, but if we force tab update via tabs API,
  // then address bar is correct
  describe('redirect of main_frame request to local gateway when Brave node is used', function () {
    it('should force native URI in address bar via tabs.update API', async function () {
      const httpDNSLinkUrl = 'https://example.com/ipns/docs.ipfs.io/some/path?query=val'
      const nativeDNSLinkUri = 'ipns://docs.ipfs.io/some/path?query=val'
      spoofDnsTxtRecord('docs.ipfs.io', dnslinkResolver, '/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss')
      state.ipfsNodeType = braveNodeType
      // ensure clean modifyRequest
      runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
      modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
      // test
      const request = {
        tabId: 42,
        type: 'main_frame',
        url: httpDNSLinkUrl
      }
      browser.tabs.update.flush()
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: nativeDNSLinkUri }).notCalled)
      await modifyRequest.onBeforeRequest(request)
      assert.ok(browser.tabs.update.withArgs(request.tabId, { url: nativeDNSLinkUri }).calledOnce)
      browser.tabs.update.flush()
    })
  })

  // https://github.com/ipfs-shipyard/ipfs-companion/issues/962
  describe('redirect of IPFS resource to local gateway in Brave', function () {
    it('should be redirected if not a  subresource (not impacted by Brave Shields)', function () {
      runtime.isFirefox = false
      runtime.brave = { thisIsFakeBraveRuntime: true }
      const request = {
        method: 'GET',
        type: 'image',
        url: 'https://ipfs.io/ipfs/bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss',
        initiator: 'https://some-website.example.com' // Brave (built on Chromium)
      }
      expect(modifyRequest.onBeforeRequest(request))
        .to.equal(undefined)
    })
    it('should be left untouched if subresource (would be blocked by Brave Shields)', function () {
      runtime.isFirefox = false
      runtime.brave = { thisIsFakeBraveRuntime: true }
      const cid = 'bafkqae2xmvwgg33nmuqhi3zajfiemuzahiwss'
      const request = {
        method: 'GET',
        type: 'main_frame',
        url: `https://ipfs.io/ipfs/${cid}`,
        initiator: 'https://some-website.example.com' // Brave (built on Chromium)
      }
      expect(modifyRequest.onBeforeRequest(request).redirectUrl)
        .to.equal(`http://localhost:8080/ipfs/${cid}`)
    })
  })

  after(function () {
    delete global.URL
    delete global.browser
    browser.flush()
  })
})


text:
'use strict'
import { describe, it, before, beforeEach, after } from 'mocha'
import sinon from 'sinon'
import { expect } from 'chai'
import { URL } from 'url'
import browser from 'sinon-chrome'
import { initState } from '../../../add-on/src/lib/state.js'
import createRuntimeChecks from '../../../add-on/src/lib/runtime-checks.js'
import { createRequestModifier } from '../../../add-on/src/lib/ipfs-request.js'
import createDnslinkResolver from '../../../add-on/src/lib/dnslink.js'
import { createIpfsPathValidator } from '../../../add-on/src/lib/ipfs-path.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

const url2request = (string) => {
  return { url: string, type: 'main_frame' }
}

const fakeRequestId = () => {
  return Math.floor(Math.random() * 100000).toString()
}

// const nodeTypes = ['external', 'embedded']

describe('modifyRequest processing of DNSLinks', function () {
  let state, dnslinkResolver, ipfsPathValidator, modifyRequest, runtime

  before(function () {
    global.URL = URL
    global.browser = browser
    browser.runtime.id = 'testid'
  })

  beforeEach(async function () {
    state = Object.assign(initState(optionDefaults), {
      ipfsNodeType: 'external',
      peerCount: 1,
      redirect: true,
      catchUnhandledProtocols: true,
      gwURLString: 'http://127.0.0.1:8080',
      pubGwURLString: 'https://ipfs.io'
    })
    const getState = () => state
    const getIpfs = () => {}
    dnslinkResolver = createDnslinkResolver(getState)
    runtime = Object.assign({}, await createRuntimeChecks(browser)) // make it mutable for tests
    ipfsPathValidator = createIpfsPathValidator(getState, getIpfs, dnslinkResolver)
    modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
  })

  describe('a request to FQDN with dnslinkPolicy=false', function () {
    let activeGateway
    beforeEach(function () {
      // Ensure the eager dnslinkPolicy is on (dns txt lookup for every request)
      state.dnslinkPolicy = false
      // API is online and has one peer
      state.peerCount = 1
      // embedded node (js-ipfs) defaults to public gw
      activeGateway = (state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString)
      // clear dnslink cache to ensure DNS TXT record lookup is triggered
      dnslinkResolver.clearCache()
    })
    it('should do nothing if DNS TXT record exists', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should do nothing if dnslink for FQDN is in cache', function () {
      // stub existence of a positive DNSLink cache hit
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should do nothing if DNS TXT record exists and dnslink is in cache', function () {
      // stub existence of a valid DNS record and cache
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should do nothing if DNS TXT record exists and x-ipfs-path is disabled', function () {
      // enable detection of x-ipfs-path to ensure it is not enough without dnslinkPolicy=detectIpfsPathHeader
      state.detectIpfsPathHeader = false
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipns/this-should-be-ignored.io')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // simulate presence of x-ipfs-path header returned by HTTP gateway
      request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipns/value-from-x-ipfs-path.io' }]
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
    it('should ignore DNS TXT record and use /ipfs/ path from x-ipfs-path if both are present', function () {
      // enable detection of x-ipfs-path to ensure it is not enough without dnslinkPolicy=detectIpfsPathHeader
      state.detectIpfsPathHeader = true
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipns/this-should-be-ignored.io')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // simulate presence of x-ipfs-path header returned by HTTP gateway
      request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
      // onHeadersReceived should redirect to value from X-Ipfs-Path
      expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(activeGateway + '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd?argTest#hashTest')
    })
    it('should ignore DNS TXT record and also ignore /ipns/ path from x-ipfs-path if both are present', function () {
      // enable detection of x-ipfs-path to ensure it is not enough without dnslinkPolicy=detectIpfsPathHeader
      state.detectIpfsPathHeader = true
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipns/this-should-be-ignored.io')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
      // simulate presence of x-ipfs-path header returned by HTTP gateway
      request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipns/value-from-x-ipfs-path-to-ignore.io' }]
      // onHeadersReceived should ignore /ipns/ from x-ipfs-path because dnslink is disabled in preferences
      // and redirect would confuse users
      expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
    })
  })

  describe('a request to FQDN with dnslinkPolicy=enabled', function () {
    let activeGateway
    beforeEach(function () {
      // Ensure the eager dnslinkPolicy is on (dns txt lookup for every request)
      state.dnslinkPolicy = 'enabled'
      // disable detection of x-ipfs-path to ensure isolated test
      // TODO: create separate 'describe' section  for detectIpfsPathHeader==true
      state.detectIpfsPathHeader = false
      // API is online and has one peer
      state.peerCount = 1
      // embedded node (js-ipfs) defaults to public gw
      activeGateway = (state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString)
      // clear dnslink cache to ensure DNS TXT record lookup is triggered
      dnslinkResolver.clearCache()
    })
    it('should redirect in onBeforeRequest if dnslink exists', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
      expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should redirect in onBeforeRequest if DNS TXT record exists, XHR is cross-origin and runtime is Chromium', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      runtime.isFirefox = false
      // Chrome uses 'initiator' for origin
      const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should redirect in onBeforeRequest if dnslink exists, XHR is cross-origin and runtime is Firefox', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      runtime.isFirefox = true
      // Firefox uses 'originUrl' for origin
      const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should redirect later in onHeadersReceived if dnslink exists, XHR is cross-origin and runtime is Firefox <69', function () {
      // stub existence of a valid DNS record
      const fqdn = 'explore.ipld.io'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
      //
      // Context for CORS XHR problems in Firefox <69: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
      runtime.requiresXHRCORSfix = true
      // Firefox uses 'originUrl' for origin
      const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      // onBeforeRequest should not change anything, as it will trigger false-positive CORS error
      expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
      // onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
      expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
    })
    it('should do nothing if dnslink does not exist and XHR is cross-origin in Firefox', function () {
      // stub no dnslink
      const fqdn = 'youtube.com'
      dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns(undefined)
      // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
      runtime.isFirefox = true
      // Firefox uses 'originUrl' for origin
      const xhrRequest = { url: 'https://youtube.com/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
      // onBeforeRequest should not change anything
      expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
      // onHeadersReceived should not change anything
      expect(modifyRequest.onHeadersReceived(xhrRequest)).to.equal(undefined)
    })
  })

  describe('a request to FQDN with dnslinkPolicy=detectIpfsPathHeader', function () {
    let activeGateway
    beforeEach(function () {
      // Enable the eager dnslinkPolicy (dns txt lookup for every request)
      state.dnslinkPolicy = 'detectIpfsPathHeader'
      // disable detection of x-ipfs-path to ensure isolated test
      // TODO: create separate 'describe' section  for detectIpfsPathHeader==true
      state.detectIpfsPathHeader = true
      // API is online and has one peer
      state.peerCount = 1
      // embedded node (js-ipfs) defaults to public gw
      activeGateway = (state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString)
    })
    describe('and dnslink cache miss', function () {
      beforeEach(function () {
        // force-clear dnslink cache to enable cache miss
        dnslinkResolver.clearCache()
      })
      it('should redirect subrequests in onHeadersReceived if DNS TXT record exists and x-ipfs-path header is present', function () {
        // clear dnslink cache to ensure miss
        dnslinkResolver.clearCache()
        // stub existence of a valid DNS record
        const fqdn = 'explore.ipld.io'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        request.type = 'sub_frame' // we test a subrequests because main_frame gets early DNSLink preload in onBeforeRequest
        expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        // simulate presence of x-ipfs-path header returned by HTTP gateway
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
        // DNSLink is present, so we ignore hash from X-Ipfs-Path header and redirect to nice /ipns/ address
        expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
      })
      it('should redirect in onHeadersReceived if DNS TXT record is missing but x-ipfs-path header is present', function () {
        // clear dnslink cache to ensure miss
        dnslinkResolver.clearCache()
        // stub lack of DNS record
        const fqdn = 'explore.ipld.io'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns(false)
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        request.type = 'sub_frame' // we test a subrequest here because main_frame gets early DNSLink preload in onBeforeRequest
        expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        // simulate presence of x-ipfs-path header returned by HTTP gateway
        request.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
        // Note that DNSLink is missing, so a path from x-ipfs-path is used
        expect(modifyRequest.onHeadersReceived(request).redirectUrl).to.equal(activeGateway + '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd?argTest#hashTest')
      })
      it('should do nothing if DNS TXT record exists but there is no x-ipfs-path header', function () {
        // clear dnslink cache to ensure miss
        dnslinkResolver.clearCache()
        // stub existence of a valid DNS record
        const fqdn = 'explore.ipld.io'
        dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        request.type = 'sub_frame' // we test a subrequest here because main_frame gets early DNSLink preload in onBeforeRequest
        expect(modifyRequest.onBeforeRequest(request)).to.equal(undefined)
        expect(modifyRequest.onHeadersReceived(request)).to.equal(undefined)
      })
      describe('(XHR CORS scenario)', function () {
        // Test makes more sense for dnslinkPolicy=enabled, but we keep it here for completeness
        it('should redirect in onHeadersReceived if XHR is cross-origin and runtime is not Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          runtime.isFirefox = false
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          xhrRequest.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
          expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        // Test makes more sense for dnslinkPolicy=enabled, but we keep it here for completeness
        it('should redirect in onHeadersReceived if XHR is cross-origin and runtime is Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns('/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything, as it will trigger false-positive CORS error
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
          xhrRequest.responseHeaders = [{ name: 'X-Ipfs-Path', value: '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd' }]
          expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        it('should redirect later in onHeadersReceived if XHR is cross-origin and runtime is Firefox <69', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          // Context for CORS XHR problems in Firefox <69: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.requiresXHRCORSfix = true
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything, as it will trigger false-positive CORS error
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived is after CORS validation happens, so its ok to cancel and redirect late
          expect(modifyRequest.onHeadersReceived(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        // Test makes more sense for dnslinkPolicy=enabled, but we keep it here for completeness
        it('should do nothing if DNS TXT record is missing and XHR is cross-origin in Firefox', function () {
          // stub no dnslink
          const fqdn = 'youtube.com'
          dnslinkResolver.readDnslinkFromTxtRecord = sinon.stub().withArgs(fqdn).returns(false)
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'https://youtube.com/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived should not change anything
          expect(modifyRequest.onHeadersReceived(xhrRequest)).to.equal(undefined)
        })
      })
    })
    describe('and dnslink cache hit', function () {
      it('should redirect in onBeforeRequest', function () {
        // stub existence of a valid DNS record
        const fqdn = 'explore.ipld.io'
        // manually add item to cache to ensure cache hit
        dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
        //
        const request = url2request('http://explore.ipld.io/index.html?argTest#hashTest')
        expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
      })
      describe('(XHR CORS scenario)', function () {
        it('should redirect in onBeforeRequest if XHR is cross-origin and runtime is not Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          runtime.isFirefox = false
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', initiator: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        it('should redirect in onBeforeRequest if XHR is cross-origin and runtime is Firefox', function () {
          // stub existence of a valid DNS record
          const fqdn = 'explore.ipld.io'
          dnslinkResolver.setDnslink(fqdn, '/ipfs/QmbfimSwTuCvGL8XBr3yk1iCjqgk2co2n21cWmcQohymDd')
          //
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'http://explore.ipld.io/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          expect(modifyRequest.onBeforeRequest(xhrRequest).redirectUrl).to.equal(activeGateway + '/ipns/explore.ipld.io/index.html?argTest#hashTest')
        })
        it('should do nothing if DNS TXT record is missing and XHR is cross-origin in Firefox', function () {
          // stub cached info about lack of dnslink
          const fqdn = 'youtube.com'
          dnslinkResolver.setDnslink(fqdn, false)
          // Context for CORS XHR problems in Firefox: https://github.com/ipfs-shipyard/ipfs-companion/issues/436
          runtime.isFirefox = true
          const xhrRequest = { url: 'https://youtube.com/index.html?argTest#hashTest', type: 'xmlhttprequest', originUrl: 'https://www.nasa.gov/foo.html', requestId: fakeRequestId() }
          // onBeforeRequest should not change anything
          expect(modifyRequest.onBeforeRequest(xhrRequest)).to.equal(undefined)
          // onHeadersReceived should not change anything
          expect(modifyRequest.onHeadersReceived(xhrRequest)).to.equal(undefined)
        })
      })
    })
  })

  after(function () {
    delete global.URL
    delete global.browser
    browser.flush()
  })
})


text:
'use strict'
import { describe, it, beforeEach } from 'mocha'
import { expect } from 'chai'
import { URL } from 'url'
import { initState } from '../../../add-on/src/lib/state.js'
import { optionDefaults } from '../../../add-on/src/lib/options.js'

describe('state.js', function () {
  let state

  beforeEach(function () {
    global.URL = URL
    state = Object.assign(initState(optionDefaults), { peerCount: 1 })
  })

  describe('activeIntegrations(url)', function () {
    it('should return false if input is undefined', async function () {
      expect(state.activeIntegrations(undefined)).to.equal(false)
    })
    it('should return true if host is not on the opt-out list', async function () {
      state.disabledOn = ['pl.wikipedia.org']
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(true)
    })
    it('should return true if parent host is on the opt-out list, but a direct match exist on opt-in one', async function () {
      state.disabledOn = ['wikipedia.org']
      state.enabledOn = ['en.wikipedia.org']
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(true)
    })
    it('should return false if parent host is on the opt-in list, but a direct match exist on opt-out one', async function () {
      state.disabledOn = ['en.wikipedia.org']
      state.enabledOn = ['wikipedia.org']
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return true if direct match host is on both opt-in and opt-out lists', async function () {
      state.disabledOn = ['example.com']
      state.enabledOn = ['example.com']
      const url = 'https://example.com/path'
      expect(state.activeIntegrations(url)).to.equal(true)
    })
    it('should return false if parent host is on both opt-in and opt-out lists', async function () {
      state.disabledOn = ['example.com']
      state.enabledOn = ['example.com']
      const url = 'https://subdomain.example.com/path'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return false if host is not on the opt-out list but global toggle is off', async function () {
      state.disabledOn = ['pl.wikipedia.org']
      state.active = false
      const url = 'https://en.wikipedia.org/wiki/Main_Page'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return false if host is on the opt-out list', async function () {
      state.disabledOn = ['example.com', 'pl.wikipedia.org']
      const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
    it('should return false if parent host of a subdomain is on the opt-out list', async function () {
      state.disabledOn = ['wikipedia.org']
      const url = 'https://pl.wikipedia.org/wiki/Wikipedia:Strona_g%C5%82%C3%B3wna'
      expect(state.activeIntegrations(url)).to.equal(false)
    })
  })
})


text:
'use strict'
import { describe, it, before, beforeEach } from 'mocha'
import browser from 'sinon-chrome'
import sinon from 'sinon'

// Units to be tested.
import {
  prepareReloadExtensions,
  InternalTabReloader,
  LocalGatewayReloader,
  WebUiReloader
} from '../../../../../add-on/src/lib/ipfs-client/reloaders/index.js'

const CUSTOM_GATEWAY_URL = {
  customGatewayUrl: 'http://127.0.0.1:8080'
}

const logger = sinon.spy()

describe('Reloader Helper Method: prepareReloadExtensions', function () {
  it('Prepares the reloader extensions', async function () {
    browser.runtime.id = 'testid'
    browser.storage.local.get.resolves(CUSTOM_GATEWAY_URL)
    browser.runtime.getURL.returns('chrome-extension://<some_id>')
    await prepareReloadExtensions([WebUiReloader, InternalTabReloader, LocalGatewayReloader], browser, logger)
    sinon.assert.callCount(logger, 3)
  })
})

describe('Reloaders', function () {
  before(() => {
    global.browser = browser
    browser.runtime.id = 'testid'
    global.chrome = {}
  })

  beforeEach(function () {
    browser.flush()
    logger.resetHistory()
  })

  describe('WebUiReloader', function () {
    let webUiReloader
    beforeEach(async function () {
      webUiReloader = new WebUiReloader(browser, logger)
      await webUiReloader.init()
    })

    it('should initialize', function () {
      sinon.assert.calledWith(logger, 'Initialized without additional config.')
    })

    it('should handle webUi reloading', function () {
      const tabs = [{
        url: 'http://some-url.com',
        id: 1
      }, {
        url: 'ipfs://some-ipfs-server.tld/webui/index.html#/',
        id: 2
      }]

      webUiReloader.reload(tabs)
      sinon.assert.calledWith(browser.tabs.reload, 2)
      sinon.assert.calledWith(logger, 'reloading webui at ipfs://some-ipfs-server.tld/webui/index.html#/')
    })
  })

  describe('InternalTabReloader', function () {
    let internalTabReloader
    beforeEach(async function () {
      browser.runtime.getURL.returns('chrome-extension://<some_id>')
      internalTabReloader = new InternalTabReloader(browser, logger)
      await internalTabReloader.init()
    })

    it('should initialize', function () {
      sinon.assert.calledWith(logger, 'InternalTabReloader Ready for use.')
    })

    it('should handle internal tab reloading', function () {
      const tabs = [{
        url: 'http://127.0.0.1:8080/ipfs/cid/wiki1',
        id: 1
      }, {
        url: 'chrome-extension://<some_id>/index.html',
        id: 2
      }]

      internalTabReloader.reload(tabs)
      sinon.assert.calledWith(browser.tabs.reload, 2)
      sinon.assert.calledWith(logger, 'reloading internal extension page at chrome-extension://<some_id>/index.html')
    })
  })

  describe('LocalGatewayReloader', function () {
    let localGatewayReloader
    beforeEach(async function () {
      browser.storage.local.get.resolves(CUSTOM_GATEWAY_URL)
      localGatewayReloader = new LocalGatewayReloader(browser, logger)
      await localGatewayReloader.init()
    })

    it('should initialize', function () {
      sinon.assert.calledWith(logger, 'Initialized without additional config.')
    })

    it('should handle local gateway tab reloading', function () {
      const tabs = [{
        title: 'Main Page',
        url: 'http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki1',
        id: 1
      }, {
        title: '127.0.0.1:8080',
        url: 'http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki2',
        id: 2
      }]

      localGatewayReloader.reload(tabs)
      sinon.assert.calledWith(browser.tabs.reload, 2)
      sinon.assert.calledWith(logger, 'reloading local gateway at http://127.0.0.1:8080/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki2')
    })
  })
})


text:
export default () => ({
  getMessage (key, substititions) {
    if (!substititions) return key
    substititions = Array.isArray(substititions) ? substititions : [substititions]
    if (!substititions.length) return key
    return `${key}[${substititions}]`
  }
})


text:
<!doctype html>

    <meta charset="utf-8" />

    Make sure the Linkify experiment is enabled in IPFS Companion before you load this page:

 Should be linkified

        /ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

/ipfs/bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy

ipfs://QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w (cidv0b58)

ipfs://bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy (cidv1b32)

ipns://en.wikipedia-on-ipfs.org

dweb:/ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

 Should NOT be linkified

ipfs:/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

ipns:/en.wikipedia-on-ipfs.org

dweb://ipfs/QmTAsnXoWmLZQEpvyZscrReFzqxP3pvULfGVgpJuayrp1w

    dweb:/ipfs/Qm.. link
    relative /ipfs/Qm link
    gateway link


permissions: <all_urls> idle tabs notifications proxy storage unlimitedStorage contextMenus clipboardWrite webNavigation webRequest webRequestBlocking

IPFS Companion: DWEB Protocol Handler

protocol: web+dweb

IPFS Companion: IPNS Protocol Handler

protocol: web+ipns

IPFS Companion: IPFS Protocol Handler

protocol: web+ipfs

IPFS Companion: DWEB Protocol Handler

protocol: dweb

IPFS Companion: IPNS Protocol Handler

protocol: ipns

IPFS Companion: IPFS Protocol Handler

protocol: ipfs


text:
export interface CompanionOptions {
  active: boolean
  ipfsNodeType: string
  ipfsNodeConfig: string
  publicGatewayUrl: string
  publicSubdomainGatewayUrl: string
  useCustomGateway: boolean
  useSubdomains: boolean
  enabledOn: string[]
  disabledOn: string[]
  automaticMode: boolean
  linkify: boolean
  dnslinkPolicy: boolean | string
  dnslinkDataPreload: boolean
  dnslinkRedirect: boolean
  recoverFailedHttpRequests: boolean
  detectIpfsPathHeader: boolean
  preloadAtPublicGateway: boolean
  catchUnhandledProtocols: boolean
  displayNotifications: boolean
  displayReleaseNotes: boolean
  customGatewayUrl: string
  ipfsApiUrl: string
  ipfsApiPollMs: number
  logNamespaces: string
  importDir: string
  useLatestWebUI: boolean
  dismissedUpdate: null | string
  openViaWebUI: boolean
  telemetryGroupMinimal: boolean
  telemetryGroupPerformance: boolean
  telemetryGroupUx: boolean
  telemetryGroupFeedback: boolean
  telemetryGroupLocation: boolean
}

export interface CompanionState extends Omit<CompanionOptions, 'publicGatewayUrl' | 'publicSubdomainGatewayUrl' | 'useCustomGateway' | 'ipfsApiUrl' | 'customGatewayUrl'> {
  peerCount: number
  pubGwURL: URL
  pubGwURLString: string
  pubSubdomainGwURL: URL
  pubSubdomainGwURLString: string
  redirect: boolean
  apiURL: URL
  apiURLString: string
  gwURL: URL
  gwURLString: string
  activeIntegrations: (url: string) => boolean
  localGwAvailable: boolean
  webuiRootUrl: string
}

interface SwitchToggleArguments {
  id: string
  onchange: () => void
  checked?: boolean
  disabled?: boolean
  style?: string
}
export function SwitchToggle (args: SwitchToggleArguments): undefined | HTMLElement


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'

/**
 * @type {import('../../types.js').SwitchToggle}
 */
export default function switchToggle ({
  checked,
  disabled,
  id,
  onchange,
  style
}) {
  if (typeof checked === 'undefined') return
  // @ts-expect-error - TS doesn't like the `html` template tag
  return html`

          <input type="checkbox" id="${id}" onchange=${onchange} class="mdc-switch__native-control" role="switch"
            ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''}>

  `
}


text:
@import url('~@material/switch/dist/mdc.switch.css');

.mdc-switch {
  --mdc-theme-secondary: #3e9096 /* teal */
}

.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb,
.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track {
  background-color: #7f8491; /* charcoal-muted */
  border-color: #7f8491;
}


text:
'use strict'
/* eslint-env browser, webextensions */

import './quick-import.css'

import browser from 'webextension-polyfill'
import choo from 'choo'
import html from 'choo/html/index.js'
import logo from './logo.js'
import * as externalApiClient from '../lib/ipfs-client/external.js'
import { formatImportDirectory } from '../lib/ipfs-import.js'
import all from 'it-all'
import drop from 'drag-and-drop-files'
import { filesize } from 'filesize'

document.title = browser.i18n.getMessage('quickImport_page_title')

const app = choo()

app.use(quickImportStore)
app.route('*', quickImportPage)
app.mount('#root')

function quickImportStore (state, emitter) {
  state.message = ''
  state.peerCount = ''
  state.ipfsNodeType = 'external'
  state.expandOptions = false
  state.openViaWebUI = true
  state.userChangedOpenViaWebUI = false
  state.importDir = ''
  state.userChangedImportDir = false

  function updateState ({ ipfsNodeType, peerCount, importDir, openViaWebUI }) {
    state.ipfsNodeType = ipfsNodeType
    state.peerCount = peerCount
    // This event will fire repeatedly,
    // we need to ensure we don't unset the user's preferences
    // when they change the options on this page
    if (!state.userChangedImportDir) {
      state.importDir = importDir
    }
    if (!state.userChangedOpenViaWebUI) {
      state.openViaWebUI = openViaWebUI
    }
  }

  let port

  emitter.on('DOMContentLoaded', async () => {
    browser.runtime.sendMessage({ telemetry: { trackView: 'quick-import' } })
    // initialize connection to the background script which will trigger UI updates
    port = browser.runtime.connect({ name: 'browser-action-port' })
    port.onMessage.addListener(async (message) => {
      if (message.statusUpdate) {
        console.log('In browser action, received message from background:', message)
        updateState(message.statusUpdate)
        emitter.emit('render')
      }
    })
  })

  emitter.on('fileInputChange', event => processFiles(state, emitter, event.target.files))

  // update companion preference
  emitter.on('optionChange', ({ key, value }) => {
    browser.storage.local.set({ [key]: value })
  })

  // drag & drop anywhere
  drop(document.body, files => processFiles(state, emitter, files))
}

async function processFiles (state, emitter, files) {
  console.log('Processing files', files)
  const { ipfsCompanion } = await browser.runtime.getBackgroundPage()
  try {
    console.log('importing files', files)
    if (!files.length) {
      // File list may be empty in some rare cases
      // eg. when user drags something from proprietary browser context
      // We just ignore those UI interactions.
      throw new Error('found no valid sources, try selecting a local file instead')
    }
    const {
      copyImportResultsToFiles,
      copyShareLink,
      preloadFilesAtPublicGateway,
      openFilesAtGateway,
      openFilesAtWebUI
    } = ipfsCompanion.ipfsImportHandler
    const importTab = await browser.tabs.getCurrent()
    const importDir = formatImportDirectory(state.importDir)
    const httpStreaming = ipfsCompanion.state.ipfsNodeType === 'external'

    const data = []
    let total = 0
    for (const file of files) {
      data.push({
        path: file.name,
        content: (httpStreaming ? file : file.stream())
      })
      total += file.size
    }
    const humanTotal = filesize(total, { round: 2 })
    state.progress = `Importing ${humanTotal}... (keep this page open)`
    emitter.emit('render')

    const progress = (bytes) => {
      const percent = ((bytes / total) * 100).toFixed(0)
      state.progress = `Importing... (${percent}% of ${humanTotal})`
      emitter.emit('render')
    }
    const options = {
      progress,
      wrapWithDirectory: true,
      pin: false // we use MFS for implicit pinning instead
    }

    let ipfs
    if (httpStreaming) {
      // We create separate instance of http client running in thie same page to
      // avoid serialization issues in Chromium
      // (https://bugs.chromium.org/p/chromium/issues/detail?id=112163) when
      // crossing process boundary, which enables streaming upload of big files
      // (4GB+) without buffering entire thing.
      ipfs = await externalApiClient.init(browser, ipfsCompanion.state)
      // Note: at the time of writing this it was possible to use SharedWorker,
      // but it felt brittle given Google's approach to browser extension APIs,
      // and this way seems to be more future-proof.
    } else {
      ipfs = ipfsCompanion.ipfs
    }
    // ipfs.add
    const results = await all(ipfs.addAll(data, options))

    // ipfs cp → MFS
    await copyImportResultsToFiles(results, importDir)
    state.progress = 'Completed'
    emitter.emit('render')
    console.log(`Successfully imported ${files.length} files`)

    // copy shareable URL & preload
    copyShareLink(results)
    preloadFilesAtPublicGateway(results)

    // update preferred import dir if user specified one while importing
    if (state.userChangedImportDir) {
      emitter.emit('optionChange', { key: 'importDir', value: state.importDir })
    }
    // present result to the user using the beast available way
    if (!state.openViaWebUI || state.ipfsNodeType.startsWith('embedded')) {
      await openFilesAtGateway(importDir)
    } else {
      await openFilesAtWebUI(importDir)
    }

    // close import tab as it will be replaced with a new tab with imported content
    await browser.tabs.remove(importTab.id)
  } catch (err) {
    console.error('Failed to import files to IPFS', err)
    // keep import tab and display error message in it
    state.message = 'Unable to import to IPFS:'
    state.progress = `${err}`
    emitter.emit('render')
    ipfsCompanion.notify('notify_importErrorTitle', 'notify_inlineErrorMsg', `${err.message}`)
  }
}

function quickImportOptions (state, emit) {
  const onExpandOptions = (e) => { state.expandOptions = true; emit('render') }
  const onDirectoryChange = (e) => { state.userChangedImportDir = true; state.importDir = e.target.value }
  const onOpenViaWebUIChange = (e) => { state.userChangedOpenViaWebUI = true; state.openViaWebUI = e.target.checked }
  const displayOpenWebUI = state.ipfsNodeType !== 'embedded'

  if (state.expandOptions) {
    return html`

        ${displayOpenWebUI
        ? html`
          <input id='openViaWebUI' type='checkbox' onchange=${onOpenViaWebUIChange} checked=${state.openViaWebUI} />

          ${browser.i18n.getMessage('quickImport_options_openViaWebUI')}
        `
        : null}

          ${browser.i18n.getMessage('quickImport_options_importDir')}

          <input id='importDir' class='w-40 bg-transparent aqua monospace br1 ba b--aqua pa2' type='text' oninput=${onDirectoryChange} value=${state.importDir} />

    `
  }
  return html`
    <button class='mt3 f6 lh-copy link bn bg-transparent moon-gray dib pa0 pointer' style='color: #6ACAD1' onclick=${onExpandOptions}>
      ${browser.i18n.getMessage('quickImport_options_show')} »
    </button>
  `
}

function quickImportPage (state, emit) {
  const onFileInputChange = (e) => emit('fileInputChange', e)
  const { peerCount } = state

  return html`

  ${logo({
    size: 80,
    path: '../../icons',
    heartbeat: false
  })}

              ${browser.i18n.getMessage('quickImport_head_peers')}

              ${browser.i18n.getMessage('quickImport_subhead_peers', [peerCount])}

          <input class="db pointer w-100 h-100 top-0 o-0" type="file" id="quickImportInput" multiple onchange=${onFileInputChange} />

                ${browser.i18n.getMessage('quickImport_pick_file_button')}

                <emph class='underline pl3 pr2 moon-gray'>
                  ${browser.i18n.getMessage('quickImport_or')}
                </emph>
                ${browser.i18n.getMessage('quickImport_drop_it_here')}

${state.message}${state.progress}

        ${quickImportOptions(state, emit)}

  `
}


text:
<!DOCTYPE html>

  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
    <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA==" />
    <link rel="stylesheet" href="/dist/bundles/uiCommons.css">
    <link rel="stylesheet" href="/dist/bundles/importPage.css">


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import { braveNodeType } from '../lib/ipfs-client/brave.js'

function logoFileName (nodeType, isIpfsOnline) {
  let prefix
  if (nodeType.startsWith('embedded')) prefix = 'js-'
  if (nodeType === braveNodeType) prefix = 'brave-'
  return `${prefix || ''}ipfs-logo-${isIpfsOnline ? 'on' : 'off'}.svg`
}

export default function logo ({
  heartbeat = true,
  ipfsNodeType = 'external',
  isIpfsOnline = true,
  path,
  size = 52
}) {
  return html`
    <img
      alt="IPFS"
      src="${path}/${logoFileName(ipfsNodeType, isIpfsOnline)}"
      class="v-mid ${isIpfsOnline ? '' : 'o-40'} ${isIpfsOnline && heartbeat ? 'heartbeat' : ''}"
      style="width:${size}px; height:${size}px" />
  `
}


text:
@keyframes heartbeat {
  0% {
    transform: scale(1);
  }
  5% {
    transform: scale(1.05);
    filter: drop-shadow(0 0 1.05em rgba(95, 203, 207, 0.5));
  }
  10% {
    transform: scale(1.025);
    filter: drop-shadow(0 0 1.025em rgba(95, 203, 207, 0.25));
  }
  15% {
    transform: scale(1.075);
    filter: drop-shadow(0 0 1.075em rgba(95, 203, 207, 0.5));
  }
  50% {
    transform: scale(1);
  }
  100% {
    transform: scale(1);
  }
}

.heartbeat {
  animation-name: heartbeat;
  animation-iteration-count: infinite;
  animation-duration: 2.5s;
}


text:
@import url('~tachyons/css/tachyons.css');
@import url('~ipfs-css/ipfs.css');
@import url('heartbeat.css');

html, body, #root {
  height: 100%;
}

.hover-inner-shadow {
  transition: box-shadow 0.2s ease-in-out;
}
.hover-inner-shadow:hover {
  box-shadow: inset 0 0 10px 5px rgba(211, 235, 237, 0.2);
}

.no-user-select {
  user-select: none;
  -moz-user-select: none;
  -webkit-user-select: none;
}

/* Temporary CSS for custom checkbox (TODO: move/replace with ipfs-css */
input[type='checkbox'] {
  display:none;
}

input[type=checkbox] + .mark {
  background: transparent;
  height: 16px;
  width: 16px;
  display: inline-block;
  padding: 0;
  border: 1px #6ACAD1 solid;
  position: relative;
}

input[type=checkbox]:checked + .mark::after {
  color: #6ACAD1;
  position: absolute;
  left: 3px;
  top: 3px;
  content: "";
  height: 5px;
  width: 8px;
  border-left: 2px solid;
  border-bottom: 2px solid;
  transform: rotate(-45deg);
}


text:
'use strict'
/* eslint-env browser, webextensions */

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

function statusEntry ({ label, labelLegend, title, value, check, valueClass = '' }) {
  const offline = browser.i18n.getMessage('panel_statusOffline')
  label = label ? browser.i18n.getMessage(label) : null
  labelLegend = labelLegend ? browser.i18n.getMessage(labelLegend) : label
  value = value || value === 0 ? value : offline
  return html`

        ${value.substring(0, 13)}

    `
}

export default function ipfsVersion ({
  gatewayVersion
}) {
  return html`
  ${statusEntry({
    label: 'panel_statusGatewayVersion',
    title: browser.i18n.getMessage('panel_statusGatewayVersionTitle'),
    value: gatewayVersion,
    check: gatewayVersion
  })}
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import switchToggle from '../../pages/components/switch-toggle.js'

export default function navItem ({
  disabled,
  helperText,
  onClick,
  style,
  switchValue,
  text,
  title
}) {
  let buttonStyle = 'black button-reset db w-100 bg-white b--none outline-0--focus pt2 ph3 f6 tl'
  if (disabled) {
    buttonStyle += ' o-40'
  } else {
    buttonStyle += ' pointer bg-near-white--hover'
  }
  if (style) {
    buttonStyle += ` ${style}`
  }
  if (disabled) {
    title = ''
  }

  return html`

    <button class="${buttonStyle}"
            onclick=${disabled ? null : onClick}  title="${title || ''}" ${disabled ? 'disabled' : ''}>

${text}
${switchToggle({ checked: switchValue, disabled, style: 'fr ml2' })}

${helperText}

    </button>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function powerIcon ({
  action,
  active,
  size = '1.8rem',
  title
}) {
  const svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
        class="fill-current-color mr1"
        style="width:${size}; height:${size}">
        <path d="M50 20.11A29.89 29.89 0 1 0 79.89 50 29.89 29.89 0 0 0 50 20.11zm-3.22 17a3.22 3.22 0 0 1 6.44 0v6.43a3.22 3.22 0 0 1-6.44 0zM50 66.08a16.14 16.14 0 0 1-11.41-27.49 3.28 3.28 0 0 1 1.76-.65 2.48 2.48 0 0 1 2.42 2.41 2.58 2.58 0 0 1-.77 1.77A10.81 10.81 0 0 0 38.59 50a11.25 11.25 0 0 0 22.5 0 10.93 10.93 0 0 0-3.21-7.88 3.37 3.37 0 0 1-.65-1.77 2.48 2.48 0 0 1 2.42-2.41 2.16 2.16 0 0 1 1.76.65A16.14 16.14 0 0 1 50 66.08z"/>
      </svg>
    `
  return icon({ svg, title, active, action })
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import logo from '../logo.js'
import versionUpdateIcon from './version-update-icon.js'
import powerIcon from './power-icon.js'
import optionsIcon from './options-icon.js'
import ipfsVersion from './ipfs-version.js'
import gatewayStatus from './gateway-status.js'

export default function header (props) {
  const { ipfsNodeType, active, onToggleActive, onOpenPrefs, onOpenReleaseNotes, isIpfsOnline, onOpenWelcomePage, newVersion } = props
  return html`

  ${logo({
    size: 54,
    path: '../../../icons',
    ipfsNodeType,
    isIpfsOnline: (active && isIpfsOnline)
  })}

                IPFS

            ${ipfsVersion(props)}

          ${newVersion
          ? versionUpdateIcon({
            newVersion,
            active,
            title: 'panel_headerNewVersionTitle',
            action: onOpenReleaseNotes
          })
          : null}
          ${powerIcon({
            active,
            title: 'panel_headerActiveToggleTitle',
            action: onToggleActive
          })}
          ${optionsIcon({
            active,
            title: 'panel_openPreferences',
            action: onOpenPrefs
          })}

        ${gatewayStatus(props)}

  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

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

export default function icon ({
  svg, title, active, action, className
}) {
  return html`
    <button class="header-icon pa0 ma0 dib bn bg-transparent transition-all ${className} ${action ? 'pointer' : null} ${active ? 'aqua' : 'gray'}"
      style="outline:none;"
      title="${browser.i18n.getMessage(title) || title}"
      onclick=${action}>
      ${svg}
    </button>
  `
}


text:
<!DOCTYPE html>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <link rel="stylesheet" href="/dist/bundles/uiCommons.css">
    <link rel="stylesheet" href="/dist/bundles/browserAction.css">


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import header from './header.js'
import { activeTabActions } from './context-actions.js'
import tools from './tools.js'

// Render the browser action page:
// Passed current app `state` from the store and `emit`, a function to create
// events, allowing views to signal back to the store that something happened.
export default function browserActionPage (state, emit) {
  const onViewOnGateway = () => emit('viewOnGateway')
  const onCopy = (copyAction) => emit('copy', copyAction)
  const onFilesCpImport = () => emit('filesCpImport')

  const onQuickImport = () => emit('quickImport')
  const onOpenWebUi = () => emit('openWebUi', '/')
  const onOpenWelcomePage = () => emit('openWelcomePage')
  const onOpenPrefs = () => emit('openPrefs')
  const onOpenReleaseNotes = () => emit('openReleaseNotes')
  const onToggleGlobalRedirect = () => emit('toggleGlobalRedirect')
  const onToggleSiteIntegrations = () => emit('toggleSiteIntegrations')
  const onToggleActive = () => emit('toggleActive')

  const headerProps = Object.assign({ onToggleActive, onOpenPrefs, onOpenReleaseNotes, onOpenWelcomePage }, state)
  const activeTabActionsProps = Object.assign({ onViewOnGateway, onToggleSiteIntegrations, onCopy, onFilesCpImport }, state)
  const opsProps = Object.assign({ onQuickImport, onOpenWebUi, onToggleGlobalRedirect }, state)

  return html`

        ${header(headerProps)}
        ${tools(opsProps)}

      ${activeTabActions(activeTabActionsProps)}

  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import './browser-action.css'

import choo from 'choo'
import browserActionPage from './page.js'
import browserActionStore from './store.js'

const app = choo()

// Use the store to setup state defaults and event listeners for mutations
app.use(browserActionStore)

// Register our single route
app.route('*', browserActionPage)

// Start the application and render it to the given querySelector
app.mount('#root')


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function optionsIcon ({
  action,
  active,
  size = '1.8rem',
  title
}) {
  const svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
        class="fill-current-color"
        style="width:${size}; height:${size}">
        <path d="M74.05 50.23c-.07-3.58 1.86-5.85 5.11-7.1-.2-2-2.48-7.45-3.63-8.76-3.11 1.46-6.06 1.23-8.54-1.22s-2.72-5.46-1.26-8.64a29.24 29.24 0 0 0-8.8-3.63c-1.06 3.08-3.12 5-6.35 5.25-3.82.29-6.29-1.69-7.61-5.22a30.11 30.11 0 0 0-8.77 3.67c1.5 3.16 1.3 6.1-1.15 8.6s-5.45 2.76-8.64 1.29a29.33 29.33 0 0 0-3.58 8.79C24 44.43 25.94 46.62 26 50s-1.82 5.84-5.1 7.12a29.21 29.21 0 0 0 3.68 8.71c3.09-1.38 6-1.15 8.42 1.22s2.79 5.33 1.41 8.49a29.72 29.72 0 0 0 8.76 3.57 1.46 1.46 0 0 0 .11-.21 7.19 7.19 0 0 1 13.53-.16c.13.33.28.32.55.25a29.64 29.64 0 0 0 8-3.3 4 4 0 0 0 .37-.25c-1.27-2.86-1.15-5.57.88-7.94 2.44-2.84 5.5-3.26 8.91-1.8a29.23 29.23 0 0 0 3.65-8.7c-3.17-1.22-5.05-3.38-5.12-6.77zM50 59.54a8.57 8.57 0 1 1 8.59-8.31A8.58 8.58 0 0 1 50 59.54z"/>
      </svg>
    `
  return icon({ svg, title, active, action })
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'

export default function toolsButton ({
  disabled,
  iconD,
  iconSize,
  onClick,
  style,
  text,
  title
}) {
  let buttonStyle = 'header-icon fade-in w-50 ba bw1 snow b--snow bg-transparent f7 ph1 pv0 br4 ma1 flex justify-center items-center truncate'
  if (disabled) {
    buttonStyle += ' o-60'
  } else {
    buttonStyle += ' pointer'
  }
  if (style) {
    buttonStyle += ` ${style}`
  }
  if (disabled) {
    title = ''
  }

  return html`

      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="mr1" width="${iconSize}" height="${iconSize}"><path fill="currentColor" d="${iconD}"/></svg>

${text}

  `
}


text:
@import url('~tachyons/css/tachyons.css');
@import url('~ipfs-css/ipfs.css');
@import url('../heartbeat.css');
@import url('../../pages/components/switch-toggle.css');

.bg-near-white--hover:hover {
  background-color: #F4F4F4;
}

.header-icon:active {
  color: #edf0f4;
  transform: translateY(2px);
}
.header-icon[disabled],
.header-icon[disabled]:active {
  cursor: not-allowed;
  pointer-events: none;
  transform: none;
}

.outline-0--focus:focus {
  outline: 0;
}

.no-user-select {
  -webkit-user-select: none; /* Old Chrome, Safari */
  -moz-user-select: none;    /* Firefox */
  -ms-user-select: none;     /* Internet Explorer/Edge */
  user-select: none;         /* Non-prefixed version, Chrome and Opera */
}

.force-select-all {
  -webkit-user-select: all;  /* Chrome 49+ */
  -moz-user-select: all;     /* Firefox 43+ */
  -ms-user-select: all;      /* No support yet */
  user-select: all;          /* Likely future */
}

.fade-in {
  animation: fade-in 600ms;
}
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.blink {
  animation: blink 1s linear infinite;
}

@keyframes blink {
  50% {
    opacity: 0;
  }
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function versionUpdateIcon ({
  action,
  active,
  className,
  newVersion,
  size = '1.8rem',
  title
}) {
  let svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
        class="fill-yellow-muted mr1"
        style="width:${size}; height:${size}">
        <path xmlns="http://www.w3.org/2000/svg" d="M71.13 28.87a29.88 29.88 0 100 42.26 29.86 29.86 0 000-42.26zm-18.39 37.6h-5.48V44.71h5.48zm0-26.53h-5.48v-5.49h5.48z"/>
      </svg>
    `
  // special handling for beta and dev builds
  // TODO: remove when we have no users on beta channel anymore
  if (newVersion.match(/\./g).length > 2) {
    svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 86 86"
          class="fill-red-muted mr1"
          style="width:${size}; height:${size}">
          <path d="M82.84 71.14L55.06 23a5.84 5.84 0 00-10.12 0L17.16 71.14a5.85 5.85 0 005.06 8.77h55.56a5.85 5.85 0 005.06-8.77zm-30.1-.66h-5.48V65h5.48zm0-10.26h-5.48V38.46h5.48z"/>
      </svg>
    `
    title = 'Beta channel is deprecated, please switch to regular releases'
    className = `${className} blink`
  }
  return icon({ svg, title, active, action, className })
}


text:
'use strict'
/* eslint-env browser, webextensions */

import browser from 'webextension-polyfill'
import html from 'choo/html/index.js'
import navItem from './nav-item.js'
import navHeader from './nav-header.js'
import { sameGateway } from '../../lib/ipfs-path.js'
import { formatImportDirectory } from '../../lib/ipfs-import.js'
import {
  contextMenuViewOnGateway,
  contextMenuCopyAddressAtPublicGw,
  contextMenuCopyPermalink,
  contextMenuCopyRawCid,
  contextMenuCopyCanonicalAddress,
  contextMenuCopyCidAddress
} from '../../lib/context-menus.js'

const notReady = browser.i18n.getMessage('panelCopy_notReadyHint')

// Context Actions are displayed in Browser Action and Page Action (FF only)
export function contextActions ({
  active,
  redirect,
  isRedirectContext,
  pubGwURLString,
  gwURLString,
  currentTab,
  currentFqdn,
  currentDnslinkFqdn,
  currentTabIntegrationsOptOut,
  currentTabContentPath = notReady,
  currentTabImmutablePath = notReady,
  currentTabCid = notReady,
  currentTabPublicUrl = notReady,
  currentTabPermalink = notReady,
  ipfsNodeType,
  isIpfsContext,
  isIpfsOnline,
  isApiAvailable,
  onToggleSiteIntegrations,
  onViewOnGateway,
  onCopy,
  importDir,
  onFilesCpImport
}) {
  const activeCidResolver = active && isIpfsOnline && isApiAvailable && currentTabCid
  const activeFilesCpImport = active && isIpfsOnline && isApiAvailable && !ipfsNodeType.startsWith('embedded')
  const isMutable = currentTabContentPath.startsWith('/ipns/')
  const activeViewOnGateway = (currentTab) => {
    if (!currentTab) return false
    const { url } = currentTab
    return !(url.startsWith('ip') || sameGateway(url, gwURLString) || sameGateway(url, pubGwURLString))
  }
  const renderIpfsContextItems = () => {
    if (!isIpfsContext) return
    return html`

  ${activeViewOnGateway(currentTab)
    ? navItem({
      text: browser.i18n.getMessage(contextMenuViewOnGateway),
      onClick: () => onViewOnGateway(contextMenuViewOnGateway)
    })
    : null}
  ${navItem({
    text: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw),
    title: browser.i18n.getMessage('panel_copyCurrentPublicGwUrlTooltip'),
    helperText: currentTabPublicUrl,
    onClick: () => onCopy(contextMenuCopyAddressAtPublicGw)
  })}
  ${isMutable
    ? navItem({
      text: browser.i18n.getMessage(contextMenuCopyPermalink),
      title: browser.i18n.getMessage('panel_copyCurrentPermalinkTooltip'),
      helperText: currentTabPermalink,
      onClick: () => onCopy(contextMenuCopyPermalink)
    })
    : ''}
  ${isMutable
    ? navItem({
      text: browser.i18n.getMessage(contextMenuCopyCanonicalAddress),
      title: browser.i18n.getMessage('panelCopy_currentIpnsAddressTooltip'),
      helperText: currentTabContentPath,
      onClick: () => onCopy(contextMenuCopyCanonicalAddress)
    })
    : ''}
  ${navItem({
    text: browser.i18n.getMessage(contextMenuCopyCidAddress),
    title: browser.i18n.getMessage('panelCopy_currentIpfsAddressTooltip'),
    helperText: currentTabImmutablePath,
    onClick: () => onCopy(contextMenuCopyCidAddress)
  })}
  ${navItem({
    text: browser.i18n.getMessage(contextMenuCopyRawCid),
    title: browser.i18n.getMessage('panelCopy_copyRawCidTooltip'),
    helperText: currentTabCid,
    disabled: !activeCidResolver,
    onClick: () => onCopy(contextMenuCopyRawCid)
  })}
  ${navItem({
    text: browser.i18n.getMessage('panel_importCurrentIpfsAddress'),
    title: browser.i18n.getMessage('panel_importCurrentIpfsAddressTooltip'),
    helperText: formatImportDirectory(importDir),
    disabled: !activeFilesCpImport,
    onClick: onFilesCpImport
  })}

    `
  }
  const renderSiteIntegrationsToggle = () => {
    if (!isRedirectContext) return
    return html`
  ${navItem({
    text: browser.i18n.getMessage('panel_activeTabSiteIntegrationsToggle', currentFqdn),
    title: browser.i18n.getMessage('panel_activeTabSiteIntegrationsToggleTooltip', currentFqdn),
    style: 'truncate',
    disabled: !(active),
    switchValue: active && !currentTabIntegrationsOptOut,
    onClick: onToggleSiteIntegrations
  })}
      `
  }
  return html`

  ${renderSiteIntegrationsToggle()}
  ${renderIpfsContextItems()}

  `
}

// "Active Tab" section is displayed in Browser Action  only
// if redirect can be toggled or current tab has any IPFS Context Actions
export function activeTabActions (state) {
  const showActiveTabSection = (state.isRedirectContext) || state.isIpfsContext
  if (!showActiveTabSection) return
  return html`

      ${navHeader('panel_activeTabSectionHeader')}

        ${contextActions(state)}

  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

import html from 'choo/html/index.js'
import icon from './icon.js'

export default function redirectIcon ({
  action,
  active,
  size = '2rem',
  title
}) {
  const svg = html`
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"
        class="fill-current-color"
        style="width:${size}; height:${size}">
        <path d="M75.4 41.6l-7.3-11.2c-.1-.2-.4-.2-.5 0l-7.3 11.3c-.1.2 0 .4.2.4h4.3l-.2 20.6c0 3.2-2.6 5.8-5.8 5.8h-.2a5.8 5.8 0 0 1-5.8-5.8V37.3a11.8 11.8 0 0 0-23.6 0L29 58.7h-4.3c-.2 0-.4.3-.3.5l7.3 11.6c.1.2.4.2.6 0l7.3-11.6c.1-.2 0-.5-.3-.5h-4.2l.2-21.5a5.8 5.8 0 0 1 11.6 0v25.3c0 6.5 5.3 11.8 11.8 11.8h.2c6.5 0 11.8-5.3 11.8-11.8l.2-20.4h4.4c.4 0 .2-.4.1-.5z"/>
      </svg>
    `
  return icon({
    svg,
    title,
    active,
    action
  })
}


text:
'use strict'
/* eslint-env browser, webextensions */

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

export default function tools ({
  active,
  ipfsNodeType,
  isApiAvailable,
  isIpfsOnline,
  onOpenWebUi,
  onQuickImport
}) {
  const activeQuickImport = active && isIpfsOnline && isApiAvailable
  const activeWebUI = active && isIpfsOnline && ipfsNodeType !== 'embedded'

  return html`
    <div class="flex pb2 ph2 justify-between">
  ${toolsButton({
    text: browser.i18n.getMessage('panel_quickImport'),
    title: browser.i18n.getMessage('panel_quickImportTooltip'),
    disabled: !activeQuickImport,
    onClick: onQuickImport,
    iconSize: 20,
    iconD: 'M71.13 28.87a29.88 29.88 0 100 42.26 29.86 29.86 0 000-42.26zm-18.39 37.6h-5.48V52.74H33.53v-5.48h13.73V33.53h5.48v13.73h13.73v5.48H52.74z'
  })}
  ${toolsButton({
    text: browser.i18n.getMessage('panel_openWebui'),
    title: browser.i18n.getMessage('panel_openWebuiTooltip'),
    disabled: !activeWebUI,
    onClick: onOpenWebUi,
    iconSize: 18,
    iconD: 'M69.69 20.57c-.51-.51-1.06-1-1.62-1.47l-.16-.1c-.56-.46-1.15-.9-1.76-1.32l-.5-.35c-.25-.17-.52-.32-.79-.48A28.27 28.27 0 0050 12.23h-.69a28.33 28.33 0 00-27.52 28.36c0 13.54 19.06 37.68 26 46a3.21 3.21 0 005 0c6.82-8.32 25.46-32.25 25.46-45.84a28.13 28.13 0 00-8.56-20.18zM51.07 49.51a9.12 9.12 0 119.13-9.12 9.12 9.12 0 01-9.13 9.12z'
  })}
    </div>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

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

export default function navHeader (label) {
  return html`
    <div class="no-select w-100 outline-0--focus tl ph3 pt2 mt1 pb1 o-40 f6">
      ${browser.i18n.getMessage(label)}
    </div>
  `
}


text:
'use strict'
/* eslint-env browser, webextensions */

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

function statusEntry ({ label, labelLegend, value, check, itemClass = '', valueClass = '' }) {
  const offline = browser.i18n.getMessage('panel_statusOffline')
  label = label ? browser.i18n.getMessage(label) : null
  labelLegend = labelLegend ? browser.i18n.getMessage(labelLegend) : label
  value = value || value === 0 ? value : offline
  return html`

        ${label}
        ${value}

    `
}

export default function gatewayStatus ({
  gatewayAddress,
  gatewayVersion,
  ipfsApiUrl,
  ipfsNodeType,
  swarmPeers
}) {
  const api = ipfsApiUrl && ipfsNodeType === 'embedded' ? 'js-ipfs' : ipfsApiUrl
  return html`

    ${statusEntry({
      label: 'panel_statusSwarmPeers',
      labelLegend: 'panel_statusSwarmPeersTitle',
      value: swarmPeers,
      check: swarmPeers
    })}
    ${statusEntry({
      label: 'panel_statusGatewayAddress',
      labelLegend: 'panel_statusGatewayAddressTitle',
      value: gatewayAddress,
      check: gatewayAddress
    })}
    ${statusEntry({
      label: 'panel_statusApiAddress',
      labelLegend: 'panel_statusApiAddressTitle',
      value: api,
      check: gatewayVersion
    })}

  `
}