summaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-02-11 18:55:09 -0500
committerGitHub <noreply@github.com>2021-02-11 18:55:09 -0500
commit94db6c69fa4aa25231e213c6d0e185197bfe3418 (patch)
tree711696bc44954c7e480ac7ed6d528f11dd2ac580 /ext/bg/js
parent07cd006127c23c17e5c750122a4ac6916994bc76 (diff)
Permissions button in browser action popup (#1368)
* Add key icon * Update context icon styles * Add permissions links * Show warning badge if permissions are insufficient for certain settings * Create PermissionsUtil * Use PermissionsUtil in Backend * Update SettingsController to use PermissionsUtil * Update AnkiController to use getRequiredPermissionsForAnkiFieldValue * Show the permissions buttons/links on the context page when necessary * Update MV3 compatibility
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/backend.js38
-rw-r--r--ext/bg/js/context-main.js37
-rw-r--r--ext/bg/js/permissions-util.js126
-rw-r--r--ext/bg/js/settings/anki-controller.js28
-rw-r--r--ext/bg/js/settings/backup-controller.js2
-rw-r--r--ext/bg/js/settings/permissions-toggle-controller.js8
-rw-r--r--ext/bg/js/settings/settings-controller.js52
7 files changed, 195 insertions, 96 deletions
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index e5f8466e..3dd1955f 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -28,6 +28,7 @@
* MediaUtility
* ObjectPropertyAccessor
* OptionsUtil
+ * PermissionsUtil
* ProfileConditions
* RequestBuilder
* Translator
@@ -83,6 +84,8 @@ class Backend {
this._defaultBrowserActionTitle = null;
this._badgePrepareDelayTimer = null;
this._logErrorLevel = null;
+ this._permissions = null;
+ this._permissionsUtil = new PermissionsUtil();
this._messageHandlers = new Map([
['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}],
@@ -174,12 +177,17 @@ class Backend {
const onMessage = this._onMessageWrapper.bind(this);
chrome.runtime.onMessage.addListener(onMessage);
+
+ const onPermissionsChanged = this._onWebExtensionEventWrapper(this._onPermissionsChanged.bind(this));
+ chrome.permissions.onAdded.addListener(onPermissionsChanged);
+ chrome.permissions.onRemoved.addListener(onPermissionsChanged);
}
async _prepareInternal() {
try {
this._prepareInternalSync();
+ this._permissions = await this._permissionsUtil.getAllPermissions();
this._defaultBrowserActionTitle = await this._getBrowserIconTitle();
this._badgePrepareDelayTimer = setTimeout(() => {
this._badgePrepareDelayTimer = null;
@@ -357,6 +365,10 @@ class Backend {
this._sendMessageTabIgnoreResponse(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}});
}
+ _onPermissionsChanged() {
+ this._checkPermissions();
+ }
+
// Message handlers
_onApiRequestBackendReadySignal(_params, sender) {
@@ -682,7 +694,7 @@ class Backend {
let permissionsOkay = false;
try {
- permissionsOkay = await this._hasPermissions({permissions: ['nativeMessaging']});
+ permissionsOkay = await this._permissionsUtil.hasPermissions({permissions: ['nativeMessaging']});
} catch (e) {
// NOP
}
@@ -1263,6 +1275,10 @@ class Backend {
text = 'off';
color = '#555555';
status = 'Disabled';
+ } else if (!this._hasRequiredPermissionsForSettings(options)) {
+ text = '!';
+ color = '#f0ad4e';
+ status = 'Some settings require additional permissions';
} else if (!this._isAnyDictionaryEnabled(options)) {
text = '!';
color = '#f0ad4e';
@@ -1941,17 +1957,6 @@ class Backend {
});
}
- _hasPermissions(permissions) {
- return new Promise((resolve, reject) => chrome.permissions.contains(permissions, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- }));
- }
-
_getTabById(tabId) {
return new Promise((resolve, reject) => {
chrome.tabs.get(
@@ -1967,4 +1972,13 @@ class Backend {
);
});
}
+
+ async _checkPermissions() {
+ this._permissions = await this._permissionsUtil.getAllPermissions();
+ this._updateBadge();
+ }
+
+ _hasRequiredPermissionsForSettings(options) {
+ return this._permissions === null || this._permissionsUtil.hasRequiredPermissionsForOptions(this._permissions, options);
+ }
}
diff --git a/ext/bg/js/context-main.js b/ext/bg/js/context-main.js
index 3d9c90ab..a7ea1471 100644
--- a/ext/bg/js/context-main.js
+++ b/ext/bg/js/context-main.js
@@ -17,12 +17,14 @@
/* global
* HotkeyHelpController
+ * PermissionsUtil
* api
*/
class DisplayController {
constructor() {
this._optionsFull = null;
+ this._permissionsUtil = new PermissionsUtil();
}
async prepare() {
@@ -40,6 +42,7 @@ class DisplayController {
const optionsPageUrl = optionsFull.global.useSettingsV2 ? '/bg/settings2.html' : manifest.options_ui.page;
this._setupButtonEvents('.action-open-settings', 'openSettingsPage', chrome.runtime.getURL(optionsPageUrl));
+ this._setupButtonEvents('.action-open-permissions', null, chrome.runtime.getURL('/bg/permissions.html'));
const {profiles, profileCurrent} = optionsFull;
const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null;
@@ -68,16 +71,18 @@ class DisplayController {
_setupButtonEvents(selector, command, url) {
const nodes = document.querySelectorAll(selector);
for (const node of nodes) {
- node.addEventListener('click', (e) => {
- if (e.button !== 0) { return; }
- api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'});
- e.preventDefault();
- }, false);
- node.addEventListener('auxclick', (e) => {
- if (e.button !== 1) { return; }
- api.commandExec(command, {mode: 'newTab'});
- e.preventDefault();
- }, false);
+ if (typeof command === 'string') {
+ node.addEventListener('click', (e) => {
+ if (e.button !== 0) { return; }
+ api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'});
+ e.preventDefault();
+ }, false);
+ node.addEventListener('auxclick', (e) => {
+ if (e.button !== 1) { return; }
+ api.commandExec(command, {mode: 'newTab'});
+ e.preventDefault();
+ }, false);
+ }
if (typeof url === 'string') {
node.href = url;
@@ -131,6 +136,7 @@ class DisplayController {
toggle.addEventListener('change', onToggleChanged, false);
}
this._updateDictionariesEnabledWarnings(options);
+ this._updatePermissionsWarnings(options);
}
async _setupHotkeys() {
@@ -201,6 +207,17 @@ class DisplayController {
node.hidden = hasEnabledDictionary;
}
}
+
+ async _updatePermissionsWarnings(options) {
+ const permissions = await this._permissionsUtil.getAllPermissions();
+ if (this._permissionsUtil.hasRequiredPermissionsForOptions(permissions, options)) { return; }
+
+ const warnings = document.querySelectorAll('.action-open-permissions,.permissions-required-warning');
+ for (const node of warnings) {
+ console.log(node);
+ node.hidden = false;
+ }
+ }
}
(async () => {
diff --git a/ext/bg/js/permissions-util.js b/ext/bg/js/permissions-util.js
new file mode 100644
index 00000000..bd3a18ce
--- /dev/null
+++ b/ext/bg/js/permissions-util.js
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+class PermissionsUtil {
+ constructor() {
+ this._ankiFieldMarkersRequiringClipboardPermission = new Set([
+ 'clipboard-image',
+ 'clipboard-text'
+ ]);
+ this._ankiMarkerPattern = /\{([\w-]+)\}/g;
+ }
+
+ hasPermissions(permissions) {
+ return new Promise((resolve, reject) => chrome.permissions.contains(permissions, (result) => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(result);
+ }
+ }));
+ }
+
+ setPermissionsGranted(permissions, shouldHave) {
+ return (
+ shouldHave ?
+ new Promise((resolve, reject) => chrome.permissions.request(permissions, (result) => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(result);
+ }
+ })) :
+ new Promise((resolve, reject) => chrome.permissions.remove(permissions, (result) => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(!result);
+ }
+ }))
+ );
+ }
+
+ getAllPermissions() {
+ return new Promise((resolve, reject) => chrome.permissions.getAll((result) => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve(result);
+ }
+ }));
+ }
+
+ getRequiredPermissionsForAnkiFieldValue(fieldValue) {
+ const markers = this._getAnkiFieldMarkers(fieldValue);
+ const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission;
+ for (const marker of markers) {
+ if (markerPermissions.has(marker)) {
+ return ['clipboardRead'];
+ }
+ }
+ return [];
+ }
+
+ hasRequiredPermissionsForOptions(permissions, options) {
+ const permissionsSet = new Set(permissions.permissions);
+
+ if (!permissionsSet.has('nativeMessaging')) {
+ if (options.parsing.enableMecabParser) {
+ return false;
+ }
+ }
+
+ if (!permissionsSet.has('clipboardRead')) {
+ if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) {
+ return false;
+ }
+ const fieldMarkersRequiringClipboardPermission = this._ankiFieldMarkersRequiringClipboardPermission;
+ const fieldsList = [
+ options.anki.terms.fields,
+ options.anki.kanji.fields
+ ];
+ for (const fields of fieldsList) {
+ for (const fieldValue of Object.values(fields)) {
+ const markers = this._getAnkiFieldMarkers(fieldValue);
+ for (const marker of markers) {
+ if (fieldMarkersRequiringClipboardPermission.has(marker)) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // Private
+
+ _getAnkiFieldMarkers(fieldValue) {
+ const pattern = this._ankiMarkerPattern;
+ const markers = [];
+ let match;
+ while ((match = pattern.exec(fieldValue)) !== null) {
+ markers.push(match[1]);
+ }
+ return markers;
+ }
+}
diff --git a/ext/bg/js/settings/anki-controller.js b/ext/bg/js/settings/anki-controller.js
index cb6922b8..db3e3c14 100644
--- a/ext/bg/js/settings/anki-controller.js
+++ b/ext/bg/js/settings/anki-controller.js
@@ -34,10 +34,6 @@ class AnkiController {
onRemoved: this._removeCardController.bind(this),
isStale: this._isCardControllerStale.bind(this)
});
- this._fieldMarkersRequiringClipboardPermission = new Set([
- 'clipboard-image',
- 'clipboard-text'
- ]);
this._stringComparer = new Intl.Collator(); // Locale does not matter
this._getAnkiDataPromise = null;
this._ankiErrorContainer = null;
@@ -157,13 +153,7 @@ class AnkiController {
}
getRequiredPermissions(fieldValue) {
- const markers = this._getFieldMarkers(fieldValue);
- for (const marker of markers) {
- if (this._fieldMarkersRequiringClipboardPermission.has(marker)) {
- return ['clipboardRead'];
- }
- }
- return [];
+ return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue);
}
containsAnyMarker(field) {
@@ -338,16 +328,6 @@ class AnkiController {
this._ankiErrorMessageDetailsToggle.hidden = false;
}
- _getFieldMarkers(fieldValue) {
- const pattern = /\{([\w-]+)\}/g;
- const markers = [];
- let match;
- while ((match = pattern.exec(fieldValue)) !== null) {
- markers.push(match[1]);
- }
- return markers;
- }
-
_sortStringArray(array) {
const stringComparer = this._stringComparer;
array.sort((a, b) => stringComparer.compare(a, b));
@@ -656,7 +636,7 @@ class AnkiCardController {
async _requestPermissions(permissions) {
try {
- await this._settingsController.setPermissionsGranted(permissions, true);
+ await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true);
} catch (e) {
yomichan.logError(e);
}
@@ -669,8 +649,8 @@ class AnkiCardController {
node.dataset.requiredPermission = permissions.join(' ');
const hasPermissions = await (
request ?
- this._settingsController.setPermissionsGranted(permissions, true) :
- this._settingsController.hasPermissions(permissions)
+ this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true) :
+ this._settingsController.permissionsUtil.hasPermissions({permissions})
);
node.dataset.hasPermissions = `${hasPermissions}`;
} else {
diff --git a/ext/bg/js/settings/backup-controller.js b/ext/bg/js/settings/backup-controller.js
index 34817ee9..8837b927 100644
--- a/ext/bg/js/settings/backup-controller.js
+++ b/ext/bg/js/settings/backup-controller.js
@@ -87,7 +87,7 @@ class BackupController {
const optionsFull = await this._settingsController.getOptionsFull();
const environment = await api.getEnvironmentInfo();
const fieldTemplatesDefault = await api.getDefaultAnkiFieldTemplates();
- const permissions = await this._settingsController.getAllPermissions();
+ const permissions = await this._settingsController.permissionsUtil.getAllPermissions();
// Format options
for (const {options} of optionsFull.profiles) {
diff --git a/ext/bg/js/settings/permissions-toggle-controller.js b/ext/bg/js/settings/permissions-toggle-controller.js
index 04c8f3f2..f80e7585 100644
--- a/ext/bg/js/settings/permissions-toggle-controller.js
+++ b/ext/bg/js/settings/permissions-toggle-controller.js
@@ -71,13 +71,13 @@ class PermissionsToggleController {
if (value || !hasPermissionsSetting) {
toggle.checked = valuePre;
- const requiredPermissions = this._getRequiredPermissions(toggle);
+ const permissions = this._getRequiredPermissions(toggle);
try {
- value = await this._settingsController.setPermissionsGranted(requiredPermissions, value);
+ value = await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, value);
} catch (error) {
value = valuePre;
try {
- value = await this._settingsController.hasPermissions(requiredPermissions);
+ value = await this._settingsController.permissionsUtil.hasPermissions({permissions});
} catch (error2) {
// NOP
}
@@ -113,7 +113,7 @@ class PermissionsToggleController {
}
async _updateValidity() {
- const permissions = await this._settingsController.getAllPermissions();
+ const permissions = await this._settingsController.permissionsUtil.getAllPermissions();
this._onPermissionsChanged({permissions});
}
diff --git a/ext/bg/js/settings/settings-controller.js b/ext/bg/js/settings/settings-controller.js
index a3885ef6..11a9435c 100644
--- a/ext/bg/js/settings/settings-controller.js
+++ b/ext/bg/js/settings/settings-controller.js
@@ -18,6 +18,7 @@
/* global
* HtmlTemplateCollection
* OptionsUtil
+ * PermissionsUtil
* api
*/
@@ -29,6 +30,7 @@ class SettingsController extends EventDispatcher {
this._pageExitPreventions = new Set();
this._pageExitPreventionEventListeners = new EventListenerCollection();
this._templates = new HtmlTemplateCollection(document);
+ this._permissionsUtil = new PermissionsUtil();
}
get source() {
@@ -44,6 +46,10 @@ class SettingsController extends EventDispatcher {
this._setProfileIndex(value);
}
+ get permissionsUtil() {
+ return this._permissionsUtil;
+ }
+
prepare() {
yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this));
chrome.permissions.onAdded.addListener(this._onPermissionsChanged.bind(this));
@@ -134,50 +140,6 @@ class SettingsController extends EventDispatcher {
return optionsFull;
}
- hasPermissions(permissions) {
- return new Promise((resolve, reject) => chrome.permissions.contains({permissions}, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- }));
- }
-
- setPermissionsGranted(permissions, shouldHave) {
- return (
- shouldHave ?
- new Promise((resolve, reject) => chrome.permissions.request({permissions}, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- })) :
- new Promise((resolve, reject) => chrome.permissions.remove({permissions}, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(!result);
- }
- }))
- );
- }
-
- getAllPermissions() {
- return new Promise((resolve, reject) => chrome.permissions.getAll((result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- }));
- }
-
// Private
_setProfileIndex(value) {
@@ -242,7 +204,7 @@ class SettingsController extends EventDispatcher {
const event = 'permissionsChanged';
if (!this.hasListeners(event)) { return; }
- const permissions = await this.getAllPermissions();
+ const permissions = await this._permissionsUtil.getAllPermissions();
this.trigger(event, {permissions});
}
}