Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
105 changes: 81 additions & 24 deletions background_scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,69 @@ const HintCoordinator = {
},
};

let globallyDisabled = false;
const globallyDisabledLoaded = chrome.storage.local.get("globallyDisabled").then((items) => {
if (items.globallyDisabled != null) globallyDisabled = items.globallyDisabled;
});

function getIconSet() {
if (bgUtils.isFirefox()) {
return {
"enabled": "../icons/action_enabled.svg",
"partial": "../icons/action_partial.svg",
"disabled": "../icons/action_disabled.svg",
};
}
return {
"enabled": {
"16": "../icons/action_enabled_16.png",
"32": "../icons/action_enabled_32.png",
},
"partial": {
"16": "../icons/action_partial_16.png",
"32": "../icons/action_partial_32.png",
},
"disabled": {
"16": "../icons/action_disabled_16.png",
"32": "../icons/action_disabled_32.png",
},
};
}

async function toggleGloballyDisabled() {
globallyDisabled = !globallyDisabled;
chrome.storage.local.set({ globallyDisabled });
const iconSet = getIconSet();
const whichIcon = globallyDisabled ? "disabled" : "enabled";
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
chrome.action.setIcon({ path: iconSet[whichIcon], tabId: tab.id }).catch(() => {});
chrome.tabs.sendMessage(tab.id, {
handler: "toggleGloballyDisabled",
disabled: globallyDisabled,
}).catch(() => {});
}
}

chrome.commands.onCommand.addListener(async (command) => {
if (command === "toggleGloballyDisabled") {
await toggleGloballyDisabled();
}
});

chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.handler === "getGloballyDisabled") {
chrome.storage.local.get("globallyDisabled", (items) => {
sendResponse({ globallyDisabled: items.globallyDisabled ?? false });
});
return true;
} else if (request.handler === "toggleGloballyDisabled") {
toggleGloballyDisabled().then(() => sendResponse());
return true;
}
return false;
});

