Skip to content

Commit 96a194e

Browse files
committed
extension: better handling of missing permissions
- open extension settings on error - preserve popup state on unsuccessful capture - workaround for manifest v3 stuff in end2end tests
1 parent 9daca10 commit 96a194e

File tree

6 files changed

+96
-46
lines changed

6 files changed

+96
-46
lines changed

extension/generate_manifest.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,15 @@ export function generateManifest({
155155

156156
if (v3) {
157157
if (target === T.FIREFOX) {
158-
// firefox doesn't support optional host permissions
158+
// firefox doesn't support optional_host_permissions yet
159+
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1766026
160+
// and https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/optional_permissions#host_permissions
159161
// note that these will still have to be granted by user (unlike in chrome)
160-
manifest['host_permissions'] = [...host_permissions, ...optional_host_permissions]
162+
manifest.host_permissions = host_permissions
163+
manifest.optional_permissions.push(...optional_host_permissions)
161164
} else {
162-
manifest['host_permissions'] = host_permissions
163-
manifest['optional_host_permissions'] = optional_host_permissions
165+
manifest.host_permissions = host_permissions
166+
manifest.optional_host_permissions = optional_host_permissions
164167
}
165168
} else {
166169
manifest.permissions.push(...host_permissions)

extension/src/background.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ async function makeCaptureRequest(
5959
// We can't call ensurePermissions here, getting "permissions.request may only be called from a user input handler" error
6060
// even though it's called from the keyboard shortcut???
6161
// so the best we can do is try to check and at least show a more helful error
62+
// also relevant https://bugzilla.mozilla.org/show_bug.cgi?id=1811608
6263

6364
const has = await hasPermissions(endpoint)
6465
if (!has) {
65-
throw new Error(`${endpoint}: no permissions detected! Go to extension settings and approve them.`)
66+
// kinda awkward to open options page here etc, but fine for now
67+
browser.tabs.create({url: 'options.html'})
68+
throw new Error(`${endpoint}: no permissions detected!\nApprove the endpoint in extension settings, and repeat capture after that.`)
6669
}
6770

6871
const data = JSON.stringify(params)
@@ -96,14 +99,16 @@ async function makeCaptureRequest(
9699
}
97100

98101

99-
// TODO ugh. need defensive error handling on the very top...
100-
async function capture(comment: string | null = null, tag_str: string | null = null) {
102+
async function capture(comment: string | null = null, tag_str: string | null = null): Promise<boolean> {
103+
/**
104+
* Returns whether capture has succeeded
105+
*/
101106
const tabs = await browser.tabs.query({currentWindow: true, active: true})
102107
const tab = tabs[0]
103108
if (tab.url == null) {
104-
// todo await?
109+
// todo when does this happen?
105110
showNotification('ERROR: trying to capture null')
106-
return
111+
return false
107112
}
108113
const url: string = tab.url
109114
const title: string | null = tab.title || null
@@ -126,8 +131,6 @@ async function capture(comment: string | null = null, tag_str: string | null = n
126131
let selection
127132
if (has_scripting) {
128133
const tab_id = tab.id as number
129-
// TODO switch to polyfill and add flow types
130-
// scripting is already promise based so it should be oly change to types
131134
const results = await browser.scripting.executeScript({
132135
target: {tabId: tab_id},
133136
func: () => window.getSelection()!.toString()
@@ -143,35 +146,47 @@ async function capture(comment: string | null = null, tag_str: string | null = n
143146

144147
try {
145148
await makeCaptureRequest(payload(selection), opts)
149+
return true
146150
} catch (err) {
147151
console.error(err)
148152
// todo make sure we catch errors from then clause too?
149153
const error_message = `ERROR: ${(err as Error).toString()}`
150154
console.error(error_message)
151-
showNotification(error_message, 1)
152-
// TODO crap, doesn't really seem to respect urgency...
155+
showNotification(error_message, 1) // todo crap, doesn't really seem to respect urgency...
156+
return false
153157
}
154158
}
155159

156160

157-
browser.commands.onCommand.addListener(command => {
161+
browser.commands.onCommand.addListener((command: string) => {
158162
if (command === COMMAND_CAPTURE_SIMPLE) {
159-
// todo await
160-
capture(null, null)
163+
// not checking return value here, can't really do much?
164+
capture(null, null) // todo await?
161165
}
162166
})
163167

164168

165-
browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse: () => void) => {
169+
// ok so sadly it seems like async listener doesn't really work in chrome due to a bug
170+
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage#sending_an_asynchronous_response_using_a_promise
171+
// also see https://stackoverflow.com/questions/44056271/chrome-runtime-onmessage-response-with-async-await
172+
browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse: (_arg: any) => void) => {
166173
if (message.method === 'logging') {
167174
console.error("[%s] %o", message.source, message.data)
168175
}
169176
if (message.method === METHOD_CAPTURE_WITH_EXTRAS) {
170177
const comment = message.comment
171178
const tag_str = message.tag_str
172-
// todo await
173-
capture(comment, tag_str)
174-
sendResponse()
179+
180+
// NOTE: calling async directly doesn't work here in firefox
181+
// (getting "Could not establish connection. Receiving end does not exist" error)
182+
// I guess has something to do with micro (async) vs macro (setTimeout) tasks
183+
// although if anything I'd expect macro tasks to work worse :shrug:
184+
setTimeout(async () => {
185+
// this will be handled in the popup
186+
const success = await capture(comment, tag_str)
187+
sendResponse({success: success})
188+
})
189+
return true // means 'async response'
175190
}
176191
if (message.method == 'DARK_MODE') {
177192
const icon_path = message.hasDarkMode ? 'img/unicorn_dark.png' : 'img/unicorn.png'
@@ -181,6 +196,3 @@ browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse:
181196
action.setIcon({path: icon_path})
182197
}
183198
})
184-
185-
// TODO handle cannot access chrome:// url??
186-

extension/src/options.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
</style>
1111
</head>
1212
<body>
13+
<h1>Grasp settings</h1>
1314
<fieldset>
1415
<legend>Endpoint</legend>
15-
<input type='URL' id='endpoint_id'/>
16+
<input type='URL' size='40' id='endpoint_id'/>
1617
<div id='has_permission_id' style="display: none; color: red">
1718
No permission to access the endpoint. Will request when you press "Save".
1819
</div>

extension/src/popup.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,24 @@ async function restoreState(state: State | null) {
8484
}
8585

8686

87-
function submitCapture () {
87+
async function submitCapture () {
8888
const state = getUiState()
8989

9090
const message = {
9191
'method': METHOD_CAPTURE_WITH_EXTRAS,
9292
...state,
9393
}
9494

95-
browser.runtime.sendMessage(message).then((_: any) => {
95+
const result = await browser.runtime.sendMessage(message)
96+
if (result.success) {
9697
// @ts-expect-error
9798
window.justSubmitted = true
9899
clearState()
99-
100-
// TODO not sure about this?
101-
window.close()
102100
console.log("[popup] captured!")
103-
})
104-
101+
} else {
102+
// if capture wasn't successful, keep the state intact
103+
}
104+
window.close()
105105
}
106106

107107

tests/addon.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,29 @@ class OptionsPage:
3838
def open(self) -> None:
3939
self.helper.open_page(self.helper.options_page_name)
4040

41+
def check_opened(self) -> None:
42+
current_url = self.helper.driver.current_url
43+
assert current_url.endswith(self.helper.options_page_name), current_url # just in case
44+
45+
def save(self, *, wait_for_permissions: bool = False) -> None:
46+
self.check_opened()
47+
48+
driver = self.helper.driver
49+
50+
se = driver.find_element('id', 'save_id')
51+
se.click()
52+
53+
if wait_for_permissions:
54+
# we can't accept this alert via webdriver, it's a native chrome alert, not DOM
55+
click.confirm(click.style('You should see prompt for permissions. Accept them', blink=True, fg='yellow'), abort=True)
56+
57+
alert = driver.switch_to.alert
58+
assert alert.text == 'Saved!', alert.text # just in case
59+
alert.accept()
60+
4161
def change_endpoint(self, endpoint: str, *, wait_for_permissions: bool = False) -> None:
62+
self.check_opened()
63+
4264
driver = self.helper.driver
4365

4466
current_url = driver.current_url
@@ -52,16 +74,7 @@ def change_endpoint(self, endpoint: str, *, wait_for_permissions: bool = False)
5274
ep.clear()
5375
ep.send_keys(endpoint)
5476

55-
se = driver.find_element('id', 'save_id')
56-
se.click()
57-
58-
if wait_for_permissions:
59-
# we can't accept this alert via webdriver, it's a native chrome alert, not DOM
60-
click.confirm(click.style('You should see prompt for permissions. Accept them', blink=True, fg='yellow'), abort=True)
61-
62-
alert = driver.switch_to.alert
63-
assert alert.text == 'Saved!', alert.text # just in case
64-
alert.accept()
77+
self.save(wait_for_permissions=wait_for_permissions)
6578

6679

6780
@dataclass
@@ -74,7 +87,12 @@ def open(self) -> None:
7487
def enter_data(self, *, comment: str, tags: str) -> None:
7588
helper = self.addon.helper
7689

77-
helper.gui_hotkey('tab') # give focus to the input
90+
if helper.driver.name == 'firefox':
91+
# for some reason in firefox under geckodriver it woudn't focus comment input field??
92+
# tried both regular and dev edition firefox with latest geckodriver
93+
# works fine when extension is loaded in firefox manually or in chrome with chromedriver..
94+
# TODO file a bug??
95+
helper.gui_hotkey('tab') # give focus to the input
7896

7997
helper.gui_write(comment)
8098

tests/test_end2end.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def confirm(what: str) -> None:
8080

8181
# chrome v3 works
8282
# firefox v2 works
83-
# firefox v3 BROKEN -- needs the user to approve the localhost access
83+
# firefox v3 works (although a little more elaborate due to additional approvals)
8484
def test_capture_no_configuration(addon: Addon) -> None:
8585
"""
8686
This checks that capture works with default hostname/port without opening settings first
@@ -96,21 +96,36 @@ def test_capture_no_configuration(addon: Addon) -> None:
9696

9797
addon.quick_capture()
9898

99+
if addon.helper.driver.name == 'firefox' and addon.helper.manifest_version == 3:
100+
# Seems like if v3 firefox, localhost permissions aren't granted by default
101+
# (despite being declared in host_permissions manifest)
102+
# so the above will result in error + opening options page so the user can approve
103+
time.sleep(0.5) # meh. give the options page time to open
104+
[orig_page, options_page] = driver.window_handles
105+
driver.switch_to.window(options_page) # meh. is there a better way??
106+
addon.options_page.save(wait_for_permissions=True)
107+
driver.close() # close settings
108+
driver.switch_to.window(orig_page) # meh. is there a better way??
109+
110+
addon.quick_capture() # retry capture
111+
99112
confirm('Should show a successful capture notification, and the link should be in your default capture file')
100113

101114

102115
# chrome v3 works
103116
# firefox v2 works
104-
# firefox v3 BROKEN (sort of)
105-
# seems like manifest v3 is prompting for permission even if we only change port
117+
# firefox v3 works
106118
def test_capture_bad_port(addon: Addon) -> None:
107119
"""
108120
Check that we get error notification instead of silently failing if the endpoint is wrong
109121
"""
110122
driver = addon.helper.driver
111123

112124
addon.options_page.open()
113-
addon.options_page.change_endpoint(endpoint='http://localhost:12345/capture')
125+
126+
# seems like manifest v3 in firefox is prompting for permission even if we only change port
127+
wait_for_permissions = addon.helper.driver.name == 'firefox' and addon.helper.manifest_version == 3
128+
addon.options_page.change_endpoint(endpoint='http://localhost:12345/capture', wait_for_permissions=wait_for_permissions)
114129

115130
driver.get('https://example.com')
116131

@@ -200,3 +215,4 @@ def test_capture_with_extra_data(addon: Addon, server: Server) -> None:
200215
note
201216
'''
202217
)
218+
confirm("Popup should be closed")

0 commit comments

Comments
 (0)