From 5c562c0288edc0ea7f53fa8ec6577dd9bd555dba Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Tue, 10 Jan 2017 21:13:38 -0800 Subject: [PATCH 1/4] fixing ankiweb, version up --- ext/bg/js/ankiweb.js | 26 +++++++++++++++++++------- ext/manifest.json | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ext/bg/js/ankiweb.js b/ext/bg/js/ankiweb.js index 6a45b9c5..69a1b44d 100644 --- a/ext/bg/js/ankiweb.js +++ b/ext/bg/js/ankiweb.js @@ -47,7 +47,8 @@ class AnkiWeb { const data = { data: JSON.stringify([fields, note.tags.join(' ')]), mid: model.id, - deck: note.deckName + deck: note.deckName, + csrf_token: info.token }; return AnkiWeb.loadAccountPage('https://ankiweb.net/edit/save', data, this.username, this.password); @@ -78,8 +79,8 @@ class AnkiWeb { return Promise.resolve(this.noteInfo); } - return AnkiWeb.scrape(this.username, this.password).then(({deckNames, models}) => { - this.noteInfo = {deckNames, models}; + return AnkiWeb.scrape(this.username, this.password).then(({deckNames, models, token}) => { + this.noteInfo = {deckNames, models, token}; return this.noteInfo; }); } @@ -100,8 +101,14 @@ class AnkiWeb { return Promise.reject('failed to scrape deck data'); } + const tokenMatch = /editor\.csrf_token = \'(.*)\';/.exec(response); + if (tokenMatch === null) { + return Promise.reject('failed to acquire csrf_token'); + } + const modelsJson = JSON.parse(modelsMatch[1]); const decksJson = JSON.parse(decksMatch[1]); + const token = tokenMatch[1]; const deckNames = Object.keys(decksJson).map(d => decksJson[d].name); const models = []; @@ -113,16 +120,16 @@ class AnkiWeb { }); } - return {deckNames, models}; + return {deckNames, models, token}; }); } - static login(username, password) { + static login(username, password, token) { if (username.length === 0 || password.length === 0) { return Promise.reject('login credentials not specified'); } - const data = {username, password, submitted: 1}; + const data = {username, password, csrf_token: token, submitted: 1}; return AnkiWeb.loadPage('https://ankiweb.net/account/login', data).then(response => { if (!response.includes('class="mitem"')) { return Promise.reject('failed to authenticate'); @@ -133,7 +140,12 @@ class AnkiWeb { static loadAccountPage(url, data, username, password) { return AnkiWeb.loadPage(url, data).then(response => { if (response.includes('name="password"')) { - return AnkiWeb.login(username, password).then(() => AnkiWeb.loadPage(url, data)); + const tokenMatch = /name="csrf_token" value="(.*)"/.exec(response); + if (tokenMatch === null) { + return Promise.reject('failed to acquire csrf_token'); + } + + return AnkiWeb.login(username, password, tokenMatch[1]).then(() => AnkiWeb.loadPage(url, data)); } else { return response; } diff --git a/ext/manifest.json b/ext/manifest.json index c7c1ffa9..e1c1b7a9 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Yomichan", - "version": "1.0.1", + "version": "1.0.2", "description": "Japanese dictionary with Anki integration", "icons": {"16": "img/icon16.png", "48": "img/icon48.png", "128": "img/icon128.png"}, From 916751420e28bb055fbf162ffcbf6663c6b828a8 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Wed, 11 Jan 2017 20:21:11 -0800 Subject: [PATCH 2/4] sayonara ankiweb --- ext/bg/background.html | 1 - ext/bg/js/ankiweb.js | 180 -------------------------------------- ext/bg/js/options-form.js | 20 +---- ext/bg/js/options.js | 6 +- ext/bg/js/yomichan.js | 3 - ext/bg/options.html | 18 +--- ext/manifest.json | 5 +- 7 files changed, 8 insertions(+), 225 deletions(-) delete mode 100644 ext/bg/js/ankiweb.js diff --git a/ext/bg/background.html b/ext/bg/background.html index 49fc6d0f..3ecfa3dc 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -4,7 +4,6 @@ - diff --git a/ext/bg/js/ankiweb.js b/ext/bg/js/ankiweb.js deleted file mode 100644 index 69a1b44d..00000000 --- a/ext/bg/js/ankiweb.js +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2016 Alex Yatskov - * Author: Alex Yatskov - * - * 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 . - */ - -class AnkiWeb { - constructor(username, password) { - this.username = username; - this.password = password; - this.noteInfo = null; - - chrome.webRequest.onBeforeSendHeaders.addListener( - details => { - details.requestHeaders.push({name: 'Origin', value: 'https://ankiweb.net'}); - return {requestHeaders: details.requestHeaders}; - }, - {urls: ['https://ankiweb.net/*']}, - ['blocking', 'requestHeaders'] - ); - } - - addNote(note) { - return this.retrieve().then(info => { - const model = info.models.find(m => m.name === note.modelName); - if (!model) { - return Promise.reject('cannot add note model provided'); - } - - const fields = []; - for (const field of model.fields) { - fields.push(note.fields[field]); - } - - const data = { - data: JSON.stringify([fields, note.tags.join(' ')]), - mid: model.id, - deck: note.deckName, - csrf_token: info.token - }; - - return AnkiWeb.loadAccountPage('https://ankiweb.net/edit/save', data, this.username, this.password); - }).then(response => response !== '0'); - } - - canAddNotes(notes) { - return Promise.resolve(new Array(notes.length).fill(true)); - } - - getDeckNames() { - return this.retrieve().then(info => info.deckNames); - } - - getModelNames() { - return this.retrieve().then(info => info.models.map(m => m.name)); - } - - getModelFieldNames(modelName) { - return this.retrieve().then(info => { - const model = info.models.find(m => m.name === modelName); - return model ? model.fields : []; - }); - } - - retrieve() { - if (this.noteInfo !== null) { - return Promise.resolve(this.noteInfo); - } - - return AnkiWeb.scrape(this.username, this.password).then(({deckNames, models, token}) => { - this.noteInfo = {deckNames, models, token}; - return this.noteInfo; - }); - } - - logout() { - return AnkiWeb.loadPage('https://ankiweb.net/account/logout', null); - } - - static scrape(username, password) { - return AnkiWeb.loadAccountPage('https://ankiweb.net/edit/', null, username, password).then(response => { - const modelsMatch = /editor\.models = (.*}]);/.exec(response); - if (modelsMatch === null) { - return Promise.reject('failed to scrape model data'); - } - - const decksMatch = /editor\.decks = (.*}});/.exec(response); - if (decksMatch === null) { - return Promise.reject('failed to scrape deck data'); - } - - const tokenMatch = /editor\.csrf_token = \'(.*)\';/.exec(response); - if (tokenMatch === null) { - return Promise.reject('failed to acquire csrf_token'); - } - - const modelsJson = JSON.parse(modelsMatch[1]); - const decksJson = JSON.parse(decksMatch[1]); - const token = tokenMatch[1]; - - const deckNames = Object.keys(decksJson).map(d => decksJson[d].name); - const models = []; - for (const modelJson of modelsJson) { - models.push({ - name: modelJson.name, - id: modelJson.id, - fields: modelJson.flds.map(f => f.name) - }); - } - - return {deckNames, models, token}; - }); - } - - static login(username, password, token) { - if (username.length === 0 || password.length === 0) { - return Promise.reject('login credentials not specified'); - } - - const data = {username, password, csrf_token: token, submitted: 1}; - return AnkiWeb.loadPage('https://ankiweb.net/account/login', data).then(response => { - if (!response.includes('class="mitem"')) { - return Promise.reject('failed to authenticate'); - } - }); - } - - static loadAccountPage(url, data, username, password) { - return AnkiWeb.loadPage(url, data).then(response => { - if (response.includes('name="password"')) { - const tokenMatch = /name="csrf_token" value="(.*)"/.exec(response); - if (tokenMatch === null) { - return Promise.reject('failed to acquire csrf_token'); - } - - return AnkiWeb.login(username, password, tokenMatch[1]).then(() => AnkiWeb.loadPage(url, data)); - } else { - return response; - } - }); - } - - static loadPage(url, data) { - return new Promise((resolve, reject) => { - let dataEnc = null; - if (data) { - const params = []; - for (const key in data) { - params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`); - } - - dataEnc = params.join('&'); - } - - const xhr = new XMLHttpRequest(); - xhr.addEventListener('error', () => reject('failed to execute network request')); - xhr.addEventListener('load', () => resolve(xhr.responseText)); - if (dataEnc) { - xhr.open('POST', url); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.send(dataEnc); - } else { - xhr.open('GET', url); - xhr.send(); - } - }); - } -} diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js index ae13dbb1..8216f158 100644 --- a/ext/bg/js/options-form.js +++ b/ext/bg/js/options-form.js @@ -39,8 +39,6 @@ function getFormValues() { optsNew.scanLength = parseInt($('#scan-length').val(), 10); optsNew.ankiMethod = $('#anki-method').val(); - optsNew.ankiUsername = $('#anki-username').val(); - optsNew.ankiPassword = $('#anki-password').val(); optsNew.ankiCardTags = $('#anki-card-tags').val().split(/[,; ]+/); optsNew.sentenceExtent = parseInt($('#sentence-extent').val(), 10); optsNew.ankiTermDeck = $('#anki-term-deck').val(); @@ -67,13 +65,8 @@ function getFormValues() { function updateVisibility(opts) { switch (opts.ankiMethod) { - case 'ankiweb': - $('#anki-general').show(); - $('.anki-login').show(); - break; case 'ankiconnect': $('#anki-general').show(); - $('.anki-login').hide(); break; default: $('#anki-general').hide(); @@ -102,8 +95,6 @@ $(document).ready(() => { $('#scan-length').val(opts.scanLength); $('#anki-method').val(opts.ankiMethod); - $('#anki-username').val(opts.ankiUsername); - $('#anki-password').val(opts.ankiPassword); $('#anki-card-tags').val(opts.ankiCardTags.join(' ')); $('#sentence-extent').val(opts.sentenceExtent); @@ -430,16 +421,7 @@ function onOptionsChanged(e) { return saveOptions(optsNew).then(() => { yomichan().setOptions(optsNew); updateVisibility(optsNew); - - const loginChanged = - optsNew.ankiUsername !== optsOld.ankiUsername || - optsNew.ankiPassword !== optsOld.ankiPassword; - - if (loginChanged && optsNew.ankiMethod === 'ankiweb') { - showAnkiError(null); - showAnkiSpinner(true); - return anki().logout().then(() => populateAnkiDeckAndModel(optsNew)); - } else if (loginChanged || optsNew.ankiMethod !== optsOld.ankiMethod) { + if (optsNew.ankiMethod !== optsOld.ankiMethod) { showAnkiError(null); showAnkiSpinner(true); return populateAnkiDeckAndModel(optsNew); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 28448b96..f1fe0dac 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -31,8 +31,6 @@ function sanitizeOptions(options) { dictionaries: {}, ankiMethod: 'disabled', - ankiUsername: '', - ankiPassword: '', ankiCardTags: ['yomichan'], sentenceExtent: 200, @@ -50,6 +48,10 @@ function sanitizeOptions(options) { } } + if (options.ankiMethod === 'ankiweb') { + options.ankiMethod = 'disabled'; + } + return options; } diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js index ef80f16c..12dd89ac 100644 --- a/ext/bg/js/yomichan.js +++ b/ext/bg/js/yomichan.js @@ -94,9 +94,6 @@ class Yomichan { this.options = options; switch (options.ankiMethod) { - case 'ankiweb': - this.anki = new AnkiWeb(options.ankiUsername, options.ankiPassword); - break; case 'ankiconnect': this.anki = new AnkiConnect(); break; diff --git a/ext/bg/options.html b/ext/bg/options.html index 7ad5ea5c..0ef65abb 100644 --- a/ext/bg/options.html +++ b/ext/bg/options.html @@ -124,9 +124,8 @@

Yomichan features automatic flashcard creation for Anki, a free application - designed to help you retain knowledge. While the AnkiConnect plugin - offers the best experience, it is also possible to create flashcards through AnkiWeb, - provided you already have an account. + designed to help you retain knowledge. This functionality requires prior installation of the + AnkiConnect plugin.

@@ -139,23 +138,10 @@
-
- - - -
-
diff --git a/ext/manifest.json b/ext/manifest.json index e1c1b7a9..09905cd8 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Yomichan", - "version": "1.0.2", + "version": "1.0.3", "description": "Japanese dictionary with Anki integration", "icons": {"16": "img/icon16.png", "48": "img/icon48.png", "128": "img/icon128.png"}, @@ -25,11 +25,8 @@ "page": "bg/options.html" }, "permissions": [ - "webRequest", - "webRequestBlocking", "file://*/*", "http://127.0.0.1/*", - "https://ankiweb.net/*", "storage" ], "web_accessible_resources": [ From dcd17bc65472ccae01ee7bbca8d00d344032e2df Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Wed, 11 Jan 2017 20:25:00 -0800 Subject: [PATCH 3/4] porting popup window dimension calculation fix --- ext/fg/js/popup.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 398e975e..8e71fefa 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -38,21 +38,21 @@ class Popup { showNextTo(elementRect, content) { this.inject(); - const containerRect = this.container.getBoundingClientRect(); + const containerStyle = window.getComputedStyle(this.container); + const containerHeight = parseInt(containerStyle.height); + const containerWidth = parseInt(containerStyle.width); let x = elementRect.left; - let width = containerRect.width; + let width = containerWidth; if (x + width >= window.innerWidth) { - const widthMax = window.innerWidth - x; - width = Math.min(width, widthMax); + width = Math.min(width, x); x = window.innerWidth - width; } let y = elementRect.bottom + this.offset; - let height = containerRect.height; + let height = containerHeight; if (y + height >= window.innerHeight) { - const heightMax = window.innerHeight - y - this.offset; - height = Math.min(height, heightMax); + height = Math.min(height, y); y = elementRect.top - height - this.offset; } From ed1b1d91812fcb7283c39415ee7d11330b969b28 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Wed, 11 Jan 2017 20:41:44 -0800 Subject: [PATCH 4/4] Updating README.md --- README.md | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7ea7e1e7..6b2b33d9 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,8 @@ works, please see the [Yomichan Import](https://foosoft.net/projects/yomichan-im ## Anki Integration ## Yomichan features automatic flashcard creation for [Anki](http://ankisrs.net/), a free application designed to help you -retain knowledge. While the [AnkiConnect](https://foosoft.net/projects/anki-connect/) plugin offers the best experience, -it is also possible to create flashcards through [AnkiWeb](https://ankiweb.net/), provided you already have an account. - -### Using AnkiConnect ### - -Installing the AnkiConnect plugin for Anki is the preferred way of enabling automatic flashcard creation in Yomichan. +retain knowledge. This functionality requires prior installation of the +[AnkiConnect](https://foosoft.net/projects/anki-connect/) plugin. The installation process can be done in three steps: 1. Open the *Install Add-on* dialog by selecting *Tools* > *Add-ons* > *Browse & Install* in Anki. 2. Input *2055492159* into the text box labeled *Code* and press the *OK* button to proceed. @@ -61,19 +57,6 @@ Installing the AnkiConnect plugin for Anki is the preferred way of enabling auto When using AnkiConnect, Anki must be kept running in the background for automatic flashcard creation to function. -### Using AnkiWeb ### - -Yomichan is able to create flashcards directly on AnkiWeb, assuming you have [registered an -account](https://ankiweb.net/account/register). While this method can be convenient as it does not require Anki to be -running to running in the background, some features are not supported due to technical limitations: - -* Preventing the creation of duplicate flashcards. -* Embedding audio in flashcards via the `{audio}` marker. - -Your AnkiWeb login information must specified on the options page to make use of automatic flashcard creation. Note -that AnkiWeb will temporarily block your IP address if there are too many login attempts; if this happens, just try -again after waiting for about an hour. - ### Flashcard Configuration ### Before flashcards can be automatically created through Yomichan, Anki must be configured as follows: @@ -117,6 +100,13 @@ Before flashcards can be automatically created through Yomichan, Anki must be co ## Frequently Asked Questions ## +* **What happened to AnkiWeb integration? Why was it removed?** + + The author of Anki wants to maintain tight control of AnkiWeb by restricting automated web requests, while at the + same time not providing an API for adding or removing flash cards. As circumventing these restrictions led to + account restrictions placed on users of this extension, I was forced to remove this feature. Note that it is still + possible to automatically generate flashcards with the AnkiConnect plugin. + * **Is it possible to use Yomichan with files saved locally on my computer?** It in order to be able use Yomichan with local files, you must first tick the *Allow access to file URLs* checkbox