Skip to content
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1c17668
Add host permission prompts for Safari
adomasven Jul 28, 2026
71b87d9
Restore intro paragraph in Chromium site-access prompt
dstillman Jul 31, 2026
70972e9
Prevent overlapping permission modals on the Safari preferences pane
dstillman Jul 31, 2026
6f9264e
Show a single Safari Settings instructions paragraph in combined prompts
dstillman Jul 31, 2026
dbff69d
Rework Safari localhost prompting in the save flow
dstillman Jul 31, 2026
3044593
Keep the Z toolbar icon on Safari until translator detection runs
dstillman Jul 31, 2026
50b2798
Show a neutral toolbar status on excluded pages without localhost access
dstillman Jul 31, 2026
ddbb46f
Expose HostPermissions.hasPermission() to injected pages
dstillman Aug 3, 2026
ade8a8e
Update Google Docs submodule
dstillman Aug 10, 2026
585d500
Reword the localhost permission explanation
dstillman Aug 3, 2026
34b0a5c
Show the localhost permission explanation for blocked integration req…
dstillman Aug 3, 2026
6a5441a
Stop pointing to Safari's permission dialog once a request has been b…
dstillman Aug 5, 2026
88e9d38
Recommend all-websites access in the pre-request permission explanation
dstillman Aug 6, 2026
5a63dc9
Recommend all-websites access once per browser session instead of onc…
dstillman Aug 6, 2026
af5ad1a
Reload pages where Safari re-injects content scripts after a permissi…
dstillman Aug 7, 2026
42d7373
Inject content scripts on demand for user actions on Safari
dstillman Aug 7, 2026
7f68174
Use plural Safari Settings instructions for multiple domains
dstillman Aug 7, 2026
4db8be5
Don't cache script-injection state when a page already has scripts
dstillman Aug 7, 2026
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
14 changes: 13 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ var injectIncludeFirefox = ['browser-polyfill.js'].concat(
['api.js'],
injectIncludeLast);

