Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mv3): Ask for Host Permissions if not exist. #1250

Merged
merged 7 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -786,5 +786,29 @@
"recovery_page_update_preferences": {
"message": "Update your IPFS Companion preferences",
"description": "Learn more link on the recovery screen (recovery_page_learn_more)"
},
"request_permissions_page_title": {
"message": "Grant Host Permissions | IPFS Companion",
"description": "Title of the recovery page (recovery_page_title)"
},
"request_permissions_page_sub_header": {
"message": "IPFS Companion needs additional permissions :(",
"description": "Sub-Header on the recovery screen (recovery_page_sub_header)"
},
"request_permissions_page_message_p1": {
"message": "Host permissions are required to access the web requests and interact with the IPFS web.",
"description": "Message Para-1 on the recovery screen (recovery_page_message_p1)"
},
"request_permissions_page_message_p2": {
"message": "Host permissions (permission to access data on all websites) are a new addition to the WebExtensions API, your browser considers it as an optional permission by default. IPFS companion needs this permission to intercept all web requests and modify those accordingly. Please click the button below to grant the required permissions.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Host permissions (permission to access data on all websites) are a new addition to the WebExtensions API, your browser considers it as an optional permission by default. IPFS companion needs this permission to intercept all web requests and modify those accordingly. Please click the button below to grant the required permissions.

We should communicate exactly what we're doing here. Can we link to your mv3 wiki / explainer doc?

I want to make sure we keep users from thinking that we're observing site-content or in some way profiting off of their site data. IIUC, the only thing we're doing is noticing that a URL is being loaded, checking if that URL is "an IPFS supporting website", and then adding a redirect rule & refreshing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct, and has been covered in the extension description, this screen only shows up if the permissions don't exist, would you like to take a stab at fixing the language which comes across as non-invasive but required permissions?

Copy link
Member

@lidel lidel Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've adjusted the language in 9453464 to make it less scary and more informative (focus on WHY we need it, not about API extravaganza :)).

2023-08-04_17-26

"description": "Message Para-2 on the recovery screen (recovery_page_message_p2)"
},
"request_permissions_page_button": {
"message": "Grant Host Permissions",
"description": "Button on the recovery screen (recovery_page_button)"
},
"request_permissions_page_learn_more": {
"message": "Learn more about host permissions",
"description": "Learn more link on the recovery screen (recovery_page_learn_more)"
}
}
54 changes: 54 additions & 0 deletions add-on/src/landing-pages/permissions/request.css
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file is copied from recovery.css, I have a todo item to make these more generic, will look into it after mv3.

Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +41 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah! !important usually ends up cascading need for important. A little too self-important of the property if you ask me.

to confirm, this is just for the permissions page, and in no way leaks into our other CSS, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is correct, I do have #1252 which will be a refactor at a later stage.

}

#left-col svg {
width: 60px;
}
}

.recovery-root {
width: 100%;
height: 100%;
text-align: left;
}
20 changes: 20 additions & 0 deletions add-on/src/landing-pages/permissions/request.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>IPFS Node is Offline</title>
<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/requestPermissionsPage.css">
</head>
<body class="navy bg-white sans-serif">
<app class="flex flex-column transition-all vh-100">
<main class="bg-white flex-grow-1">
<div id="root"></div>
</main>
<script src="/dist/bundles/uiCommons.bundle.js"></script>
<script src="/dist/bundles/requestPermissionsPage.bundle.js"></script>
</app>
</body>
</html>
56 changes: 56 additions & 0 deletions add-on/src/landing-pages/permissions/request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict'
/* eslint-env browser, webextensions */

import choo from 'choo'
import html from 'choo/html/index.js'
import { i18n, runtime, permissions } from 'webextension-polyfill'
import { nodeOffSvg } from '../welcome/page.js'
import createWelcomePageStore from '../welcome/store.js'
import { optionsPage } from '../../lib/constants.js'
import './request.css'

const app = choo()

const learnMoreLink = html`<a class="navy link underline-under hover-aqua" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/host_permissions#requested_permissions_and_user_prompts" target="_blank" rel="noopener noreferrer">${i18n.getMessage('request_permissions_page_learn_more')}</a>`

const optionsPageLink = html`<a class="navy link underline-under hover-aqua" id="learn-more" href="${optionsPage}" target="_blank" rel="noopener noreferrer">${i18n.getMessage('recovery_page_update_preferences')}</a>`

