From 49afbe2fbf93307ad42f37288440b1b18ed275f5 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 29 Nov 2017 08:53:59 +0000 Subject: [PATCH 1/6] moves copy functions into a module --- add-on/src/lib/context-menus.js | 25 +++++ add-on/src/lib/copy.js | 56 +++++++++++ add-on/src/lib/ipfs-companion.js | 117 ++++------------------- add-on/src/lib/notifier.js | 26 +++++ add-on/src/popup/browser-action/store.js | 9 +- 5 files changed, 132 insertions(+), 101 deletions(-) create mode 100644 add-on/src/lib/context-menus.js create mode 100644 add-on/src/lib/copy.js create mode 100644 add-on/src/lib/notifier.js diff --git a/add-on/src/lib/context-menus.js b/add-on/src/lib/context-menus.js new file mode 100644 index 000000000..b14c13cc8 --- /dev/null +++ b/add-on/src/lib/context-menus.js @@ -0,0 +1,25 @@ +'use strict' + +const browser = require('webextension-polyfill') + +async function findUrlForContext (context) { + if (context) { + if (context.linkUrl) { + // present when clicked on a link + return context.linkUrl + } + if (context.srcUrl) { + // present when clicked on page element such as image or video + return context.srcUrl + } + if (context.pageUrl) { + // pageUrl is the root frame + return context.pageUrl + } + } + // falback to the url of current tab + const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) + return currentTab.url +} + +module.exports.findUrlForContext = findUrlForContext diff --git a/add-on/src/lib/copy.js b/add-on/src/lib/copy.js new file mode 100644 index 000000000..d3eee54fa --- /dev/null +++ b/add-on/src/lib/copy.js @@ -0,0 +1,56 @@ +'use strict' + +const browser = require('webextension-polyfill') +const { safeIpfsPath } = require('./ipfs-path') +const { findUrlForContext } = require('./context-menus') + +async function copyTextToClipboard (copyText) { + const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) + const tabId = currentTab.id + // Lets take a moment and ponder on the state of copying a string in 2017: + const copyToClipboardIn2017 = `function copyToClipboardIn2017(text) { + function oncopy(event) { + document.removeEventListener('copy', oncopy, true); + event.stopImmediatePropagation(); + event.preventDefault(); + event.clipboardData.setData('text/plain', text); + } + document.addEventListener('copy', oncopy, true); + document.execCommand('copy'); + }` + + // In Firefox you can't select text or focus an input field in background pages, + // so you can't write to the clipboard from a background page. + // We work around this limitation by injecting content scropt into a tab and copying there. + // Yes, this is 2017. + try { + const copyHelperPresent = (await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: "typeof copyToClipboardIn2017 === 'function';" }))[0] + if (!copyHelperPresent) { + await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: copyToClipboardIn2017 }) + } + await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: 'copyToClipboardIn2017(' + JSON.stringify(copyText) + ');' }) + } catch (error) { + console.error('Failed to copy text: ' + error) + } +} + +function createCopier (getState, notify) { + return { + async copyCanonicalAddress (context) { + const url = await findUrlForContext(context) + const rawIpfsAddress = safeIpfsPath(url) + copyTextToClipboard(rawIpfsAddress) + notify('notify_copiedCanonicalAddressTitle', rawIpfsAddress) + }, + + async copyAddressAtPublicGw (context) { + const url = await findUrlForContext(context) + const state = getState() + const urlAtPubGw = url.replace(state.gwURLString, state.pubGwURLString) + copyTextToClipboard(urlAtPubGw) + notify('notify_copiedPublicURLTitle', urlAtPubGw) + } + } +} + +module.exports = createCopier diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 0933619a4..a530fda46 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -6,9 +6,12 @@ const { optionDefaults, storeMissingOptions } = require('./options') const { initState } = require('./state') const IsIpfs = require('is-ipfs') const IpfsApi = require('ipfs-api') -const { createIpfsPathValidator, safeIpfsPath, urlAtPublicGw } = require('./ipfs-path') +const { createIpfsPathValidator, urlAtPublicGw } = require('./ipfs-path') const createDnsLink = require('./dns-link') const { createRequestModifier } = require('./ipfs-request') +const { createNotifier } = require('./notifier') +const createCopier = require('./copy') +const { findUrlForContext } = require('./context-menus') // INIT // =================================================================== @@ -17,6 +20,8 @@ var state // avoid redundant API reads by utilizing local cache of various state var dnsLink var ipfsPathValidator var modifyRequest +var notify +var copier // init happens on addon load in background/background.js module.exports = async function init () { @@ -24,6 +29,8 @@ module.exports = async function init () { const options = await browser.storage.local.get(optionDefaults) state = window.state = initState(options) ipfs = window.ipfs = initIpfsApi(options.ipfsApiUrl) + notify = createNotifier(getState) + copier = createCopier(getState, notify) dnsLink = createDnsLink(getState) ipfsPathValidator = createIpfsPathValidator(getState, dnsLink) modifyRequest = createRequestModifier(getState, dnsLink, ipfsPathValidator) @@ -44,6 +51,8 @@ module.exports.destroy = function () { dnsLink = null modifyRequest = null ipfsPathValidator = null + notify = null + copier = null } function getState () { @@ -124,11 +133,16 @@ function onRuntimeConnect (port) { } } +const BrowserActionMessageHandlers = { + notification: (message) => notify(message.title, message.message), + copyCanonicalAddress: () => copier.copyCanonicalAddress(), + copyAddressAtPublicGw: () => copier.copyAddressAtPublicGw() +} + function handleMessageFromBrowserAction (message) { - // console.log('In background script, received message from browser action', message) - if (message.event === 'notification') { - notify(message.title, message.message) - } + const handler = BrowserActionMessageHandlers[message && message.event] + if (!handler) return console.warn('Unknown browser action message event', message) + handler(message) } async function sendStatusUpdateToBrowserAction () { @@ -157,25 +171,6 @@ async function sendStatusUpdateToBrowserAction () { // GUI // =================================================================== -function notify (titleKey, messageKey, messageParam) { - const title = browser.i18n.getMessage(titleKey) - let message - if (messageKey.startsWith('notify_')) { - message = messageParam ? browser.i18n.getMessage(messageKey, messageParam) : browser.i18n.getMessage(messageKey) - } else { - message = messageKey - } - if (state.displayNotifications) { - browser.notifications.create({ - 'type': 'basic', - 'iconUrl': browser.extension.getURL('icons/ipfs-logo-on.svg'), - 'title': title, - 'message': message - }) - } - console.info(`[ipfs-companion] ${title}: ${message}`) -} - // contextMenus // ------------------------------------------------------------------- const contextMenuUploadToIpfs = 'contextMenu_UploadToIpfs' @@ -196,14 +191,14 @@ try { title: browser.i18n.getMessage(contextMenuCopyIpfsAddress), contexts: ['page', 'image', 'video', 'audio', 'link'], documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: copyCanonicalAddress + onclick: () => copier.copyCanonicalAddress() }) browser.contextMenus.create({ id: contextMenuCopyPublicGwUrl, title: browser.i18n.getMessage(contextMenuCopyPublicGwUrl), contexts: ['page', 'image', 'video', 'audio', 'link'], documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: copyAddressAtPublicGw + onclick: () => copier.copyAddressAtPublicGw() }) } catch (err) { console.log('[ipfs-companion] Error creating contextMenus', err) @@ -295,76 +290,6 @@ window.uploadResultHandler = uploadResultHandler // Copying URLs // ------------------------------------------------------------------- -window.safeIpfsPath = safeIpfsPath - -async function findUrlForContext (context) { - if (context) { - if (context.linkUrl) { - // present when clicked on a link - return context.linkUrl - } - if (context.srcUrl) { - // present when clicked on page element such as image or video - return context.srcUrl - } - if (context.pageUrl) { - // pageUrl is the root frame - return context.pageUrl - } - } - // falback to the url of current tab - const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) - return currentTab.url -} - -async function copyCanonicalAddress (context) { - const url = await findUrlForContext(context) - const rawIpfsAddress = safeIpfsPath(url) - copyTextToClipboard(rawIpfsAddress) - notify('notify_copiedCanonicalAddressTitle', rawIpfsAddress) -} - -window.copyCanonicalAddress = copyCanonicalAddress - -async function copyAddressAtPublicGw (context) { - const url = await findUrlForContext(context) - const urlAtPubGw = url.replace(state.gwURLString, state.pubGwURLString) - copyTextToClipboard(urlAtPubGw) - notify('notify_copiedPublicURLTitle', urlAtPubGw) -} - -window.copyAddressAtPublicGw = copyAddressAtPublicGw - -async function copyTextToClipboard (copyText) { - const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) - const tabId = currentTab.id - // Lets take a moment and ponder on the state of copying a string in 2017: - const copyToClipboardIn2017 = `function copyToClipboardIn2017(text) { - function oncopy(event) { - document.removeEventListener('copy', oncopy, true); - event.stopImmediatePropagation(); - event.preventDefault(); - event.clipboardData.setData('text/plain', text); - } - document.addEventListener('copy', oncopy, true); - document.execCommand('copy'); - }` - - // In Firefox you can't select text or focus an input field in background pages, - // so you can't write to the clipboard from a background page. - // We work around this limitation by injecting content scropt into a tab and copying there. - // Yes, this is 2017. - try { - const copyHelperPresent = (await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: "typeof copyToClipboardIn2017 === 'function';" }))[0] - if (!copyHelperPresent) { - await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: copyToClipboardIn2017 }) - } - await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: 'copyToClipboardIn2017(' + JSON.stringify(copyText) + ');' }) - } catch (error) { - console.error('Failed to copy text: ' + error) - } -} - async function updateContextMenus (changedTabId) { try { await browser.contextMenus.update(contextMenuUploadToIpfs, {enabled: state.peerCount > 0}) diff --git a/add-on/src/lib/notifier.js b/add-on/src/lib/notifier.js new file mode 100644 index 000000000..d3b65ee10 --- /dev/null +++ b/add-on/src/lib/notifier.js @@ -0,0 +1,26 @@ +'use strict' + +const browser = require('webextension-polyfill') + +function createNotifier (getState) { + return (titleKey, messageKey, messageParam) => { + const title = browser.i18n.getMessage(titleKey) + let message + if (messageKey.startsWith('notify_')) { + message = messageParam ? browser.i18n.getMessage(messageKey, messageParam) : browser.i18n.getMessage(messageKey) + } else { + message = messageKey + } + if (getState().displayNotifications) { + browser.notifications.create({ + type: 'basic', + iconUrl: browser.extension.getURL('icons/ipfs-logo-on.svg'), + title: title, + message: message + }) + } + console.info(`[ipfs-companion] ${title}: ${message}`) + } +} + +module.exports = createNotifier diff --git a/add-on/src/popup/browser-action/store.js b/add-on/src/popup/browser-action/store.js index 64fdb7731..607ca9bc3 100644 --- a/add-on/src/popup/browser-action/store.js +++ b/add-on/src/popup/browser-action/store.js @@ -2,6 +2,7 @@ /* eslint-env browser, webextensions */ const browser = require('webextension-polyfill') +const { safeIpfsPath } = require('../../lib/ipfs-path') // The store contains and mutates the state for the app module.exports = (state, emitter) => { @@ -35,14 +36,12 @@ module.exports = (state, emitter) => { }) emitter.on('copyPublicGwAddr', async function copyCurrentPublicGwAddress () { - const bg = await getBackgroundPage() - await bg.copyAddressAtPublicGw() + port.postMessage({ event: 'copyAddressAtPublicGw' }) window.close() }) emitter.on('copyIpfsAddr', async function copyCurrentCanonicalAddress () { - const bg = await getBackgroundPage() - await bg.copyCanonicalAddress() + port.postMessage({ event: 'copyCanonicalAddress' }) window.close() }) @@ -202,7 +201,7 @@ function getBackgroundPage () { async function resolveToIPFS (path) { const bg = await getBackgroundPage() - path = bg.safeIpfsPath(path) // https://github.com/ipfs/ipfs-companion/issues/303 + path = safeIpfsPath(path) // https://github.com/ipfs/ipfs-companion/issues/303 if (/^\/ipns/.test(path)) { const response = await bg.ipfs.name.resolve(path, {recursive: true, nocache: false}) return response.Path From 28d714151aacdd25685e0958cf4ff4a0cb60d977 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 29 Nov 2017 11:59:32 +0000 Subject: [PATCH 2/6] changes ipfs-companion module to expose api, removes remaining window references --- add-on/src/background/background.js | 6 +- add-on/src/lib/{copy.js => copier.js} | 0 add-on/src/lib/ipfs-companion.js | 909 +++++++++++---------- add-on/src/popup/browser-action/store.js | 16 +- add-on/src/popup/quick-upload.js | 41 +- test/functional/lib/ipfs-companion.test.js | 8 +- 6 files changed, 500 insertions(+), 480 deletions(-) rename add-on/src/lib/{copy.js => copier.js} (100%) diff --git a/add-on/src/background/background.js b/add-on/src/background/background.js index b99482ec7..ce31f3c1f 100644 --- a/add-on/src/background/background.js +++ b/add-on/src/background/background.js @@ -1,7 +1,9 @@ 'use strict' /* eslint-env browser, webextensions */ -const init = require('../lib/ipfs-companion') +const createIpfsCompanion = require('../lib/ipfs-companion') // init add-on after all libs are loaded -document.addEventListener('DOMContentLoaded', init) +document.addEventListener('DOMContentLoaded', async () => { + window.ipfsCompanion = await createIpfsCompanion() +}) diff --git a/add-on/src/lib/copy.js b/add-on/src/lib/copier.js similarity index 100% rename from add-on/src/lib/copy.js rename to add-on/src/lib/copier.js diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index a530fda46..3c36b677c 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -9,26 +9,26 @@ const IpfsApi = require('ipfs-api') const { createIpfsPathValidator, urlAtPublicGw } = require('./ipfs-path') const createDnsLink = require('./dns-link') const { createRequestModifier } = require('./ipfs-request') -const { createNotifier } = require('./notifier') -const createCopier = require('./copy') +const createNotifier = require('./notifier') +const createCopier = require('./copier') const { findUrlForContext } = require('./context-menus') -// INIT -// =================================================================== -var ipfs // ipfs-api instance -var state // avoid redundant API reads by utilizing local cache of various states -var dnsLink -var ipfsPathValidator -var modifyRequest -var notify -var copier - // init happens on addon load in background/background.js module.exports = async function init () { + // INIT + // =================================================================== + var ipfs // ipfs-api instance + var state // avoid redundant API reads by utilizing local cache of various states + var dnsLink + var ipfsPathValidator + var modifyRequest + var notify + var copier + try { const options = await browser.storage.local.get(optionDefaults) - state = window.state = initState(options) - ipfs = window.ipfs = initIpfsApi(options.ipfsApiUrl) + state = initState(options) + ipfs = initIpfsApi(options.ipfsApiUrl) notify = createNotifier(getState) copier = createCopier(getState, notify) dnsLink = createDnsLink(getState) @@ -41,516 +41,537 @@ module.exports = async function init () { console.error('Unable to initialize addon due to error', error) notify('notify_addonIssueTitle', 'notify_addonIssueMsg') } -} - -module.exports.destroy = function () { - clearInterval(apiStatusUpdateInterval) - apiStatusUpdateInterval = null - ipfs = null - state = null - dnsLink = null - modifyRequest = null - ipfsPathValidator = null - notify = null - copier = null -} - -function getState () { - return state -} -function initIpfsApi (ipfsApiUrl) { - const url = new URL(ipfsApiUrl) - return IpfsApi({host: url.hostname, port: url.port, procotol: url.protocol}) -} + function getState () { + return state + } -function registerListeners () { - browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ['']}, ['blocking', 'requestHeaders']) - browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ['']}, ['blocking']) - browser.storage.onChanged.addListener(onStorageChange) - browser.webNavigation.onCommitted.addListener(onNavigationCommitted) - browser.tabs.onUpdated.addListener(onUpdatedTab) - browser.tabs.onActivated.addListener(onActivatedTab) - browser.runtime.onMessage.addListener(onRuntimeMessage) - browser.runtime.onConnect.addListener(onRuntimeConnect) -} + function initIpfsApi (ipfsApiUrl) { + const url = new URL(ipfsApiUrl) + return IpfsApi({host: url.hostname, port: url.port, procotol: url.protocol}) + } -// HTTP Request Hooks -// =================================================================== + function registerListeners () { + browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ['']}, ['blocking', 'requestHeaders']) + browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ['']}, ['blocking']) + browser.storage.onChanged.addListener(onStorageChange) + browser.webNavigation.onCommitted.addListener(onNavigationCommitted) + browser.tabs.onUpdated.addListener(onUpdatedTab) + browser.tabs.onActivated.addListener(onActivatedTab) + browser.runtime.onMessage.addListener(onRuntimeMessage) + browser.runtime.onConnect.addListener(onRuntimeConnect) + } -function onBeforeSendHeaders (request) { - if (request.url.startsWith(state.apiURLString)) { - // For some reason js-ipfs-api sent requests with "Origin: null" under Chrome - // which produced '403 - Forbidden' error. - // This workaround removes bogus header from API requests - for (let i = 0; i < request.requestHeaders.length; i++) { - let header = request.requestHeaders[i] - if (header.name === 'Origin' && (header.value == null || header.value === 'null')) { - request.requestHeaders.splice(i, 1) - break + // HTTP Request Hooks + // =================================================================== + + function onBeforeSendHeaders (request) { + if (request.url.startsWith(state.apiURLString)) { + // For some reason js-ipfs-api sent requests with "Origin: null" under Chrome + // which produced '403 - Forbidden' error. + // This workaround removes bogus header from API requests + for (let i = 0; i < request.requestHeaders.length; i++) { + let header = request.requestHeaders[i] + if (header.name === 'Origin' && (header.value == null || header.value === 'null')) { + request.requestHeaders.splice(i, 1) + break + } } } + return { + requestHeaders: request.requestHeaders + } } - return { - requestHeaders: request.requestHeaders + + function onBeforeRequest (request) { + return modifyRequest(request) } -} -function onBeforeRequest (request) { - return modifyRequest(request) -} + // RUNTIME MESSAGES (one-off messaging) + // =================================================================== + // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage -// RUNTIME MESSAGES (one-off messaging) -// =================================================================== -// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage - -function onRuntimeMessage (request, sender) { - // console.log((sender.tab ? 'Message from a content script:' + sender.tab.url : 'Message from the extension'), request) - if (request.pubGwUrlForIpfsOrIpnsPath) { - const path = request.pubGwUrlForIpfsOrIpnsPath - const result = ipfsPathValidator.validIpfsOrIpnsPath(path) ? urlAtPublicGw(path, state.pubGwURLString) : null - return Promise.resolve({pubGwUrlForIpfsOrIpnsPath: result}) + function onRuntimeMessage (request, sender) { + // console.log((sender.tab ? 'Message from a content script:' + sender.tab.url : 'Message from the extension'), request) + if (request.pubGwUrlForIpfsOrIpnsPath) { + const path = request.pubGwUrlForIpfsOrIpnsPath + const result = ipfsPathValidator.validIpfsOrIpnsPath(path) ? urlAtPublicGw(path, state.pubGwURLString) : null + return Promise.resolve({pubGwUrlForIpfsOrIpnsPath: result}) + } } -} -// PORTS (connection-based messaging) -// =================================================================== -// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/connect -// Make a connection between different contexts inside the add-on, -// e.g. signalling between browser action popup and background page that works -// in everywhere, even in private contexts (https://github.com/ipfs/ipfs-companion/issues/243) - -const browserActionPortName = 'browser-action-port' -var browserActionPort - -function onRuntimeConnect (port) { - // console.log('onConnect', port) - if (port.name === browserActionPortName) { - browserActionPort = port - browserActionPort.onMessage.addListener(handleMessageFromBrowserAction) - browserActionPort.onDisconnect.addListener(() => { browserActionPort = null }) - sendStatusUpdateToBrowserAction() + // PORTS (connection-based messaging) + // =================================================================== + // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/connect + // Make a connection between different contexts inside the add-on, + // e.g. signalling between browser action popup and background page that works + // in everywhere, even in private contexts (https://github.com/ipfs/ipfs-companion/issues/243) + + const browserActionPortName = 'browser-action-port' + var browserActionPort + + function onRuntimeConnect (port) { + // console.log('onConnect', port) + if (port.name === browserActionPortName) { + browserActionPort = port + browserActionPort.onMessage.addListener(handleMessageFromBrowserAction) + browserActionPort.onDisconnect.addListener(() => { browserActionPort = null }) + sendStatusUpdateToBrowserAction() + } } -} -const BrowserActionMessageHandlers = { - notification: (message) => notify(message.title, message.message), - copyCanonicalAddress: () => copier.copyCanonicalAddress(), - copyAddressAtPublicGw: () => copier.copyAddressAtPublicGw() -} + const BrowserActionMessageHandlers = { + notification: (message) => notify(message.title, message.message), + copyCanonicalAddress: () => copier.copyCanonicalAddress(), + copyAddressAtPublicGw: () => copier.copyAddressAtPublicGw() + } -function handleMessageFromBrowserAction (message) { - const handler = BrowserActionMessageHandlers[message && message.event] - if (!handler) return console.warn('Unknown browser action message event', message) - handler(message) -} + function handleMessageFromBrowserAction (message) { + const handler = BrowserActionMessageHandlers[message && message.event] + if (!handler) return console.warn('Unknown browser action message event', message) + handler(message) + } -async function sendStatusUpdateToBrowserAction () { - if (browserActionPort) { - const info = { - peerCount: state.peerCount, - gwURLString: state.gwURLString, - pubGwURLString: state.pubGwURLString, - currentTab: await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) - } - try { - let v = await ipfs.version() - if (v) { - info.gatewayVersion = v.commit ? v.version + '/' + v.commit : v.version + async function sendStatusUpdateToBrowserAction () { + if (browserActionPort) { + const info = { + peerCount: state.peerCount, + gwURLString: state.gwURLString, + pubGwURLString: state.pubGwURLString, + currentTab: await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) } - } catch (error) { - info.gatewayVersion = null - } - if (info.currentTab) { - info.ipfsPageActionsContext = isIpfsPageActionsContext(info.currentTab.url) + try { + let v = await ipfs.version() + if (v) { + info.gatewayVersion = v.commit ? v.version + '/' + v.commit : v.version + } + } catch (error) { + info.gatewayVersion = null + } + if (info.currentTab) { + info.ipfsPageActionsContext = isIpfsPageActionsContext(info.currentTab.url) + } + browserActionPort.postMessage({statusUpdate: info}) } - browserActionPort.postMessage({statusUpdate: info}) } -} -// GUI -// =================================================================== + // GUI + // =================================================================== -// contextMenus -// ------------------------------------------------------------------- -const contextMenuUploadToIpfs = 'contextMenu_UploadToIpfs' -const contextMenuCopyIpfsAddress = 'panelCopy_currentIpfsAddress' -const contextMenuCopyPublicGwUrl = 'panel_copyCurrentPublicGwUrl' - -try { - browser.contextMenus.create({ - id: contextMenuUploadToIpfs, - title: browser.i18n.getMessage(contextMenuUploadToIpfs), - contexts: ['image', 'video', 'audio'], - documentUrlPatterns: [''], - enabled: false, - onclick: addFromURL - }) - browser.contextMenus.create({ - id: contextMenuCopyIpfsAddress, - title: browser.i18n.getMessage(contextMenuCopyIpfsAddress), - contexts: ['page', 'image', 'video', 'audio', 'link'], - documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: () => copier.copyCanonicalAddress() - }) - browser.contextMenus.create({ - id: contextMenuCopyPublicGwUrl, - title: browser.i18n.getMessage(contextMenuCopyPublicGwUrl), - contexts: ['page', 'image', 'video', 'audio', 'link'], - documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: () => copier.copyAddressAtPublicGw() - }) -} catch (err) { - console.log('[ipfs-companion] Error creating contextMenus', err) -} + // contextMenus + // ------------------------------------------------------------------- + const contextMenuUploadToIpfs = 'contextMenu_UploadToIpfs' + const contextMenuCopyIpfsAddress = 'panelCopy_currentIpfsAddress' + const contextMenuCopyPublicGwUrl = 'panel_copyCurrentPublicGwUrl' -function inFirefox () { - return !!navigator.userAgent.match('Firefox') -} + try { + browser.contextMenus.create({ + id: contextMenuUploadToIpfs, + title: browser.i18n.getMessage(contextMenuUploadToIpfs), + contexts: ['image', 'video', 'audio'], + documentUrlPatterns: [''], + enabled: false, + onclick: addFromURL + }) + browser.contextMenus.create({ + id: contextMenuCopyIpfsAddress, + title: browser.i18n.getMessage(contextMenuCopyIpfsAddress), + contexts: ['page', 'image', 'video', 'audio', 'link'], + documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], + onclick: () => copier.copyCanonicalAddress() + }) + browser.contextMenus.create({ + id: contextMenuCopyPublicGwUrl, + title: browser.i18n.getMessage(contextMenuCopyPublicGwUrl), + contexts: ['page', 'image', 'video', 'audio', 'link'], + documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], + onclick: () => copier.copyAddressAtPublicGw() + }) + } catch (err) { + console.log('[ipfs-companion] Error creating contextMenus', err) + } + + function inFirefox () { + return !!navigator.userAgent.match('Firefox') + } -function preloadAtPublicGateway (path) { - // asynchronous HTTP HEAD request preloads triggers content without downloading it - return new Promise((resolve, reject) => { - const http = new XMLHttpRequest() - http.open('HEAD', urlAtPublicGw(path, state.pubGwURLString)) - http.onreadystatechange = function () { - if (this.readyState === this.DONE) { - console.info(`[ipfs-companion] preloadAtPublicGateway(${path}):`, this.statusText) - if (this.status === 200) { - resolve(this.statusText) - } else { - reject(new Error(this.statusText)) + function preloadAtPublicGateway (path) { + // asynchronous HTTP HEAD request preloads triggers content without downloading it + return new Promise((resolve, reject) => { + const http = new XMLHttpRequest() + http.open('HEAD', urlAtPublicGw(path, state.pubGwURLString)) + http.onreadystatechange = function () { + if (this.readyState === this.DONE) { + console.info(`[ipfs-companion] preloadAtPublicGateway(${path}):`, this.statusText) + if (this.status === 200) { + resolve(this.statusText) + } else { + reject(new Error(this.statusText)) + } } } - } - http.send() - }) -} + http.send() + }) + } -// URL Uploader -// ------------------------------------------------------------------- + // URL Uploader + // ------------------------------------------------------------------- -async function addFromURL (info) { - const srcUrl = await findUrlForContext(info) - try { - if (inFirefox()) { - // workaround due to https://github.com/ipfs/ipfs-companion/issues/227 - const fetchOptions = { - cache: 'force-cache', - referrer: info.pageUrl + async function addFromURL (info) { + const srcUrl = await findUrlForContext(info) + let result + try { + if (inFirefox()) { + // workaround due to https://github.com/ipfs/ipfs-companion/issues/227 + const fetchOptions = { + cache: 'force-cache', + referrer: info.pageUrl + } + // console.log('addFromURL.info', info) + // console.log('addFromURL.fetchOptions', fetchOptions) + const response = await fetch(srcUrl, fetchOptions) + const blob = await response.blob() + + const buffer = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => resolve(Buffer.from(reader.result)) + reader.onerror = reject + reader.readAsArrayBuffer(blob) + }) + + result = await ipfs.add(buffer) + } else { + result = await ipfs.util.addFromURL(srcUrl) } - // console.log('addFromURL.info', info) - // console.log('addFromURL.fetchOptions', fetchOptions) - const response = await fetch(srcUrl, fetchOptions) - const reader = new FileReader() - reader.onloadend = () => { - const buffer = ipfs.Buffer.from(reader.result) - ipfs.add(buffer, uploadResultHandler) + } catch (error) { + console.error(`Error for ${contextMenuUploadToIpfs}`, error) + if (error.message === 'NetworkError when attempting to fetch resource.') { + notify('notify_uploadErrorTitle', 'notify_uploadTrackingProtectionErrorMsg') + console.warn('IPFS upload often fails because remote file can not be downloaded due to Tracking Protection. See details at: https://github.com/ipfs/ipfs-companion/issues/227') + browser.tabs.create({ + 'url': 'https://github.com/ipfs/ipfs-companion/issues/227' + }) + } else { + notify('notify_uploadErrorTitle', 'notify_inlineErrorMsg', `${error.message}`) } - reader.readAsArrayBuffer(await response.blob()) - } else { - ipfs.util.addFromURL(srcUrl, uploadResultHandler) - } - } catch (error) { - console.error(`Error for ${contextMenuUploadToIpfs}`, error) - if (error.message === 'NetworkError when attempting to fetch resource.') { - notify('notify_uploadErrorTitle', 'notify_uploadTrackingProtectionErrorMsg') - console.warn('IPFS upload often fails because remote file can not be downloaded due to Tracking Protection. See details at: https://github.com/ipfs/ipfs-companion/issues/227') - browser.tabs.create({ - 'url': 'https://github.com/ipfs/ipfs-companion/issues/227' - }) - } else { - notify('notify_uploadErrorTitle', 'notify_inlineErrorMsg', `${error.message}`) + return } + + return uploadResultHandler(result) } -} -function uploadResultHandler (err, result) { - if (err || !result) { - console.error('[ipfs-companion] ipfs add error', err, result) - notify('notify_uploadErrorTitle', 'notify_inlineErrorMsg', `${err}`) - return - } - result.forEach(function (file) { - if (file && file.hash) { - const path = `/ipfs/${file.hash}` - browser.tabs.create({ - 'url': new URL(state.gwURLString + path).toString() - }) - console.info('[ipfs-companion] successfully stored', path) - if (state.preloadAtPublicGateway) { - preloadAtPublicGateway(path) + function uploadResultHandler (result) { + result.forEach(function (file) { + if (file && file.hash) { + const path = `/ipfs/${file.hash}` + browser.tabs.create({ + 'url': new URL(state.gwURLString + path).toString() + }) + console.info('[ipfs-companion] successfully stored', path) + if (state.preloadAtPublicGateway) { + preloadAtPublicGateway(path) + } } - } - }) -} - -window.uploadResultHandler = uploadResultHandler - -// Copying URLs -// ------------------------------------------------------------------- + }) + } -async function updateContextMenus (changedTabId) { - try { - await browser.contextMenus.update(contextMenuUploadToIpfs, {enabled: state.peerCount > 0}) - if (changedTabId) { - // recalculate tab-dependant menu items - const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) - if (currentTab && currentTab.id === changedTabId) { - const ipfsContext = isIpfsPageActionsContext(currentTab.url) - browser.contextMenus.update(contextMenuCopyIpfsAddress, {enabled: ipfsContext}) - browser.contextMenus.update(contextMenuCopyPublicGwUrl, {enabled: ipfsContext}) + async function updateContextMenus (changedTabId) { + try { + await browser.contextMenus.update(contextMenuUploadToIpfs, {enabled: state.peerCount > 0}) + if (changedTabId) { + // recalculate tab-dependant menu items + const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) + if (currentTab && currentTab.id === changedTabId) { + const ipfsContext = isIpfsPageActionsContext(currentTab.url) + browser.contextMenus.update(contextMenuCopyIpfsAddress, {enabled: ipfsContext}) + browser.contextMenus.update(contextMenuCopyPublicGwUrl, {enabled: ipfsContext}) + } } + } catch (err) { + console.log('[ipfs-companion] Error updating context menus', err) } - } catch (err) { - console.log('[ipfs-companion] Error updating context menus', err) } -} -// Page-specific Actions -// ------------------------------------------------------------------- + // Page-specific Actions + // ------------------------------------------------------------------- -// used in browser-action popup -// eslint-disable-next-line no-unused-vars -function isIpfsPageActionsContext (url) { - return IsIpfs.url(url) && !url.startsWith(state.apiURLString) -} + // used in browser-action popup + // eslint-disable-next-line no-unused-vars + function isIpfsPageActionsContext (url) { + return IsIpfs.url(url) && !url.startsWith(state.apiURLString) + } -async function onActivatedTab (activeInfo) { - await updateContextMenus(activeInfo.tabId) -} + async function onActivatedTab (activeInfo) { + await updateContextMenus(activeInfo.tabId) + } -async function onNavigationCommitted (details) { - await updateContextMenus(details.tabId) -} + async function onNavigationCommitted (details) { + await updateContextMenus(details.tabId) + } -async function onUpdatedTab (tabId, changeInfo, tab) { - if (changeInfo.status && changeInfo.status === 'complete' && tab.url && tab.url.startsWith('http')) { - if (state.linkify) { - console.info(`[ipfs-companion] Running linkfyDOM for ${tab.url}`) - try { - const browserApiPresent = (await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: "typeof browser !== 'undefined'" }))[0] - if (!browserApiPresent) { + async function onUpdatedTab (tabId, changeInfo, tab) { + if (changeInfo.status && changeInfo.status === 'complete' && tab.url && tab.url.startsWith('http')) { + if (state.linkify) { + console.info(`[ipfs-companion] Running linkfyDOM for ${tab.url}`) + try { + const browserApiPresent = (await browser.tabs.executeScript(tabId, { runAt: 'document_start', code: "typeof browser !== 'undefined'" }))[0] + if (!browserApiPresent) { + await browser.tabs.executeScript(tabId, { + file: '/dist/contentScripts/browser-polyfill.min.js', + matchAboutBlank: false, + allFrames: true, + runAt: 'document_start' + }) + } await browser.tabs.executeScript(tabId, { - file: '/dist/contentScripts/browser-polyfill.min.js', + file: '/dist/contentScripts/linkifyDOM.js', matchAboutBlank: false, allFrames: true, - runAt: 'document_start' + runAt: 'document_idle' }) + } catch (error) { + console.error(`Unable to linkify DOM at '${tab.url}' due to`, error) } - await browser.tabs.executeScript(tabId, { - file: '/dist/contentScripts/linkifyDOM.js', - matchAboutBlank: false, - allFrames: true, - runAt: 'document_idle' - }) - } catch (error) { - console.error(`Unable to linkify DOM at '${tab.url}' due to`, error) } - } - if (state.catchUnhandledProtocols) { - // console.log(`[ipfs-companion] Normalizing links with unhandled protocols at ${tab.url}`) - // See: https://github.com/ipfs/ipfs-companion/issues/286 - try { - // pass the URL of user-preffered public gateway - await browser.tabs.executeScript(tabId, { - code: `window.ipfsCompanionPubGwURL = '${state.pubGwURLString}'`, - matchAboutBlank: false, - allFrames: true, - runAt: 'document_start' - }) - // inject script that normalizes `href` and `src` containing unhandled protocols - await browser.tabs.executeScript(tabId, { - file: '/dist/contentScripts/normalizeLinksWithUnhandledProtocols.js', - matchAboutBlank: false, - allFrames: true, - runAt: 'document_end' - }) - } catch (error) { - console.error(`Unable to normalize links at '${tab.url}' due to`, error) + if (state.catchUnhandledProtocols) { + // console.log(`[ipfs-companion] Normalizing links with unhandled protocols at ${tab.url}`) + // See: https://github.com/ipfs/ipfs-companion/issues/286 + try { + // pass the URL of user-preffered public gateway + await browser.tabs.executeScript(tabId, { + code: `window.ipfsCompanionPubGwURL = '${state.pubGwURLString}'`, + matchAboutBlank: false, + allFrames: true, + runAt: 'document_start' + }) + // inject script that normalizes `href` and `src` containing unhandled protocols + await browser.tabs.executeScript(tabId, { + file: '/dist/contentScripts/normalizeLinksWithUnhandledProtocols.js', + matchAboutBlank: false, + allFrames: true, + runAt: 'document_end' + }) + } catch (error) { + console.error(`Unable to normalize links at '${tab.url}' due to`, error) + } } } } -} -// API STATUS UPDATES -// ------------------------------------------------------------------- -// API is polled for peer count every ipfsApiPollMs + // API STATUS UPDATES + // ------------------------------------------------------------------- + // API is polled for peer count every ipfsApiPollMs -const offlinePeerCount = -1 -const idleInSecs = 5 * 60 + const offlinePeerCount = -1 + const idleInSecs = 5 * 60 -var apiStatusUpdateInterval + var apiStatusUpdateInterval -async function setApiStatusUpdateInterval (ipfsApiPollMs) { - if (apiStatusUpdateInterval) { - clearInterval(apiStatusUpdateInterval) + async function setApiStatusUpdateInterval (ipfsApiPollMs) { + if (apiStatusUpdateInterval) { + clearInterval(apiStatusUpdateInterval) + } + apiStatusUpdateInterval = setInterval(() => runIfNotIdle(apiStatusUpdate), ipfsApiPollMs) + await apiStatusUpdate() } - apiStatusUpdateInterval = setInterval(() => runIfNotIdle(apiStatusUpdate), ipfsApiPollMs) - await apiStatusUpdate() -} -async function apiStatusUpdate () { - let oldPeerCount = state.peerCount - state.peerCount = await getSwarmPeerCount() - updatePeerCountDependentStates(oldPeerCount, state.peerCount) - sendStatusUpdateToBrowserAction() -} + async function apiStatusUpdate () { + let oldPeerCount = state.peerCount + state.peerCount = await getSwarmPeerCount() + updatePeerCountDependentStates(oldPeerCount, state.peerCount) + sendStatusUpdateToBrowserAction() + } -function updatePeerCountDependentStates (oldPeerCount, newPeerCount) { - updateAutomaticModeRedirectState(oldPeerCount, newPeerCount) - updateBrowserActionBadge() - updateContextMenus() -} + function updatePeerCountDependentStates (oldPeerCount, newPeerCount) { + updateAutomaticModeRedirectState(oldPeerCount, newPeerCount) + updateBrowserActionBadge() + updateContextMenus() + } -async function getSwarmPeerCount () { - try { - const peerInfos = await ipfs.swarm.peers() - return peerInfos.length - } catch (error) { - // console.error(`Error while ipfs.swarm.peers: ${err}`) - return offlinePeerCount + async function getSwarmPeerCount () { + try { + const peerInfos = await ipfs.swarm.peers() + return peerInfos.length + } catch (error) { + // console.error(`Error while ipfs.swarm.peers: ${err}`) + return offlinePeerCount + } } -} -async function runIfNotIdle (action) { - try { - const state = await browser.idle.queryState(idleInSecs) - if (state === 'active') { + async function runIfNotIdle (action) { + try { + const state = await browser.idle.queryState(idleInSecs) + if (state === 'active') { + return action() + } + } catch (error) { + console.error('Unable to read idle state, executing action without idle check', error) return action() } - } catch (error) { - console.error('Unable to read idle state, executing action without idle check', error) - return action() } -} -// browserAction -// ------------------------------------------------------------------- - -async function updateBrowserActionBadge () { - let badgeText, badgeColor, badgeIcon - badgeText = state.peerCount.toString() - if (state.peerCount > 0) { - // All is good (online with peers) - badgeColor = '#418B8E' - badgeIcon = '/icons/ipfs-logo-on.svg' - } else if (state.peerCount === 0) { - // API is online but no peers - badgeColor = 'red' - badgeIcon = '/icons/ipfs-logo-on.svg' - } else { - // API is offline - badgeText = '' - badgeColor = '#8C8C8C' - badgeIcon = '/icons/ipfs-logo-off.svg' - } - try { - await browser.browserAction.setBadgeBackgroundColor({color: badgeColor}) - await browser.browserAction.setBadgeText({text: badgeText}) - await setBrowserActionIcon(badgeIcon) - } catch (error) { - console.error('Unable to update browserAction badge due to error', error) + // browserAction + // ------------------------------------------------------------------- + + async function updateBrowserActionBadge () { + let badgeText, badgeColor, badgeIcon + badgeText = state.peerCount.toString() + if (state.peerCount > 0) { + // All is good (online with peers) + badgeColor = '#418B8E' + badgeIcon = '/icons/ipfs-logo-on.svg' + } else if (state.peerCount === 0) { + // API is online but no peers + badgeColor = 'red' + badgeIcon = '/icons/ipfs-logo-on.svg' + } else { + // API is offline + badgeText = '' + badgeColor = '#8C8C8C' + badgeIcon = '/icons/ipfs-logo-off.svg' + } + try { + await browser.browserAction.setBadgeBackgroundColor({color: badgeColor}) + await browser.browserAction.setBadgeText({text: badgeText}) + await setBrowserActionIcon(badgeIcon) + } catch (error) { + console.error('Unable to update browserAction badge due to error', error) + } } -} -async function setBrowserActionIcon (iconPath) { - let iconDefinition = {path: iconPath} - try { - // Try SVG first -- Firefox supports it natively - await browser.browserAction.setIcon(iconDefinition) - } catch (error) { - // Fallback! - // Chromium does not support SVG [ticket below is 8 years old, I can't even..] - // https://bugs.chromium.org/p/chromium/issues/detail?id=29683 - // Still, we want icon, so we precompute rasters of popular sizes and use them instead - await browser.browserAction.setIcon(rasterIconDefinition(iconPath)) + async function setBrowserActionIcon (iconPath) { + let iconDefinition = {path: iconPath} + try { + // Try SVG first -- Firefox supports it natively + await browser.browserAction.setIcon(iconDefinition) + } catch (error) { + // Fallback! + // Chromium does not support SVG [ticket below is 8 years old, I can't even..] + // https://bugs.chromium.org/p/chromium/issues/detail?id=29683 + // Still, we want icon, so we precompute rasters of popular sizes and use them instead + await browser.browserAction.setIcon(rasterIconDefinition(iconPath)) + } } -} -function rasterIconDefinition (svgPath) { - // icon sizes to cover ranges from: - // - https://bugs.chromium.org/p/chromium/issues/detail?id=647182 - // - https://developer.chrome.com/extensions/manifest/icons - return { - 'path': { - '19': rasterIconPath(svgPath, 19), - '38': rasterIconPath(svgPath, 38), - '128': rasterIconPath(svgPath, 128) + function rasterIconDefinition (svgPath) { + // icon sizes to cover ranges from: + // - https://bugs.chromium.org/p/chromium/issues/detail?id=647182 + // - https://developer.chrome.com/extensions/manifest/icons + return { + 'path': { + '19': rasterIconPath(svgPath, 19), + '38': rasterIconPath(svgPath, 38), + '128': rasterIconPath(svgPath, 128) + } } } -} - -function rasterIconPath (iconPath, size) { - // point at precomputed PNG file - let baseName = /\/icons\/(.+)\.svg/.exec(iconPath)[1] - return `/icons/png/${baseName}_${size}.png` -} -/* Easter-Egg: PoC that generates raster on the fly ;-) -function rasterIconData (iconPath, size) { - let icon = new Image() - icon.src = iconPath - let canvas = document.createElement('canvas') - let context = canvas.getContext('2d') - context.clearRect(0, 0, size, size) - context.drawImage(icon, 0, 0, size, size) - return context.getImageData(0, 0, size, size) -} -*/ + function rasterIconPath (iconPath, size) { + // point at precomputed PNG file + let baseName = /\/icons\/(.+)\.svg/.exec(iconPath)[1] + return `/icons/png/${baseName}_${size}.png` + } -// OPTIONS -// =================================================================== + /* Easter-Egg: PoC that generates raster on the fly ;-) + function rasterIconData (iconPath, size) { + let icon = new Image() + icon.src = iconPath + let canvas = document.createElement('canvas') + let context = canvas.getContext('2d') + context.clearRect(0, 0, size, size) + context.drawImage(icon, 0, 0, size, size) + return context.getImageData(0, 0, size, size) + } + */ + + // OPTIONS + // =================================================================== + + function updateAutomaticModeRedirectState (oldPeerCount, newPeerCount) { + // enable/disable gw redirect based on API going online or offline + if (state.automaticMode) { + if (oldPeerCount < 1 && newPeerCount > 0 && !state.redirect) { + browser.storage.local.set({useCustomGateway: true}) + .then(() => notify('notify_apiOnlineTitle', 'notify_apiOnlineAutomaticModeMsg')) + } else if (oldPeerCount > 0 && newPeerCount < 1 && state.redirect) { + browser.storage.local.set({useCustomGateway: false}) + .then(() => notify('notify_apiOfflineTitle', 'notify_apiOfflineAutomaticModeMsg')) + } + } + } -function updateAutomaticModeRedirectState (oldPeerCount, newPeerCount) { - // enable/disable gw redirect based on API going online or offline - if (state.automaticMode) { - if (oldPeerCount < 1 && newPeerCount > 0 && !state.redirect) { - browser.storage.local.set({useCustomGateway: true}) - .then(() => notify('notify_apiOnlineTitle', 'notify_apiOnlineAutomaticModeMsg')) - } else if (oldPeerCount > 0 && newPeerCount < 1 && state.redirect) { - browser.storage.local.set({useCustomGateway: false}) - .then(() => notify('notify_apiOfflineTitle', 'notify_apiOfflineAutomaticModeMsg')) + function onStorageChange (changes, area) { + for (let key in changes) { + let change = changes[key] + if (change.oldValue !== change.newValue) { + // debug info + // console.info(`Storage key "${key}" in namespace "${area}" changed. Old value was "${change.oldValue}", new value is "${change.newValue}".`) + if (key === 'ipfsApiUrl') { + state.apiURL = new URL(change.newValue) + state.apiURLString = state.apiURL.toString() + ipfs = initIpfsApi(state.apiURLString) + apiStatusUpdate() + } else if (key === 'ipfsApiPollMs') { + setApiStatusUpdateInterval(change.newValue) + } else if (key === 'customGatewayUrl') { + state.gwURL = new URL(change.newValue) + state.gwURLString = state.gwURL.toString() + } else if (key === 'publicGatewayUrl') { + state.pubGwURL = new URL(change.newValue) + state.pubGwURLString = state.pubGwURL.toString() + } else if (key === 'useCustomGateway') { + state.redirect = change.newValue + } else if (key === 'linkify') { + state.linkify = change.newValue + } else if (key === 'catchUnhandledProtocols') { + state.catchUnhandledProtocols = change.newValue + } else if (key === 'displayNotifications') { + state.displayNotifications = change.newValue + } else if (key === 'automaticMode') { + state.automaticMode = change.newValue + } else if (key === 'dnslink') { + state.dnslink = change.newValue + } else if (key === 'preloadAtPublicGateway') { + state.preloadAtPublicGateway = change.newValue + } + } } } -} -function onStorageChange (changes, area) { - for (let key in changes) { - let change = changes[key] - if (change.oldValue !== change.newValue) { - // debug info - // console.info(`Storage key "${key}" in namespace "${area}" changed. Old value was "${change.oldValue}", new value is "${change.newValue}".`) - if (key === 'ipfsApiUrl') { - state.apiURL = new URL(change.newValue) - state.apiURLString = state.apiURL.toString() - ipfs = window.ipfs = initIpfsApi(state.apiURLString) - apiStatusUpdate() - } else if (key === 'ipfsApiPollMs') { - setApiStatusUpdateInterval(change.newValue) - } else if (key === 'customGatewayUrl') { - state.gwURL = new URL(change.newValue) - state.gwURLString = state.gwURL.toString() - } else if (key === 'publicGatewayUrl') { - state.pubGwURL = new URL(change.newValue) - state.pubGwURLString = state.pubGwURL.toString() - } else if (key === 'useCustomGateway') { - state.redirect = change.newValue - } else if (key === 'linkify') { - state.linkify = change.newValue - } else if (key === 'catchUnhandledProtocols') { - state.catchUnhandledProtocols = change.newValue - } else if (key === 'displayNotifications') { - state.displayNotifications = change.newValue - } else if (key === 'automaticMode') { - state.automaticMode = change.newValue - } else if (key === 'dnslink') { - state.dnslink = change.newValue - } else if (key === 'preloadAtPublicGateway') { - state.preloadAtPublicGateway = change.newValue + // Public API + // (typically attached to a window variable to interact with this companion instance) + const api = { + get ipfs () { + return ipfs + }, + + async ipfsAddAndShow (buffer) { + let result + try { + result = await api.ipfs.add(buffer) + } catch (err) { + console.error('Failed to IPFS add', err) + notify('notify_uploadErrorTitle', 'notify_inlineErrorMsg', `${err.message}`) + throw err } + uploadResultHandler(result) + return result + }, + + destroy () { + clearInterval(apiStatusUpdateInterval) + apiStatusUpdateInterval = null + ipfs = null + state = null + dnsLink = null + modifyRequest = null + ipfsPathValidator = null + notify = null + copier = null } } + + return api } // OTHER diff --git a/add-on/src/popup/browser-action/store.js b/add-on/src/popup/browser-action/store.js index 607ca9bc3..dedc79b64 100644 --- a/add-on/src/popup/browser-action/store.js +++ b/add-on/src/popup/browser-action/store.js @@ -50,9 +50,9 @@ module.exports = (state, emitter) => { emitter.emit('render') try { - const bg = await getBackgroundPage() + const { ipfsCompanion } = await getBackgroundPage() const currentPath = await resolveToIPFS(new URL(state.currentTabUrl).pathname) - const pinResult = await bg.ipfs.pin.add(currentPath, { recursive: true }) + const pinResult = await ipfsCompanion.ipfs.pin.add(currentPath, { recursive: true }) console.log('ipfs.pin.add result', pinResult) notify('notify_pinnedIpfsResourceTitle', currentPath) state.isPinned = true @@ -69,9 +69,9 @@ module.exports = (state, emitter) => { emitter.emit('render') try { - const bg = await getBackgroundPage() + const { ipfsCompanion } = await getBackgroundPage() const currentPath = await resolveToIPFS(new URL(state.currentTabUrl).pathname) - const result = await bg.ipfs.pin.rm(currentPath, {recursive: true}) + const result = await ipfsCompanion.ipfs.pin.rm(currentPath, {recursive: true}) console.log('ipfs.pin.rm result', result) notify('notify_unpinnedIpfsResourceTitle', currentPath) state.isPinned = false @@ -175,9 +175,9 @@ module.exports = (state, emitter) => { async function updatePinnedState (status) { try { - const bg = await getBackgroundPage() + const { ipfsCompanion } = await getBackgroundPage() const currentPath = await resolveToIPFS(new URL(status.currentTab.url).pathname) - const response = await bg.ipfs.pin.ls(currentPath, {quiet: true}) + const response = await ipfsCompanion.ipfs.pin.ls(currentPath, {quiet: true}) console.log(`positive ipfs.pin.ls for ${currentPath}: ${JSON.stringify(response)}`) state.isPinned = true } catch (error) { @@ -200,10 +200,10 @@ function getBackgroundPage () { } async function resolveToIPFS (path) { - const bg = await getBackgroundPage() path = safeIpfsPath(path) // https://github.com/ipfs/ipfs-companion/issues/303 if (/^\/ipns/.test(path)) { - const response = await bg.ipfs.name.resolve(path, {recursive: true, nocache: false}) + const { ipfsCompanion } = await getBackgroundPage() + const response = await ipfsCompanion.ipfs.name.resolve(path, {recursive: true, nocache: false}) return response.Path } return path diff --git a/add-on/src/popup/quick-upload.js b/add-on/src/popup/quick-upload.js index 2a01e0734..10833a824 100644 --- a/add-on/src/popup/quick-upload.js +++ b/add-on/src/popup/quick-upload.js @@ -19,28 +19,25 @@ function quickUploadStore (state, emitter) { emitter.on('fileInputChange', async (event) => { const file = event.target.files[0] try { - const bg = await browser.runtime.getBackgroundPage() - let reader = new FileReader() - reader.onloadend = () => { - const buffer = Buffer.from(reader.result) - bg.ipfs.add(buffer, (err, result) => { - if (err || !result) { - // keep upload tab and display error message in it - state.message = `Unable to upload to IPFS API: ${err}` - emitter.emit('render') - } else { - // close upload tab as it will be replaced with a new tab with uploaded content - browser.tabs.getCurrent().then(tab => { - browser.tabs.remove(tab.id) - }) - } - // execute handler - return bg.uploadResultHandler(err, result) - }) - } - reader.readAsArrayBuffer(file) - } catch (error) { - console.error(`Unable to perform quick upload due to ${error}`) + const { ipfsCompanion } = await browser.runtime.getBackgroundPage() + + const buffer = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => resolve(Buffer.from(reader.result)) + reader.onerror = reject + reader.readAsArrayBuffer(file) + }) + + await ipfsCompanion.ipfsAddAndShow(buffer) + + // close upload tab as it will be replaced with a new tab with uploaded content + const tab = await browser.tabs.getCurrent() + browser.tabs.remove(tab.id) + } catch (err) { + console.error('Unable to perform quick upload', err) + // keep upload tab and display error message in it + state.message = `Unable to upload to IPFS API: ${err}` + emitter.emit('render') } }) } diff --git a/test/functional/lib/ipfs-companion.test.js b/test/functional/lib/ipfs-companion.test.js index bc506fd1f..cc66e1b21 100644 --- a/test/functional/lib/ipfs-companion.test.js +++ b/test/functional/lib/ipfs-companion.test.js @@ -21,9 +21,9 @@ describe('init', () => { it('should query local storage for options with hardcoded defaults for fallback', async () => { browser.storage.local.get.returns(Promise.resolve(optionDefaults)) browser.storage.local.set.returns(Promise.resolve()) - await init() + const ipfsCompanion = await init() browser.storage.local.get.calledWith(optionDefaults) - init.destroy() + ipfsCompanion.destroy() }) after(() => { @@ -58,7 +58,7 @@ describe.skip('onStorageChange()', function () { browser.contextMenus.update.returns(Promise.resolve()) browser.idle.queryState.returns(Promise.resolve('active')) - await init() + const ipfsCompanion = await init() const oldIpfsApiUrl = 'http://127.0.0.1:5001' const newIpfsApiUrl = 'http://1.2.3.4:8080' @@ -67,7 +67,7 @@ describe.skip('onStorageChange()', function () { const ipfs = global.window.ipfs browser.storage.onChanged.dispatch(changes, area) expect(ipfs).to.not.equal(window.ipfs) - init.destroy() + ipfsCompanion.destroy() }) after(() => { From 580859e2d210ec0b46d9eaf782991dd696b1f6a2 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 30 Nov 2017 11:10:16 +0000 Subject: [PATCH 3/6] fixes smothered error --- add-on/src/lib/ipfs-companion.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 3c36b677c..d1122b993 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -39,7 +39,8 @@ module.exports = async function init () { await storeMissingOptions(options, optionDefaults, browser.storage.local) } catch (error) { console.error('Unable to initialize addon due to error', error) - notify('notify_addonIssueTitle', 'notify_addonIssueMsg') + if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg') + throw error } function getState () { From 4cb62eeb223c8f943770cb0a9aa964eacf75c508 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 30 Nov 2017 11:45:47 +0000 Subject: [PATCH 4/6] fixes context menu update error --- add-on/src/lib/context-menus.js | 54 +++++++++++++++++++++++ add-on/src/lib/ipfs-companion.js | 76 ++++++-------------------------- add-on/src/lib/ipfs-path.js | 4 ++ 3 files changed, 71 insertions(+), 63 deletions(-) diff --git a/add-on/src/lib/context-menus.js b/add-on/src/lib/context-menus.js index b14c13cc8..2e6595f50 100644 --- a/add-on/src/lib/context-menus.js +++ b/add-on/src/lib/context-menus.js @@ -23,3 +23,57 @@ async function findUrlForContext (context) { } module.exports.findUrlForContext = findUrlForContext + +const contextMenuUploadToIpfs = 'contextMenu_UploadToIpfs' +const contextMenuCopyCanonicalAddress = 'panelCopy_currentIpfsAddress' +const contextMenuCopyAddressAtPublicGw = 'panel_copyCurrentPublicGwUrl' + +function createContextMenus (getState, ipfsPathValidator, { onUploadToIpfs, onCopyCanonicalAddress, onCopyAddressAtPublicGw }) { + browser.contextMenus.create({ + id: contextMenuUploadToIpfs, + title: browser.i18n.getMessage(contextMenuUploadToIpfs), + contexts: ['image', 'video', 'audio'], + documentUrlPatterns: [''], + enabled: false, + onclick: onUploadToIpfs + }) + + browser.contextMenus.create({ + id: contextMenuCopyCanonicalAddress, + title: browser.i18n.getMessage(contextMenuCopyCanonicalAddress), + contexts: ['page', 'image', 'video', 'audio', 'link'], + documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], + onclick: onCopyCanonicalAddress + }) + + browser.contextMenus.create({ + id: contextMenuCopyAddressAtPublicGw, + title: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw), + contexts: ['page', 'image', 'video', 'audio', 'link'], + documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], + onclick: onCopyAddressAtPublicGw + }) + + return { + async update (changedTabId) { + try { + await browser.contextMenus.update(contextMenuUploadToIpfs, {enabled: getState().peerCount > 0}) + if (changedTabId) { + // recalculate tab-dependant menu items + const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) + if (currentTab && currentTab.id === changedTabId) { + const ipfsContext = ipfsPathValidator.isIpfsPageActionsContext(currentTab.url) + browser.contextMenus.update(contextMenuCopyCanonicalAddress, {enabled: ipfsContext}) + browser.contextMenus.update(contextMenuCopyAddressAtPublicGw, {enabled: ipfsContext}) + } + } + } catch (err) { + console.log('[ipfs-companion] Error updating context menus', err) + } + } + + // TODO: destroy? + } +} + +module.exports.createContextMenus = createContextMenus diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index d1122b993..89d3942df 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -4,14 +4,13 @@ const browser = require('webextension-polyfill') const { optionDefaults, storeMissingOptions } = require('./options') const { initState } = require('./state') -const IsIpfs = require('is-ipfs') const IpfsApi = require('ipfs-api') const { createIpfsPathValidator, urlAtPublicGw } = require('./ipfs-path') const createDnsLink = require('./dns-link') const { createRequestModifier } = require('./ipfs-request') const createNotifier = require('./notifier') const createCopier = require('./copier') -const { findUrlForContext } = require('./context-menus') +const { createContextMenus, findUrlForContext } = require('./context-menus') // init happens on addon load in background/background.js module.exports = async function init () { @@ -24,6 +23,7 @@ module.exports = async function init () { var modifyRequest var notify var copier + var contextMenus try { const options = await browser.storage.local.get(optionDefaults) @@ -33,6 +33,11 @@ module.exports = async function init () { copier = createCopier(getState, notify) dnsLink = createDnsLink(getState) ipfsPathValidator = createIpfsPathValidator(getState, dnsLink) + contextMenus = createContextMenus(getState, ipfsPathValidator, { + onUploadToIpfs: addFromURL, + onCopyCanonicalAddress: () => copier.copyCanonicalAddress(), + onCopyAddressAtPublicGw: () => copier.copyAddressAtPublicGw() + }) modifyRequest = createRequestModifier(getState, dnsLink, ipfsPathValidator) registerListeners() await setApiStatusUpdateInterval(options.ipfsApiPollMs) @@ -150,7 +155,7 @@ module.exports = async function init () { info.gatewayVersion = null } if (info.currentTab) { - info.ipfsPageActionsContext = isIpfsPageActionsContext(info.currentTab.url) + info.ipfsPageActionsContext = ipfsPathValidator.isIpfsPageActionsContext(info.currentTab.url) } browserActionPort.postMessage({statusUpdate: info}) } @@ -159,39 +164,6 @@ module.exports = async function init () { // GUI // =================================================================== - // contextMenus - // ------------------------------------------------------------------- - const contextMenuUploadToIpfs = 'contextMenu_UploadToIpfs' - const contextMenuCopyIpfsAddress = 'panelCopy_currentIpfsAddress' - const contextMenuCopyPublicGwUrl = 'panel_copyCurrentPublicGwUrl' - - try { - browser.contextMenus.create({ - id: contextMenuUploadToIpfs, - title: browser.i18n.getMessage(contextMenuUploadToIpfs), - contexts: ['image', 'video', 'audio'], - documentUrlPatterns: [''], - enabled: false, - onclick: addFromURL - }) - browser.contextMenus.create({ - id: contextMenuCopyIpfsAddress, - title: browser.i18n.getMessage(contextMenuCopyIpfsAddress), - contexts: ['page', 'image', 'video', 'audio', 'link'], - documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: () => copier.copyCanonicalAddress() - }) - browser.contextMenus.create({ - id: contextMenuCopyPublicGwUrl, - title: browser.i18n.getMessage(contextMenuCopyPublicGwUrl), - contexts: ['page', 'image', 'video', 'audio', 'link'], - documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: () => copier.copyAddressAtPublicGw() - }) - } catch (err) { - console.log('[ipfs-companion] Error creating contextMenus', err) - } - function inFirefox () { return !!navigator.userAgent.match('Firefox') } @@ -245,7 +217,7 @@ module.exports = async function init () { result = await ipfs.util.addFromURL(srcUrl) } } catch (error) { - console.error(`Error for ${contextMenuUploadToIpfs}`, error) + console.error('Error in upload to IPFS context menu', error) if (error.message === 'NetworkError when attempting to fetch resource.') { notify('notify_uploadErrorTitle', 'notify_uploadTrackingProtectionErrorMsg') console.warn('IPFS upload often fails because remote file can not be downloaded due to Tracking Protection. See details at: https://github.com/ipfs/ipfs-companion/issues/227') @@ -276,38 +248,15 @@ module.exports = async function init () { }) } - async function updateContextMenus (changedTabId) { - try { - await browser.contextMenus.update(contextMenuUploadToIpfs, {enabled: state.peerCount > 0}) - if (changedTabId) { - // recalculate tab-dependant menu items - const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) - if (currentTab && currentTab.id === changedTabId) { - const ipfsContext = isIpfsPageActionsContext(currentTab.url) - browser.contextMenus.update(contextMenuCopyIpfsAddress, {enabled: ipfsContext}) - browser.contextMenus.update(contextMenuCopyPublicGwUrl, {enabled: ipfsContext}) - } - } - } catch (err) { - console.log('[ipfs-companion] Error updating context menus', err) - } - } - // Page-specific Actions // ------------------------------------------------------------------- - // used in browser-action popup - // eslint-disable-next-line no-unused-vars - function isIpfsPageActionsContext (url) { - return IsIpfs.url(url) && !url.startsWith(state.apiURLString) - } - async function onActivatedTab (activeInfo) { - await updateContextMenus(activeInfo.tabId) + await contextMenus.update(activeInfo.tabId) } async function onNavigationCommitted (details) { - await updateContextMenus(details.tabId) + await contextMenus.update(details.tabId) } async function onUpdatedTab (tabId, changeInfo, tab) { @@ -386,7 +335,7 @@ module.exports = async function init () { function updatePeerCountDependentStates (oldPeerCount, newPeerCount) { updateAutomaticModeRedirectState(oldPeerCount, newPeerCount) updateBrowserActionBadge() - updateContextMenus() + contextMenus.update() } async function getSwarmPeerCount () { @@ -569,6 +518,7 @@ module.exports = async function init () { ipfsPathValidator = null notify = null copier = null + contextMenus = null } } diff --git a/add-on/src/lib/ipfs-path.js b/add-on/src/lib/ipfs-path.js index fd3ec39b5..78ff4fd03 100644 --- a/add-on/src/lib/ipfs-path.js +++ b/add-on/src/lib/ipfs-path.js @@ -36,6 +36,10 @@ function createIpfsPathValidator (getState, dnsLink) { validIpfsOrIpnsPath (path) { return IsIpfs.ipfsPath(path) || validIpnsPath(path, dnsLink) + }, + + isIpfsPageActionsContext (url) { + return IsIpfs.url(url) && !url.startsWith(getState().apiURLString) } } From 09b1f9f37d3ca4a2647f666f77fbee77be3a4ca1 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 30 Nov 2017 12:50:39 +0000 Subject: [PATCH 5/6] disables context menus on Brave - not supported yet --- add-on/src/lib/context-menus.js | 53 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/add-on/src/lib/context-menus.js b/add-on/src/lib/context-menus.js index 2e6595f50..dc194fcec 100644 --- a/add-on/src/lib/context-menus.js +++ b/add-on/src/lib/context-menus.js @@ -29,30 +29,39 @@ const contextMenuCopyCanonicalAddress = 'panelCopy_currentIpfsAddress' const contextMenuCopyAddressAtPublicGw = 'panel_copyCurrentPublicGwUrl' function createContextMenus (getState, ipfsPathValidator, { onUploadToIpfs, onCopyCanonicalAddress, onCopyAddressAtPublicGw }) { - browser.contextMenus.create({ - id: contextMenuUploadToIpfs, - title: browser.i18n.getMessage(contextMenuUploadToIpfs), - contexts: ['image', 'video', 'audio'], - documentUrlPatterns: [''], - enabled: false, - onclick: onUploadToIpfs - }) + try { + browser.contextMenus.create({ + id: contextMenuUploadToIpfs, + title: browser.i18n.getMessage(contextMenuUploadToIpfs), + contexts: ['image', 'video', 'audio'], + documentUrlPatterns: [''], + enabled: false, + onclick: onUploadToIpfs + }) - browser.contextMenus.create({ - id: contextMenuCopyCanonicalAddress, - title: browser.i18n.getMessage(contextMenuCopyCanonicalAddress), - contexts: ['page', 'image', 'video', 'audio', 'link'], - documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: onCopyCanonicalAddress - }) + browser.contextMenus.create({ + id: contextMenuCopyCanonicalAddress, + title: browser.i18n.getMessage(contextMenuCopyCanonicalAddress), + contexts: ['page', 'image', 'video', 'audio', 'link'], + documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], + onclick: onCopyCanonicalAddress + }) - browser.contextMenus.create({ - id: contextMenuCopyAddressAtPublicGw, - title: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw), - contexts: ['page', 'image', 'video', 'audio', 'link'], - documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], - onclick: onCopyAddressAtPublicGw - }) + browser.contextMenus.create({ + id: contextMenuCopyAddressAtPublicGw, + title: browser.i18n.getMessage(contextMenuCopyAddressAtPublicGw), + contexts: ['page', 'image', 'video', 'audio', 'link'], + documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], + onclick: onCopyAddressAtPublicGw + }) + } catch (err) { + // documentUrlPatterns is not supported in brave + if (err.message.indexOf('createProperties.documentUrlPatterns of contextMenus.create is not supported yet') > -1) { + console.warn('[ipfs-companion] Context menus disabled - createProperties.documentUrlPatterns of contextMenus.create is not supported yet') + return { update: () => Promise.resolve() } + } + throw err + } return { async update (changedTabId) { From f4d2b98049a0f3ac6377e43f1d432f42c52e7d28 Mon Sep 17 00:00:00 2001 From: Oli Evans Date: Fri, 1 Dec 2017 16:51:12 +0000 Subject: [PATCH 6/6] Pull up vars to fix hositing issue --- add-on/src/lib/ipfs-companion.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/add-on/src/lib/ipfs-companion.js b/add-on/src/lib/ipfs-companion.js index 89d3942df..643ab4d44 100644 --- a/add-on/src/lib/ipfs-companion.js +++ b/add-on/src/lib/ipfs-companion.js @@ -24,6 +24,9 @@ module.exports = async function init () { var notify var copier var contextMenus + var apiStatusUpdateInterval + const offlinePeerCount = -1 + const idleInSecs = 5 * 60 try { const options = await browser.storage.local.get(optionDefaults) @@ -312,11 +315,6 @@ module.exports = async function init () { // ------------------------------------------------------------------- // API is polled for peer count every ipfsApiPollMs - const offlinePeerCount = -1 - const idleInSecs = 5 * 60 - - var apiStatusUpdateInterval - async function setApiStatusUpdateInterval (ipfsApiPollMs) { if (apiStatusUpdateInterval) { clearInterval(apiStatusUpdateInterval)