Add a gated external API for tab classification and saving - #637
Add a gated external API for tab classification and saving#637mlsimon734 wants to merge 1 commit into
Conversation
Exposes two versioned requests over runtime.onMessageExternal so a companion
extension can ask what the Connector already knows about a tab, rather than
approximating Zotero's translator coverage with its own hostname list:
{ "action": "getTabInfo", "version": 1, "tabId": 123 }
{ "action": "saveTab", "version": 1, "tabId": 123 }
getTabInfo returns the tab's current translator result and PDF state; saveTab
invokes the existing browser action and waits on the page-saving promise,
keeping the Chromium action-permission check and the script-injection guard in
the path.
Both are gated on a new externalAPI.allowedExtensions preference, an empty
array by default, following the shape of allowedInterceptHosts and
allowedCSLExtensionHosts so it is editable in the existing Config Editor with
no new preferences UI. A caller with no sender.id — every web page — is refused
outright. With the preference at its default the listener answers nothing, so
an unmodified install is unchanged.
|
Given the age of AI, we are quite a bit more concerned about various ways that malicious actors can use something like this to exfiltrate user data, or in other ways gain unauthorized access, and as such I'm more cautious about merging funcitonality like this, than when I was in 2018, when originally making a comment that we would accept a PR. 2 particular things come to mind given this specific changeset:
So if we wanted to accept something like this, at the very least, we should make the UI for configuring it explicit, in Zotero Connector Preferences. Moreover, every time a Zotero action is initiated by an external extension, the progress window, and also any prompt would have to clearly indicate, that the action was initiated by the external extension identified by its name, with a quick way to disable it. Even then, given the very limited number of users this would serve, and the relative complexity of additions needed, I'm not sure if this is something we would like to add. @abaevbog @AbeJellinek do you have any thoughts? |
|
Is there precedent in other OSS extensions for a cross-extension control API? I can't really think of any other extensions I've used that would want to expose something like this, but I'm sure it's been done. We'd want to emulate an existing secure approach. I'm not overly concerned about allowing silent access to the current detection status, but I think we should show a prompt before each extension-triggered translation that says the name of the extension and allows the user to allow, reject, or block the extension. If we did that, given how limited the API we're exposing here is, I wouldn't worry too much about potential extension hijacking, and I don't think we absolutely need a configuration UI. If an extension became malicious, the worst it could do would be to trigger saves, which the user could reject/block. |
|
I think I'm mainly in agreement with @adomasven here. I'm somewhat wary of the possibility for item save to get triggered from elsewhere without an immediately clear way for the user (and us for troubleshooting) to see it. I'm also not aware of any examples of similar cross-extensions API. Besides, I believe we somewhat assume that the page is in the state the user is happy with when translation happens. When the snapshot is being saved, what you see if what you get. It gets tricky if that is no longer the case - like what happens if, say, the selected tab is unloaded? Relying on Zotero local API for detection of the page feels like a somewhat safer path to me despite extra work. |
Background
Posted to zotero-dev on 2026-08-17 and had no replies, so this is the same proposal as
a reviewable diff: https://groups.google.com/g/zotero-dev/c/Gd-U-z0rh10
The request is older than that thread. In #60, @cmcaine (Tridactyl) asked whether the
Connector would expose a function to other add-ons, and sketched the same design this
patch implements:
That issue was closed on the unrelated keyboard-shortcut question, so the API question was
never answered either way. The allowlist is the one thing that has changed since the
zotero-dev post, which described a proof of concept that accepted calls from any
extension. It no longer does.
Problem
Tabglutton is a companion browser extension that manages tab backlogs and can route scholarly tabs to Zotero. Without a Connector-facing classification API, a companion extension has to approximate Zotero's translator coverage with a hostname list for arXiv, OpenReview, PubMed, bioRxiv, publishers, journals, proxies, PDFs, and other sources. That duplicates logic the Connector has already run for each tab and will drift from Zotero's actual site coverage.
Change
With the new preference at its default, nothing in this patch runs. The listener is registered but refuses every message, so an unmodified install behaves exactly as it does today.
This adds two versioned requests over the standard
runtime.onMessageExternalpath:{ "action": "getTabInfo", "version": 1, "tabId": 123 } { "action": "saveTab", "version": 1, "tabId": 123 }For an authorized caller,
getTabInforeturns the Connector's current result for that tab:{ "ok": true, "state": "ready", "isPDF": false, "translator": { "itemType": "preprint", "label": "arXiv.org" } }stateisdetectingwhile translator detection has not completed and no PDF has been identified.translatoris omitted when the Connector has no translator result. This is the load-bearing half of the API: a genericsaveTaboperation can invoke the toolbar workflow, but it cannot tell the caller which tabs are papers. Returning the already-computed top translator type and PDF state avoids reproducing Zotero's translator coverage outside Zotero.saveTabinvokes the existing browser action for the requested tab and waits for the page-saving promise. Its successful response is:{ "ok": true, "status": "saved" }The existing Chromium action-permission check and script-injection guard remain in the path. A cancelled permission or injection check returns
cancelled, and surfaced save failures returnerrorinstead of reportingsaved. The request also retains the Connector's first-use interaction check. Unsupported versions, unknown actions, and invalid tab IDs receive explicit error statuses.Why not
/connector/detectRaised as an open question in the zotero-dev post, and worth answering here.
The Connector has already run translator detection in the page for every tab the user has
open, against the live DOM.
getTabInforeturns that existing per-tab result. Asking theclient to detect instead would mean recomputing from content the calling extension would
have to gather and ship, which is a different and weaker answer for JS-rendered pages —
and it would not cover the PDF case the Connector already tracks per tab.
That is the reasoning, not a measurement. If
/connector/detectis the route maintainerswould rather see taken, that changes the shape of this patch substantially and is worth
knowing before any of it is reviewed in detail.
Authorization
The listener is disabled by default. A caller must have a
sender.id, and that extension ID must appear in the newexternalAPI.allowedExtensionspreference. The preference defaults to an empty array. Web pages have nosender.idand are refused outright.This is the whitelist suggested in #60. It uses the same static-array preference shape as
allowedInterceptHostsandallowedCSLExtensionHosts. Users can edit it in the Connector's existing Config Editor, so this change does not add preferences UI. A static allowlist is a deliberate minimal authorization model, not a claim that it is the ideal long-term interaction: a first-use approval prompt would avoid asking users to find and type an extension ID.Verification
The following results were measured in the original proof of concept:
build/firefox/andbuild/manifestv3/, with the external listener present in both generated backgrounds.getTabInfoclassifiedhttps://arxiv.org/abs/1706.03762as{ "itemType": "preprint", "label": "arXiv.org" }. The same setup exercised the full selected-tab route: the Connector saved the paper to Zotero, the companion extension reported one Zotero save with no failures, and the source tab closed.c279ccc61d80f99b8d9275e9315d05cb66617f2e, both Connector targets built with the API in their generated backgrounds. In a live Firefox 134.0.2 run with both temporary add-ons,getTabInforeturnedunauthorizeduntilexternalAPI.allowedExtensionscontainedtabglutton@addons.local, then returned{ "state": "ready", "isPDF": false, "translator": { "itemType": "preprint", "label": "arXiv.org" } }for the arXiv abstract. A bad version returnedunsupported-version, and a bad action returnedunknown-action.Prefs.setis already proxied from pages to the background, and the same session that wrote the preference was then authorized by it.saveTabwas exercised in Firefox 134.0.2 against a live Zotero client. It returned{ "ok": true, "status": "saved" }in 1.7 seconds; one second later the library contained apreprinttitled "Attention Is All You Need" with a note and a PDF attachment. The run was repeated successfully in a profile where the Connector had never been clicked.webExtension.installcommand, so the end-to-end Firefox browser smoke setup remained manual.The following are deliberate limits rather than measured guarantees:
savedmeans the Connector's page-saving operation resolved without a surfaced failure. It is not independent verification that the resulting library item exists.Tests
No automated tests are included. The Connector's suite has no existing coverage for
onMessageExternal, and the behaviour that matters here — authorization, translatorstate, and a save that resolves — was exercised in live browsers instead, as recorded
above. Happy to add tests in whatever shape maintainers prefer.
Current upstream
The patch was originally written against
48ad1fe0and was previously ported toc279ccc61d80f99b8d9275e9315d05cb66617f2e, where the browser action had moved toZotero.HostPermissions.checkChromiumActionPermissionsand gained the_ensureScriptsInjectedguard. This version has now been regenerated and verified against master97a8b413bcf9e03e5c586d945076f7418f885f04(2026-08-27).The
c279cccpatch applied cleanly with no behavioral changes needed. Since that port, the touched files gained an unrelatedPageSaving.onSaveAsWebpagedebug message and the unrelatedsingleFileConfigpreference; neither changed this API's integration points.At
97a8b413,npm installand./build.sh -dcompleted successfully. The generatedbuild/firefox/background.jsandbuild/manifestv3/background.jsboth contain_handleExternalMessageand theruntime.onMessageExternallistener.