From 9fec86f7fee264984b8a0f250cc01922ab29fb7e Mon Sep 17 00:00:00 2001 From: DegrangeM <53106394+DegrangeM@users.noreply.github.com> Date: Sat, 8 May 2021 05:33:06 +0200 Subject: [PATCH] Add requestPermission API method (#255) * Add requestPermission Api Method * Add documentation about requestPermission method * Update version documentation --- actions/miscellaneous.md | 46 +++++++++++++++++++++++++++++++---- plugin/__init__.py | 52 +++++++++++++++++++++++++++++++++++++++- plugin/config.json | 3 ++- plugin/util.py | 1 + plugin/web.py | 21 +++++++++++----- 5 files changed, 110 insertions(+), 13 deletions(-) diff --git a/actions/miscellaneous.md b/actions/miscellaneous.md index 523990a..5cb5c54 100644 --- a/actions/miscellaneous.md +++ b/actions/miscellaneous.md @@ -1,8 +1,12 @@ # Miscellaneous Actions -* **version** +* **requestPermission** - Gets the version of the API exposed by this plugin. Currently versions `1` through `6` are defined. + 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. This should be the first call you make to make sure that your application and AnkiConnect are able to communicate properly with each other. New versions of AnkiConnect are backwards compatible; as long as you are using actions @@ -11,18 +15,50 @@ *Sample request*: ```json { - "action": "version", + "action": "requestPermission", "version": 6 } ``` - *Sample result*: + *Samples results*: ```json { - "result": 6, + "result": { + "permission": "granted", + "requireApiKey": false, + "version": 6 + }, "error": null } ``` + ```json + { + "result": { + "permission": "denied" + }, + "error": null + } + ``` + +* **version** + +Gets the version of the API exposed by this plugin. Currently versions `1` through `6` are defined. + +*Sample request*: +```json +{ + "action": "version", + "version": 6 +} +``` + +*Sample result*: +```json +{ + "result": 6, + "error": null +} +``` * **sync** diff --git a/plugin/__init__.py b/plugin/__init__.py index 6c07df2..f6b670e 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -26,6 +26,7 @@ import string import time import unicodedata +from PyQt5 import QtCore from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import QMessageBox @@ -97,7 +98,7 @@ class AnkiConnect: reply = {'result': None, 'error': None} try: - if key != util.setting('apiKey'): + if key != util.setting('apiKey') and name != 'requestPermission': raise Exception('valid api key must be provided') method = None @@ -311,6 +312,55 @@ class AnkiConnect: def version(self): return util.setting('apiVersion') + @util.api() + def requestPermission(self, origin, allowed): + 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", + } + return results + @util.api() def getProfiles(self): diff --git a/plugin/config.json b/plugin/config.json index 697b96a..d3b51ea 100644 --- a/plugin/config.json +++ b/plugin/config.json @@ -3,5 +3,6 @@ "apiLogPath": null, "webBindAddress": "127.0.0.1", "webBindPort": 8765, - "webCorsOriginList": ["http://localhost"] + "webCorsOriginList": ["http://localhost"], + "ignoreOriginList": [] } diff --git a/plugin/util.py b/plugin/util.py index 28c4382..e2d6741 100644 --- a/plugin/util.py +++ b/plugin/util.py @@ -76,6 +76,7 @@ def setting(key): 'webBindPort': 8765, 'webCorsOrigin': os.getenv('ANKICONNECT_CORS_ORIGIN', None), 'webCorsOriginList': ['http://localhost'], + 'ignoreOriginList': [], 'webTimeout': 10000, } diff --git a/plugin/web.py b/plugin/web.py index 1c1daf6..bb227fe 100644 --- a/plugin/web.py +++ b/plugin/web.py @@ -185,16 +185,25 @@ class WebServer: allowed = True resp = bytes() + paramsError = False + try: + params = json.loads(req.body.decode('utf-8')) + except ValueError: + body = json.dumps(None).encode('utf-8') + paramsError = True - if allowed : + if allowed or not paramsError and params.get('action', '') == 'requestPermission': if len(req.body) == 0: body = 'AnkiConnect v.{}'.format(util.setting('apiVersion')).encode('utf-8') else: - try: - params = json.loads(req.body.decode('utf-8')) - body = json.dumps(self.handler(params)).encode('utf-8') - except ValueError: - body = json.dumps(None).encode('utf-8') + if params.get('action', '') == 'requestPermission': + params['params'] = params.get('params', {}) + params['params']['allowed'] = allowed + params['params']['origin'] = b'origin' in req.headers and req.headers[b'origin'].decode() or '' + if not allowed : + corsOrigin = params['params']['origin'] + + body = json.dumps(self.handler(params)).encode('utf-8') headers = [ ['HTTP/1.1 200 OK', None],