var injectIncludeSafari = ['browser-polyfill.js'].concat(
var injectIncludeSafari = ['reinjectGuard.js', 'browser-polyfill.js'].concat(
injectInclude,
['api.js'],
['frameMessaging.js'],
Expand Down Expand Up @@ -158,6 +158,7 @@ if (!argv.p) {
injectIncludeManifestV3.push('test/testInject.js');
}
var backgroundIncludeBrowserExt = ['browser-polyfill.js'].concat(backgroundInclude, [
'hostPermissions.js',
'webRequestIntercept.js',
'contentTypeHandler.js',
'saveWithoutProgressWindow.js',
Expand Down Expand Up @@ -334,6 +335,17 @@ function processFile() {
backgroundScripts.map((s) => `"${s}"`).join(',\n\t\t\t'))
.replace("/*INJECT SCRIPTS*/",
injectScripts.map((s) => `"${s}"`).join(',\n\t\t\t'))
if (basename == 'manifest.json' && browser == 'safari') {
// Safari runs content scripts only on sites where the user has granted
// access, so the pre-detection gray webpage icon can show indefinitely --
// default to the Z instead
let manifest = JSON.parse(contents);
manifest.browser_action.default_icon = {
16: "images/zotero-z-16px.png",
32: "images/zotero-z-32px.png"
};
contents = JSON.stringify(manifest, null, '\t');
}
}

contents = contents
Expand Down
86 changes: 29 additions & 57 deletions src/browserExt/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,14 @@ Zotero.Connector_Browser = new function() {
var isPDF = tabInfo.isPDF;
var translators = tabInfo.translators;

// Safari runs content scripts only on sites where the user has granted access, and
// clicking the button on other sites enables the Connector on the site instead of
// saving. Until a content script reports detection results, keep the default Z icon
// rather than showing a save action that hasn't been determined.
if (Zotero.isSafari && !translators && !isPDF) {
return;
}

// Show the save menu if we have more than one save option to show, which is true in all cases
// other than for PDFs with no translator
var showSaveMenu = (translators && translators.length) || !isPDF;
Expand Down Expand Up @@ -761,10 +769,11 @@ Zotero.Connector_Browser = new function() {
// it's not treated like we do it within a gesture
await browser.permissions.request({permissions: ['clipboardWrite']});
}
const shouldContinue = await _checkPermissions(tab);
const shouldContinue = await Zotero.HostPermissions.checkChromiumActionPermissions(tab);
if (!shouldContinue) {
return;
}
await _ensureScriptsInjected(tab);

// The PDF viewer in Chromium is apparently implemented as a special extension.
// If you right-click on the pdf-reader UI and select a Zotero option, the handler
Expand Down Expand Up @@ -798,7 +807,13 @@ Zotero.Connector_Browser = new function() {
if (isOnline) {
icon = "images/zotero-new-z-16px.png";
title = "Zotero is Online";
} else {
}
else if (isOnline === null) {
// Zotero's status is unknown without localhost access, so don't claim it's offline
icon = "images/zotero-new-z-16px.png";
title = "Zotero Connector";
}
else {
icon = "images/zotero-z-16px-offline.png";
title = "Zotero is Offline";
}
Expand Down Expand Up @@ -1039,68 +1054,25 @@ Zotero.Connector_Browser = new function() {
}

/**
* Check if we have permission to run on all sites.
* Prompts the user if permissions are insufficient.
* @param {Object} tab - The current tab object
* @returns {Promise<boolean>} - Returns false if the action should not proceed
* Safari doesn't run content scripts in tabs that are already open when the user grants
* site access, so inject them on demand before performing a user action, and give
* translator detection a moment to report before a save mode is chosen
*/
async function _checkPermissions(tab) {
// Firefox doesn't have per-site permissions in MV2.
if (Zotero.isFirefox) {
return true;
}

try {
const hasPermissions = await browser.permissions.contains({
origins: ["https://*/*"]
});

if (hasPermissions) {
return true;
}

const messageIntro = Zotero.getString("permissions_siteAccess_message_intro");
let promptProps = {
title: Zotero.getString("permissions_siteAccess_title"),
button1Text: Zotero.getString("permissions_siteAccess_openPreferences"),
button2Text: Zotero.getString("general_cancel"),
button3Text: Zotero.getString("general_continueAnyway"),
message: messageIntro + Zotero.getString("permissions_siteAccess_message")
};
if (Zotero.isSafari) {
promptProps = {
title: Zotero.getString("permissions_siteAccess_title"),
button1Text: Zotero.getString("general_cancel"),
button2Text: "",
button3Text: Zotero.getString("general_continueAnyway"),
message: messageIntro + Zotero.getString(
"permissions_siteAccess_message_safari",
Zotero.getString('appConnector', ZOTERO_CONFIG.CLIENT_NAME)
)
};
}

const result = await Zotero.Messaging.sendMessage('confirm', promptProps, tab);

if (result) {
if (!Zotero.isSafari && result.button === 1) {
browser.tabs.create({
url: `about:extensions/?id=${browser.runtime.id}`
});
}
return result.button === 3;
}
} catch (e) {
Zotero.debug('Error checking permissions: ' + e.message);
return true;
async function _ensureScriptsInjected(tab) {
if (!Zotero.isSafari) return;
await Zotero.Connector_Browser.injectTranslationScripts(tab);
let tabInfo = Zotero.Connector_Browser.getTabInfo(tab.id);
for (let i = 0; i < 30 && !tabInfo.translators; i++) {
await Zotero.Promise.delay(100);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If a page has no translators, doesn't this cause a 3 second delay every save?

@dstillman dstillman Aug 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, tabInfo.translators with no detected translators is an empty array, so it's not falsy.

But if the user navigates during the loop, this wouldn't get the new value, which might result in a stall, so I'll change this to get the tab info again on each loop.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

}

async function _browserAction(tab) {
const shouldContinue = await _checkPermissions(tab);
const shouldContinue = await Zotero.HostPermissions.checkChromiumActionPermissions(tab);
if (!shouldContinue) {
return;
}
await _ensureScriptsInjected(tab);

let tabInfo = Zotero.Connector_Browser.getTabInfo(tab.id);
if (_isBetaBuildBeyondExpiration) {
Expand Down
Loading