From 04482f43860916eeb5a8b0affb887c8680a8a390 Mon Sep 17 00:00:00 2001 From: introt Date: Thu, 14 Apr 2022 04:00:52 +0000 Subject: [PATCH] requestPermission: improve UX & docs (#312) * requestPermission: change Ignore button to checkbox * Update requestPermission documentation - describe the possible results - document ignoreOriginList --- README.md | 17 ++++++--- plugin/__init__.py | 89 ++++++++++++++++++++++++---------------------- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index a7966d4..f6180dd 100644 --- a/README.md +++ b/README.md @@ -1503,11 +1503,18 @@ corresponding to when the API was available for use. * **requestPermission** - Request permission to use the API exposed by this plugin. Only request coming from origin listed in the - `webCorsOriginList` option are allowed to use the Api. Calling this method will display a popup asking the user - if he want to allow your origin to use the Api. This is the only method that can be called even if the origin of - the request isn't in the `webCorsOriginList` list. It also doesn't require the api key. Calling this method will - not display the popup if the origin is already trusted. + Requests permission to use the API exposed by this plugin. This method does not require the API key, and is the + only one that accepts requests from any origin; the other methods only accept requests from trusted origins, + which are listed under `webCorsOriginList` in the add-on config. `localhost` is trusted by default. + + Calling this method from an untrusted origin will display a popup in Anki asking the user whether they want to + allow your origin to use the API; calls from trusted origins will return the result without displaying the popup. + When denying permission, the user may also choose to ignore further permission requests from that origin. These + origins end up in the `ignoreOriginList`, editable via the add-on config. + + The result always contains the `permission` field, which in turn contains either the string `granted` or `denied`, + corresponding to whether your origin is trusted. If your origin is trusted, the fields `requireApiKey` (`true` if + required) and `version` will also be returned. This should be the first call you make to make sure that your application and Anki-Connect are able to communicate properly with each other. New versions of Anki-Connect are backwards compatible; as long as you are using actions diff --git a/plugin/__init__.py b/plugin/__init__.py index d405ac1..68e4235 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -28,7 +28,7 @@ import unicodedata from PyQt5 import QtCore from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtWidgets import QMessageBox, QCheckBox import anki import anki.exporting @@ -358,53 +358,56 @@ class AnkiConnect: def version(self): return util.setting('apiVersion') + @util.api() def requestPermission(self, origin, allowed): + results = { + "permission": "denied", + } + if allowed: - return { - "permission": "granted", - "requireApikey": bool(util.setting('apiKey')), - "version": util.setting('apiVersion') - } - - if origin in util.setting('ignoreOriginList') : - return { - "permission": "denied", - } - - msg = QMessageBox(None) - msg.setWindowTitle("A website want to access to Anki") - msg.setText(origin + " request permission to use Anki through AnkiConnect.\nDo you want to give it access ?") - msg.setInformativeText("By giving permission, the website will be able to do actions on anki, including destructives actions like deck deletion.") - msg.setWindowIcon(self.window().windowIcon()) - msg.setIcon(QMessageBox.Question) - msg.setStandardButtons(QMessageBox.Yes|QMessageBox.Ignore|QMessageBox.No) - msg.setDefaultButton(QMessageBox.No) - msg.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) - pressedButton = msg.exec_() - - if pressedButton == QMessageBox.Yes: - config = aqt.mw.addonManager.getConfig(__name__) - config["webCorsOriginList"] = util.setting('webCorsOriginList') - config["webCorsOriginList"].append(origin) - aqt.mw.addonManager.writeConfig(__name__, config) - - if pressedButton == QMessageBox.Ignore: - config = aqt.mw.addonManager.getConfig(__name__) - config["ignoreOriginList"] = util.setting('ignoreOriginList') - config["ignoreOriginList"].append(origin) - aqt.mw.addonManager.writeConfig(__name__, config) - - if pressedButton == QMessageBox.Yes: results = { - "permission": "granted", - "requireApikey": bool(util.setting('apiKey')), - "version": util.setting('apiVersion') - } - else : - results = { - "permission": "denied", + "permission": "granted", + "requireApikey": bool(util.setting('apiKey')), + "version": util.setting('apiVersion') } + + elif origin in util.setting('ignoreOriginList'): + pass # defaults to denied + + else: # prompt the user + msg = QMessageBox(None) + msg.setWindowTitle("A website wants to access to Anki") + msg.setText('"{}" requests permission to use Anki through AnkiConnect. Do you want to give it access?'.format(origin)) + msg.setInformativeText("By granting permission, you'll allow the website to modify your collection on your behalf, including the execution of destructive actions such as deck deletion.") + msg.setWindowIcon(self.window().windowIcon()) + msg.setIcon(QMessageBox.Question) + msg.setStandardButtons(QMessageBox.Yes|QMessageBox.No) + msg.setDefaultButton(QMessageBox.No) + msg.setCheckBox(QCheckBox(text='Ignore further requests from "{}"'.format(origin), parent=msg)) + msg.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + pressedButton = msg.exec_() + + if pressedButton == QMessageBox.Yes: + config = aqt.mw.addonManager.getConfig(__name__) + config["webCorsOriginList"] = util.setting('webCorsOriginList') + config["webCorsOriginList"].append(origin) + aqt.mw.addonManager.writeConfig(__name__, config) + results = { + "permission": "granted", + "requireApikey": bool(util.setting('apiKey')), + "version": util.setting('apiVersion') + } + + # if the origin isn't an empty string, the user clicks "No", and the ignore box is checked + elif origin and pressedButton == QMessageBox.No and msg.checkBox().isChecked(): + config = aqt.mw.addonManager.getConfig(__name__) + config["ignoreOriginList"] = util.setting('ignoreOriginList') + config["ignoreOriginList"].append(origin) + aqt.mw.addonManager.writeConfig(__name__, config) + + # else defaults to denied + return results