diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json
index 12f084a93..05e20d2d0 100644
--- a/add-on/_locales/en/messages.json
+++ b/add-on/_locales/en/messages.json
@@ -750,5 +750,33 @@
"option_telemetryGroupTracking_description": {
"message": "Tracking description",
"description": "A description for the 'tracking' grouping of metrics we collect (option_telemetryGroupTracking_description)"
+ },
+ "recovery_page_title" : {
+ "message": "Problem with your IPFS node | IPFS Companion",
+ "description": "Title of the recovery page (recovery_page_title)"
+ },
+ "recovery_page_sub_header": {
+ "message": "Unable to reach your IPFS node :(",
+ "description": "Sub-Header on the recovery screen (recovery_page_sub_header)"
+ },
+ "recovery_page_message_p1": {
+ "message": "Ensure your IPFS node runs and provides HTTP Gateway.",
+ "description": "Message Para-1 on the recovery screen (recovery_page_message_p1)"
+ },
+ "recovery_page_message_p2": {
+ "message": "You can also access deserialized version of the requested resource through the preferred public gateway set up in IPFS Companion. This delegates trust to a third-party address below, and skips local hash validation.",
+ "description": "Message Para-2 on the recovery screen (recovery_page_message_p2)"
+ },
+ "recovery_page_button": {
+ "message": "Continue to the public gateway",
+ "description": "Button on the recovery screen (recovery_page_button)"
+ },
+ "recovery_page_learn_more": {
+ "message": "Learn more about public gateways",
+ "description": "Learn more link on the recovery screen (recovery_page_learn_more)"
+ },
+ "recovery_page_update_preferences": {
+ "message": "Update your IPFS Companion preferences",
+ "description": "Learn more link on the recovery screen (recovery_page_learn_more)"
}
}
diff --git a/add-on/manifest.common.json b/add-on/manifest.common.json
index 461d3aad4..db717fd9d 100644
--- a/add-on/manifest.common.json
+++ b/add-on/manifest.common.json
@@ -33,7 +33,10 @@
"icons/png/ipfs-logo-off_38.png",
"icons/png/ipfs-logo-off_128.png",
"icons/ipfs-logo-on.svg",
- "icons/ipfs-logo-off.svg"
+ "icons/ipfs-logo-off.svg",
+ "dist/recovery/recovery.css",
+ "dist/recovery/recovery.html",
+ "dist/recovery/recovery.js"
],
"content_security_policy": "script-src 'self'; object-src 'self'; frame-src 'self';",
"default_locale": "en"
diff --git a/add-on/src/landing-pages/welcome/page.js b/add-on/src/landing-pages/welcome/page.js
index a83722aeb..5361dc307 100644
--- a/add-on/src/landing-pages/welcome/page.js
+++ b/add-on/src/landing-pages/welcome/page.js
@@ -47,16 +47,24 @@ export default function createWelcomePage (i18n) {
/* ========================================================
Render functions for the left side
======================================================== */
-
-const renderCompanionLogo = (i18n, isIpfsOnline) => {
+export const renderLogo = (isIpfsOnline, logoSize = 128) => {
const logoPath = '../../../icons'
- const logoSize = 128
+
+ return html`
+ ${logo({ path: logoPath, size: logoSize, isIpfsOnline })}
+ `
+}
+
+export const renderCompanionLogo = (i18n, isIpfsOnline, showTitle = true) => {
const stateUnknown = isIpfsOnline === null
return html`
- ${logo({ path: logoPath, size: logoSize, isIpfsOnline })}
-
${i18n.getMessage('page_landingWelcome_logo_title')}
+ ${renderLogo(isIpfsOnline)}
+ ${showTitle
+ ? html`
${i18n.getMessage('page_landingWelcome_logo_title')}
`
+ : ''
+ }
`
}
@@ -88,17 +96,16 @@ const renderWelcome = (i18n, peerCount, openWebUi) => {
`
}
+export const nodeOffSvg = (svgWidth = 130) => html`
+
+`
+
const renderInstallSteps = (i18n, isIpfsOnline) => {
const copyClass = 'mv0 white f5 lh-copy'
const anchorClass = 'aqua hover-white'
const stateUnknown = isIpfsOnline === null
- const svgWidth = 130
-
- const nodeOffSvg = () => html`
-
- `
const optionsUrl = browser.runtime.getURL(optionsPage)
return html`
diff --git a/add-on/src/lib/constants.js b/add-on/src/lib/constants.js
index 6de4ce58c..ef2f7c61a 100644
--- a/add-on/src/lib/constants.js
+++ b/add-on/src/lib/constants.js
@@ -3,4 +3,5 @@
export const welcomePage = '/dist/landing-pages/welcome/index.html'
export const optionsPage = '/dist/options/options.html'
+export const recoveryPagePath = '/dist/recovery/recovery.html'
export const tickMs = 250 // no CPU spike, but still responsive enough
diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js
index 0d3c1d7a0..b055f599e 100644
--- a/add-on/src/lib/ipfs-companion.js
+++ b/add-on/src/lib/ipfs-companion.js
@@ -10,7 +10,7 @@ import LRU from 'lru-cache'
import all from 'it-all'
import { optionDefaults, storeMissingOptions, migrateOptions, guiURLString, safeURL } from './options.js'
import { initState, offlinePeerCount } from './state.js'
-import { createIpfsPathValidator, sameGateway, safeHostname } from './ipfs-path.js'
+import { createIpfsPathValidator, dropSlash, sameGateway, safeHostname } from './ipfs-path.js'
import createDnslinkResolver from './dnslink.js'
import { createRequestModifier } from './ipfs-request.js'
import { initIpfsClient, destroyIpfsClient, reloadIpfsClientOfflinePages } from './ipfs-client/index.js'
@@ -224,7 +224,6 @@ export default async function init () {
async function sendStatusUpdateToBrowserAction () {
if (!browserActionPort) return
- const dropSlash = url => url.replace(/\/$/, '')
const currentTab = await browser.tabs.query({ active: true, currentWindow: true }).then(tabs => tabs[0])
const { version } = browser.runtime.getManifest()
const info = {
diff --git a/add-on/src/lib/ipfs-path.js b/add-on/src/lib/ipfs-path.js
index 64ede8bf0..2490d92ae 100644
--- a/add-on/src/lib/ipfs-path.js
+++ b/add-on/src/lib/ipfs-path.js
@@ -8,6 +8,8 @@ import isFQDN from 'is-fqdn'
// For how long more expensive lookups (DAG traversal etc) should be cached
const RESULT_TTL_MS = 300000 // 5 minutes
+export const dropSlash = url => url.replace(/\/$/, '')
+
// Turns URL or URIencoded path into a content path
export function ipfsContentPath (urlOrPath, opts) {
opts = opts || {}
diff --git a/add-on/src/lib/ipfs-request.js b/add-on/src/lib/ipfs-request.js
index a0e268f77..e20e568ff 100644
--- a/add-on/src/lib/ipfs-request.js
+++ b/add-on/src/lib/ipfs-request.js
@@ -6,9 +6,11 @@ import debug from 'debug'
import LRU from 'lru-cache'
import isIPFS from 'is-ipfs'
import isFQDN from 'is-fqdn'
-import { pathAtHttpGateway, sameGateway, ipfsUri } from './ipfs-path.js'
+import { dropSlash, ipfsUri, pathAtHttpGateway, sameGateway } from './ipfs-path.js'
import { safeURL } from './options.js'
import { braveNodeType } from './ipfs-client/brave.js'
+import { recoveryPagePath } from './constants.js'
+
const log = debug('ipfs-companion:request')
log.error = debug('ipfs-companion:request:error')
@@ -140,6 +142,13 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
const state = getState()
if (!state.active) return
+ // When local IPFS node is unreachable , show recovery page where user can redirect
+ // to public gateway.
+ if (!state.nodeActive && request.type === 'main_frame' && sameGateway(request.url, state.gwURL)) {
+ const publicUri = ipfsPathValidator.resolveToPublicUrl(request.url, state.pubGwURLString)
+ return { redirectUrl: `${dropSlash(runtimeRoot)}${recoveryPagePath}#${encodeURIComponent(publicUri)}` }
+ }
+
// When Subdomain Proxy is enabled we normalize address bar requests made
// to the local gateway and replace raw IP with 'localhost' hostname to
// take advantage of subdomain redirect provided by go-ipfs >= 0.5
diff --git a/add-on/src/lib/state.js b/add-on/src/lib/state.js
index 6ba34cb24..0a99fb2f2 100644
--- a/add-on/src/lib/state.js
+++ b/add-on/src/lib/state.js
@@ -50,8 +50,11 @@ export function initState (options, overrides) {
return false
}
}
- // TODO state.connected ~= state.peerCount > 0
- // TODO state.nodeActive ~= API is online,eg. state.peerCount > offlinePeerCount
+ // TODO refactor this into a class. It's getting too big and messy.
+ Object.defineProperty(state, 'nodeActive', {
+ // TODO: make quick fetch to confirm it works?
+ get: function () { return this.peerCount !== offlinePeerCount }
+ })
Object.defineProperty(state, 'localGwAvailable', {
// TODO: make quick fetch to confirm it works?
get: function () { return this.ipfsNodeType !== 'embedded' }
diff --git a/add-on/src/recovery/recovery.css b/add-on/src/recovery/recovery.css
new file mode 100644
index 000000000..786182825
--- /dev/null
+++ b/add-on/src/recovery/recovery.css
@@ -0,0 +1,54 @@
+@import url('~tachyons/css/tachyons.css');
+@import url('~ipfs-css/ipfs.css');
+
+#left-col {
+ background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
+ background-size: 100%;
+ background-repeat: repeat;
+}
+
+a:hover {
+ text-decoration: none;
+}
+
+a:visited {
+ color: inherit;
+}
+
+/*
+ https://github.com/tachyons-css/tachyons-queries
+ Tachyons: $point == large
+*/
+@media (min-width: 60em) {
+ #left-col {
+ position: fixed;
+ top: 0;
+ right: 55%;
+ width: 45%;
+ background-image: url('../../images/stars.png'), linear-gradient(to bottom, #041727 0%, #043b55 100%);
+ background-size: 100%;
+ background-repeat: repeat;
+ }
+
+ #right-col {
+ margin-left: 54%;
+ margin-right: 6%;
+ }
+}
+
+@media (max-height: 800px) {
+ #left-col img {
+ width: 98px !important;
+ height: 98px !important;
+ }
+
+ #left-col svg {
+ width: 60px;
+ }
+}
+
+.recovery-root {
+ width: 100%;
+ height: 100%;
+ text-align: left;
+}
diff --git a/add-on/src/recovery/recovery.html b/add-on/src/recovery/recovery.html
new file mode 100644
index 000000000..af492fbe3
--- /dev/null
+++ b/add-on/src/recovery/recovery.html
@@ -0,0 +1,20 @@
+
+
+
+ IPFS Node is Offline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/add-on/src/recovery/recovery.js b/add-on/src/recovery/recovery.js
new file mode 100644
index 000000000..e749123f8
--- /dev/null
+++ b/add-on/src/recovery/recovery.js
@@ -0,0 +1,75 @@
+'use strict'
+/* eslint-env browser, webextensions */
+
+import choo from 'choo'
+import html from 'choo/html/index.js'
+import browser, { i18n, runtime } from 'webextension-polyfill'
+import { nodeOffSvg } from '../landing-pages/welcome/page.js'
+import createWelcomePageStore from '../landing-pages/welcome/store.js'
+import { optionsPage } from '../lib/constants.js'
+import './recovery.css'
+
+const app = choo()
+
+const learnMoreLink = html`${i18n.getMessage('recovery_page_learn_more')}`
+
+const optionsPageLink = html`${i18n.getMessage('recovery_page_update_preferences')}`
+
+// TODO (whizzzkid): refactor base store to be more generic.
+app.use(createWelcomePageStore(i18n, runtime))
+// Register our single route
+app.route('*', (state) => {
+ browser.runtime.sendMessage({ telemetry: { trackView: 'recovery' } })
+ const { hash } = window.location
+ const { href: publicURI } = new URL(decodeURIComponent(hash.slice(1)))
+
+ if (!publicURI) {
+ return
+ }
+
+ const openURLFromHash = () => {
+ try {
+ console.log('Opening URL from hash:', publicURI)
+ window.location.replace(publicURI)
+ } catch (err) {
+ console.error('Failed to open URL from hash:', err)
+ }
+ }
+
+ // if the IPFS node is online, open the URL from the hash, this will redirect to the local node.
+ if (state.isIpfsOnline) {
+ openURLFromHash()
+ return
+ }
+
+ return html`
+
+
+ ${nodeOffSvg(200)}
+
${i18n.getMessage('recovery_page_sub_header')}
+
+
+
+
+
${i18n.getMessage('recovery_page_message_p1')}
+
${i18n.getMessage('recovery_page_message_p2')}
+
Public URL: ${publicURI}
+
+
+ ${learnMoreLink} | ${optionsPageLink}
+
+
+
`
+})
+
+// Start the application and render it to the given querySelector
+app.mount('#root')
+
+// Set page title and header translation
+document.title = i18n.getMessage('recovery_page_title')
diff --git a/test/functional/lib/ipfs-request-gateway-redirect.test.js b/test/functional/lib/ipfs-request-gateway-redirect.test.js
index 6389764c4..07d3ccacc 100644
--- a/test/functional/lib/ipfs-request-gateway-redirect.test.js
+++ b/test/functional/lib/ipfs-request-gateway-redirect.test.js
@@ -33,6 +33,7 @@ describe('modifyRequest.onBeforeRequest:', function () {
global.URL = URL
global.browser = browser
browser.runtime.id = 'testid'
+ browser.runtime.getURL.returns('chrome-extension://testid/')
})
beforeEach(async function () {
@@ -425,6 +426,24 @@ describe('modifyRequest.onBeforeRequest:', function () {
})
})
+ describe('Recovers Page if node is unreachable', function () {
+ beforeEach(function () {
+ global.browser = browser
+ state.ipfsNodeType = 'external'
+ state.redirect = true
+ state.peerCount = -1
+ state.gwURLString = 'http://localhost:8080'
+ state.gwURL = new URL('http://localhost:8080')
+ state.pubGwURLString = 'https://ipfs.io'
+ state.pubGwURL = new URL('https://ipfs.io')
+ })
+ it('should present recovery page if node is offline', function () {
+ expect(state.nodeActive).to.be.equal(false)
+ const request = url2request('https://localhost:8080/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR/foo/bar')
+ expect(modifyRequest.onBeforeRequest(request).redirectUrl).to.equal('chrome-extension://testid/dist/recovery/recovery.html#https%3A%2F%2Fipfs.io%2Fipfs%2FQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR%2Ffoo%2Fbar')
+ })
+ })
+
after(function () {
delete global.URL
delete global.browser
diff --git a/webpack.config.js b/webpack.config.js
index 85a19ba3f..e47214c3a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -156,6 +156,7 @@ const uiConfig = merge(commonConfig, {
browserAction: './add-on/src/popup/browser-action/index.js',
importPage: './add-on/src/popup/quick-import.js',
optionsPage: './add-on/src/options/options.js',
+ recoveryPage: './add-on/src/recovery/recovery.js',
welcomePage: './add-on/src/landing-pages/welcome/index.js'
},
optimization: {