const sendRequestHandlers = {
runBackgroundCommand(request, sender) {
return BackgroundCommands[request.registryEntry.command](request, sender);
Expand Down Expand Up @@ -661,8 +724,14 @@ const sendRequestHandlers = {
async initializeFrame(request, sender) {
// Check whether the extension is enabled for the top frame's URL, rather than the URL of the
// specific frame that sent this request.
await globallyDisabledLoaded;
const enabledState = exclusions.isEnabledForUrl(sender.tab.url);

if (globallyDisabled) {
enabledState.isEnabledForUrl = false;
enabledState.passKeys = "";
}

const isTopFrame = sender.frameId == 0;
if (isTopFrame) {
let whichIcon;
Expand All @@ -674,30 +743,7 @@ const sendRequestHandlers = {
whichIcon = "enabled";
}

let iconSet = {
"enabled": {
"16": "../icons/action_enabled_16.png",
"32": "../icons/action_enabled_32.png",
},
"partial": {
"16": "../icons/action_partial_16.png",
"32": "../icons/action_partial_32.png",
},
"disabled": {
"16": "../icons/action_disabled_16.png",
"32": "../icons/action_disabled_32.png",
},
};

if (bgUtils.isFirefox()) {
// Only Firefox supports SVG icons.
iconSet = {
"enabled": "../icons/action_enabled.svg",
"partial": "../icons/action_partial.svg",
"disabled": "../icons/action_disabled.svg",
};
}

const iconSet = getIconSet();
chrome.action.setIcon({ path: iconSet[whichIcon], tabId: sender.tab.id });
}

Expand Down Expand Up @@ -926,6 +972,17 @@ Object.assign(globalThis, {
BackgroundCommands,
majorVersionHasIncreased,
nextZoomLevel,
toggleGloballyDisabled,
sendRequestHandlers,
});

Object.defineProperty(globalThis, "globallyDisabled", {
get() {
return globallyDisabled;
},
set(v) {
globallyDisabled = v;
},
});

// The chrome.runtime.onStartup and onInstalled events are not fired when disabling and then
Expand Down
13 changes: 12 additions & 1 deletion content_scripts/vimium_frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,15 @@ globalThis.lastFocusedInput = (function () {
})();

const messageHandlers = {
toggleGloballyDisabled(request) {
if (request.disabled) {
isEnabledForUrl = false;
HUD.show("Vimium disabled", 2000);
} else {
checkIfEnabledForUrl();
HUD.show("Vimium enabled", 2000);
}
},
getFocusStatus(_request, _sender) {
return {
focused: windowIsFocused(),
Expand Down Expand Up @@ -401,7 +410,9 @@ async function handleMessage(request, sender) {
// Some request are handled elsewhere in the code base; ignore them here.
const shouldHandleMessage = request.handler !== "userIsInteractingWithThePage" &&
(isEnabledForUrl ||
["checkEnabledAfterURLChange", "runInTopFrame"].includes(request.handler));
["checkEnabledAfterURLChange", "runInTopFrame", "toggleGloballyDisabled"].includes(
request.handler,
));
if (shouldHandleMessage) {
const result = await messageHandlers[request.handler](request, sender);
return result;
Expand Down
5 changes: 5 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
// "strict_min_version": "109.0"
// }
// },
"commands": {
"toggleGloballyDisabled": {
"description": "Toggle Vimium on/off"
}
},
"action": {
"default_icon": {
"16": "icons/action_disabled_16.png",
Expand Down
12 changes: 9 additions & 3 deletions pages/action.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@ h1 {
padding: var(--padding);
}

#dialog-body > * {
#globally-disabled-notice {
padding: var(--padding);
padding-right: 0;
flex-grow: 1;
}

#dialog-body > *, #globally-disabled-notice > * {
margin: 10px 0;
}

#dialog-body > *:first-child {
#dialog-body > *:first-child, #globally-disabled-notice > *:first-child {
margin-top: 0;
}

#dialog-body > *:last-child {
#dialog-body > *:last-child, #globally-disabled-notice > *:last-child {
margin-bottom: 0;
}

Expand Down
5 changes: 5 additions & 0 deletions pages/action.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ <h1>
</p>
</div>

<div id="globally-disabled-notice" style="display: none">
<div>Vimium is globally disabled.</div>
<div><button id="re-enable-vimium">Re-enable Vimium</button></div>
</div>

<div id="dialog-body">
<div>
<span id="how-many-enabled">All</span> Vimium keys are enabled on this page.
Expand Down
13 changes: 13 additions & 0 deletions pages/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ const ActionPage = {
return;
}

const { globallyDisabled } = await chrome.runtime.sendMessage({
handler: "getGloballyDisabled",
});
if (globallyDisabled) {
hideUI();
document.querySelector("#globally-disabled-notice").style.display = "block";
document.querySelector("#re-enable-vimium").addEventListener("click", async () => {
await chrome.runtime.sendMessage({ handler: "toggleGloballyDisabled" });
globalThis.close();
});
return;
}

document.querySelector("#optionsLink").href = chrome.runtime.getURL("pages/options.html");

const saveButton = document.querySelector("#save");
Expand Down
127 changes: 127 additions & 0 deletions tests/unit_tests/global_toggle_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import "./test_helper.js";
import "../../lib/settings.js";
import "../../background_scripts/main.js";

context("Global toggle", () => {
setup(async () => {
await Settings.onLoaded();
globallyDisabled = false;
await chrome.storage.local.clear();
});

teardown(async () => {
globallyDisabled = false;
await Settings.clear();
await chrome.storage.local.clear();
});

should("toggle globallyDisabled state", async () => {
assert.isFalse(globallyDisabled);
stub(chrome.tabs, "query", () => []);
await toggleGloballyDisabled();
assert.isTrue(globallyDisabled);
await toggleGloballyDisabled();
assert.isFalse(globallyDisabled);
});

should("persist state to chrome.storage.local", async () => {
stub(chrome.tabs, "query", () => []);
await toggleGloballyDisabled();
const stored = await chrome.storage.local.get("globallyDisabled");
assert.isTrue(stored.globallyDisabled);
});

should("update icon on all tabs when toggled", async () => {
const iconUpdates = [];
stub(chrome.tabs, "query", () => [{ id: 1 }, { id: 2 }]);
stub(chrome.tabs, "sendMessage", () => Promise.resolve());
stub(chrome.action, "setIcon", (args) => {
iconUpdates.push(args);
return Promise.resolve();
});
await toggleGloballyDisabled();
assert.equal(2, iconUpdates.length);
assert.equal(1, iconUpdates[0].tabId);
assert.equal(2, iconUpdates[1].tabId);
});

should("send toggleGloballyDisabled message to all tabs", async () => {
const sentMessages = [];
stub(chrome.tabs, "query", () => [{ id: 1 }, { id: 2 }]);
stub(chrome.tabs, "sendMessage", (tabId, message) => {
sentMessages.push({ tabId, message });
return Promise.resolve();
});
stub(chrome.action, "setIcon", () => Promise.resolve());
await toggleGloballyDisabled();
assert.equal(2, sentMessages.length);
assert.equal("toggleGloballyDisabled", sentMessages[0].message.handler);
assert.isTrue(sentMessages[0].message.disabled);
assert.equal(1, sentMessages[0].tabId);
assert.equal(2, sentMessages[1].tabId);
});
});

context("initializeFrame with global toggle", () => {
setup(async () => {
await Settings.onLoaded();
globallyDisabled = false;
await chrome.storage.local.clear();
});

teardown(async () => {
globallyDisabled = false;
await Settings.clear();
await chrome.storage.local.clear();
});

should("return isEnabledForUrl false when globally disabled", async () => {
globallyDisabled = true;
stub(chrome.action, "setIcon", () => Promise.resolve());
const sender = { tab: { url: "http://www.example.com/", id: 1 }, frameId: 0 };
const response = await sendRequestHandlers.initializeFrame({}, sender);
assert.isFalse(response.isEnabledForUrl);
assert.equal("", response.passKeys);
});

should("return isEnabledForUrl true when globally enabled", async () => {
globallyDisabled = false;
stub(chrome.action, "setIcon", () => Promise.resolve());
const sender = { tab: { url: "http://www.example.com/", id: 1 }, frameId: 0 };
const response = await sendRequestHandlers.initializeFrame({}, sender);
assert.isTrue(response.isEnabledForUrl);
});

should("set disabled icon when globally disabled", async () => {
globallyDisabled = true;
let iconPath = null;
stub(chrome.action, "setIcon", (args) => {
iconPath = args.path;
return Promise.resolve();
});
const sender = { tab: { url: "http://www.example.com/", id: 1 }, frameId: 0 };
await sendRequestHandlers.initializeFrame({}, sender);
assert.isTrue(iconPath["16"].includes("disabled"));
});

should("respect URL exclusion rules when globally enabled", async () => {
globallyDisabled = false;
await Settings.set("exclusionRules", [{ pattern: "http*://mail.google.com/*", passKeys: "" }]);
stub(chrome.action, "setIcon", () => Promise.resolve());
const sender = {
tab: { url: "http://mail.google.com/inbox", id: 1 },
frameId: 0,
};
const response = await sendRequestHandlers.initializeFrame({}, sender);
assert.isFalse(response.isEnabledForUrl);
});

should("override URL exclusion when globally disabled", async () => {
globallyDisabled = true;
await Settings.set("exclusionRules", []);
stub(chrome.action, "setIcon", () => Promise.resolve());
const sender = { tab: { url: "http://www.example.com/", id: 1 }, frameId: 0 };
const response = await sendRequestHandlers.initializeFrame({}, sender);
assert.isFalse(response.isEnabledForUrl);
});
});
12 changes: 12 additions & 0 deletions tests/unit_tests/test_chrome_stubs.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ globalThis.chrome = {
setBadgeBackgroundColor() {},
},

commands: {
onCommand: {
addListener() {},
},
},

action: {
setIcon() {
return Promise.resolve();
},
},

sessions: {
MAX_SESSION_RESULTS: 25,
},
Expand Down