// TODO (whizzzkid): refactor base store to be more generic.
app.use(createWelcomePageStore(i18n, runtime))
// Register our single route
app.route('*', () => {
runtime.sendMessage({ telemetry: { trackView: 'request-permissions' } })
const requestPermission = async () => {
await permissions.request({ origins: ['<all_urls>'] })
runtime.reload()
}

return html`<div class="flex flex-column flex-row-l">
<div id="left-col" class="min-vh-100 flex flex-column justify-center items-center bg-navy white">
<div class="mb4 flex flex-column justify-center items-center">
${nodeOffSvg(200)}
<p class="mt0 mb0 f3 tc">${i18n.getMessage('request_permissions_page_sub_header')}</p>
</div>
</div>

<div id="right-col" class="pt7 mt5 w-100 flex flex-column justify-around items-center">
<p class="f3 fw5">${i18n.getMessage('request_permissions_page_message_p1')}</p>
<p class="f4 fw4">${i18n.getMessage('request_permissions_page_message_p2')}</p>
<button
class="fade-in ba bw1 b--teal bg-teal snow f7 ph2 pv3 br2 ma4 pointer"
onclick=${requestPermission}
>
<span class="f5 fw6">${i18n.getMessage('request_permissions_page_button')}</span>
</button>
<p class="f5 fw2 pt5">
${learnMoreLink} | ${optionsPageLink}
Copy link
Member

@lidel lidel Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: clicking on optionsPageLink here will also create a second tab with the same permission prompt.

We should check if the permission tab already exists, and if so, switch focus to it, instead of creating a second copy. Not a blocker, can be follow-up PR 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lidel I think that functionality should exist on the options page to make sure it doesn't get opened more than once.

Created: #1257

</span>
</div>
</div>`
})

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

// Set page title and header translation
document.title = i18n.getMessage('request_permissions_page_title')
1 change: 1 addition & 0 deletions add-on/src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,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 requestRequiredPermissionsPage = '/dist/landing-pages/permissions/request.html'
export const tickMs = 250 // no CPU spike, but still responsive enough
2 changes: 1 addition & 1 deletion add-on/src/lib/ipfs-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export function createRequestModifier (getState, dnslinkResolver, ipfsPathValida
const { requestHeaders } = request

if (isCompanionRequest(request)) {
// '403 - Forbidden' fix for Chrome and Firefox
// '403 - Forbidden' fix for Firefox
whizzzkid marked this conversation as resolved.
Show resolved Hide resolved
// --------------------------------------------
// We update "Origin: *-extension://" HTTP headers in requests made to API
// by js-kubo-rpc-client running in the background page of browser
Expand Down
11 changes: 10 additions & 1 deletion add-on/src/lib/on-installed.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import browser from 'webextension-polyfill'
import debug from 'debug'
import { welcomePage } from './constants.js'
import { requestRequiredPermissionsPage, welcomePage } from './constants.js'
import { brave, braveNodeType } from './ipfs-client/brave.js'

const { version } = browser.runtime.getManifest()
Expand All @@ -21,6 +21,15 @@ export async function onInstalled (details) {
export async function runPendingOnInstallTasks () {
const { onInstallTasks, displayReleaseNotes } = await browser.storage.local.get(['onInstallTasks', 'displayReleaseNotes'])
await browser.storage.local.remove('onInstallTasks')
// this is needed because `permissions.request` cannot be called from a script. If that happens the browser will
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this in this context exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this from the comment means why we have to do this here. js this here is just the function context.

// throws: Error: permissions.request may only be called from a user input handler
// To avoid this, we open a new tab with the permissions page and ask the user to grant the permissions.
// That makes the request valid and allows us to gain access to the permissions.
if (!(await browser.permissions.contains({ origins: ['<all_urls>'] }))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a user doesn't grant permissions in firefox, will they be constantly bombarded with the permissions page?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, if we don't have permissions then we cannot proceed. So:

  • user load the extension
  • this screen shows up
  • user clicks grant permission
  • user clicks deny
  • the extension refreshes and we're back to step 1.
  • if the allow, we go to the welcome screen.

We can as an improvement just show this once, if denied we load the extension in offline mode and show the button in the options screen. That's quite a bit more than just doin this. I can create a follow-up ticket if you'd like.

return browser.tabs.create({
url: requestRequiredPermissionsPage
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that I think about it a day later, I think this can also exist as a menu item on the options page, the only downside is the landing page will be options page before the welcome page. This way we isolate that.

Let me know if the reviewers feel this is an issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the same flow: initial landing page should be the welcome page.

We could polish the welcome page to check for permissions and have some messaging?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then the welcome page shows that the node is offline, we can either add this as notification on all the views or just keep it simple like this. What I believe might happen in the future is firefox changing their mind and make these permissions required instead of optional like chrome does.

})
}
switch (onInstallTasks) {
case 'onFirstInstall':
await useNativeNodeIfFeasible(browser)
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/recovery/recovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const optionsPageLink = html`<a class="navy link underline-under hover-aqua" id=
app.use(createWelcomePageStore(i18n, runtime))
// Register our single route
app.route('*', (state) => {
browser.runtime.sendMessage({ telemetry: { trackView: 'recovery' } })
runtime.sendMessage({ telemetry: { trackView: 'recovery' } })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this seems like it should be a separate fix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're not wrong, that's the source for this change, I think it was working like so, but just to make it consistent. It will go away in #1252

const { hash } = window.location
const { href: publicURI } = new URL(decodeURIComponent(hash.slice(1)))

Expand Down
3 changes: 2 additions & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ const uiConfig = merge(commonConfig, {
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'
welcomePage: './add-on/src/landing-pages/welcome/index.js',
requestPermissionsPage: './add-on/src/landing-pages/permissions/request.js',
},
optimization: {
splitChunks: {
Expand Down