From bb2d9501afc0e406b0dacf5675cd90985238be98 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 6 May 2020 19:32:28 -0400 Subject: [PATCH] Add apiModifySettings (#501) * Update getProfile/getProfileFromContext to store this.options in a variable * Add useSchema parameter to options getter functions * Add apiModifySettings * Use apiModifySettings instead of apiOptionsSet * Remove apiOptionsSet * Fix incorrect deleteCount check * Require explicit scope for options * Throw on invalid scope --- ext/bg/background.html | 1 + ext/bg/js/backend.js | 138 +++++++++++++++++++------------ ext/bg/js/search-query-parser.js | 22 +++-- ext/bg/js/search.js | 30 +++++-- ext/mixed/js/api.js | 8 +- 5 files changed, 132 insertions(+), 67 deletions(-) diff --git a/ext/bg/background.html b/ext/bg/background.html index ee5a1f32..9c740adf 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -46,6 +46,7 @@ + diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index d454aa22..8677e04c 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -26,6 +26,7 @@ * DictionaryImporter * JsonSchema * Mecab + * ObjectPropertyAccessor * Translator * conditionsTestValue * dictTermsSort @@ -84,7 +85,6 @@ class Backend { ['optionsSchemaGet', {handler: this._onApiOptionsSchemaGet.bind(this), async: false}], ['optionsGet', {handler: this._onApiOptionsGet.bind(this), async: false}], ['optionsGetFull', {handler: this._onApiOptionsGetFull.bind(this), async: false}], - ['optionsSet', {handler: this._onApiOptionsSet.bind(this), async: true}], ['optionsSave', {handler: this._onApiOptionsSave.bind(this), async: true}], ['kanjiFind', {handler: this._onApiKanjiFind.bind(this), async: true}], ['termsFind', {handler: this._onApiTermsFind.bind(this), async: true}], @@ -115,7 +115,8 @@ class Backend { ['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}], ['log', {handler: this._onApiLog.bind(this), async: false}], ['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}], - ['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}] + ['createActionPort', {handler: this._onApiCreateActionPort.bind(this), async: false}], + ['modifySettings', {handler: this._onApiModifySettings.bind(this), async: true}] ]); this._messageHandlersWithProgress = new Map([ ['importDictionaryArchive', {handler: this._onApiImportDictionaryArchive.bind(this), async: true}], @@ -258,8 +259,9 @@ class Backend { return this.optionsSchema; } - getFullOptions() { - return this.options; + getFullOptions(useSchema=false) { + const options = this.options; + return useSchema ? JsonSchema.createProxy(options, this.optionsSchema) : options; } setFullOptions(options) { @@ -271,21 +273,22 @@ class Backend { } } - getOptions(optionsContext) { - return this.getProfile(optionsContext).options; + getOptions(optionsContext, useSchema=false) { + return this.getProfile(optionsContext, useSchema).options; } - getProfile(optionsContext) { - const profiles = this.options.profiles; + getProfile(optionsContext, useSchema=false) { + const options = this.getFullOptions(useSchema); + const profiles = options.profiles; if (typeof optionsContext.index === 'number') { return profiles[optionsContext.index]; } - const profile = this.getProfileFromContext(optionsContext); - return profile !== null ? profile : this.options.profiles[this.options.profileCurrent]; + const profile = this.getProfileFromContext(options, optionsContext); + return profile !== null ? profile : options.profiles[options.profileCurrent]; } - getProfileFromContext(optionsContext) { - for (const profile of this.options.profiles) { + getProfileFromContext(options, optionsContext) { + for (const profile of options.profiles) { const conditionGroups = profile.conditionGroups; if (conditionGroups.length > 0 && Backend.testConditionGroups(conditionGroups, optionsContext)) { return profile; @@ -413,46 +416,6 @@ class Backend { return this.getFullOptions(); } - async _onApiOptionsSet({changedOptions, optionsContext, source}) { - const options = this.getOptions(optionsContext); - - function getValuePaths(obj) { - const valuePaths = []; - const nodes = [{obj, path: []}]; - while (nodes.length > 0) { - const node = nodes.pop(); - for (const key of Object.keys(node.obj)) { - const path = node.path.concat(key); - const obj2 = node.obj[key]; - if (obj2 !== null && typeof obj2 === 'object') { - nodes.unshift({obj: obj2, path}); - } else { - valuePaths.push([obj2, path]); - } - } - } - return valuePaths; - } - - function modifyOption(path, value) { - let pivot = options; - for (const key of path.slice(0, -1)) { - if (!hasOwn(pivot, key)) { - return false; - } - pivot = pivot[key]; - } - pivot[path[path.length - 1]] = value; - return true; - } - - for (const [value, path] of getValuePaths(changedOptions)) { - modifyOption(path, value); - } - - await this._onApiOptionsSave({source}); - } - async _onApiOptionsSave({source}) { const options = this.getFullOptions(); await optionsSave(options); @@ -829,6 +792,20 @@ class Backend { await this.database.deleteDictionary(dictionaryName, {rate: 1000}, onProgress); } + async _onApiModifySettings({targets, source}) { + const results = []; + for (const target of targets) { + try { + this._modifySetting(target); + results.push({result: true}); + } catch (e) { + results.push({error: errorToJson(e)}); + } + } + await this._onApiOptionsSave({source}); + return results; + } + // Command handlers _createActionListenerPort(port, sender, handlers) { @@ -988,6 +965,63 @@ class Backend { // Utilities + _getModifySettingObject(target) { + const scope = target.scope; + switch (scope) { + case 'profile': + if (!isObject(target.optionsContext)) { throw new Error('Invalid optionsContext'); } + return this.getOptions(target.optionsContext, true); + case 'global': + return this.getFullOptions(true); + default: + throw new Error(`Invalid scope: ${scope}`); + } + } + + async _modifySetting(target) { + const options = this._getModifySettingObject(target); + const accessor = new ObjectPropertyAccessor(options); + const action = target.action; + switch (action) { + case 'set': + { + const {path, value} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + accessor.set(ObjectPropertyAccessor.getPathArray(path), value); + } + break; + case 'delete': + { + const {path} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + accessor.delete(ObjectPropertyAccessor.getPathArray(path)); + } + break; + case 'swap': + { + const {path1, path2} = target; + if (typeof path1 !== 'string') { throw new Error('Invalid path1'); } + if (typeof path2 !== 'string') { throw new Error('Invalid path2'); } + accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2)); + } + break; + case 'splice': + { + const {path, start, deleteCount, items} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); } + if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); } + if (!Array.isArray(items)) { throw new Error('Invalid items'); } + const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); + if (!Array.isArray(array)) { throw new Error('Invalid target type'); } + array.splice(start, deleteCount, ...items); + } + break; + default: + throw new Error(`Unknown action: ${action}`); + } + } + _validatePrivilegedMessageSender(sender) { const url = sender.url; if (!(typeof url === 'string' && yomichan.isExtensionUrl(url))) { diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 935f01f2..1c89583f 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -18,7 +18,7 @@ /* global * QueryParserGenerator * TextScanner - * apiOptionsSet + * apiModifySettings * apiTermsFind * apiTextParse * docSentenceExtract @@ -72,8 +72,14 @@ class QueryParser extends TextScanner { } onParserChange(e) { - const selectedParser = e.target.value; - apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext()); + const value = e.target.value; + apiModifySettings([{ + action: 'set', + path: 'parsing.selectedParser', + value, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } getMouseEventListeners() { @@ -92,8 +98,14 @@ class QueryParser extends TextScanner { refreshSelectedParser() { if (this.parseResults.length > 0) { if (!this.getParseResult()) { - const selectedParser = this.parseResults[0].id; - apiOptionsSet({parsing: {selectedParser}}, this.getOptionsContext()); + const value = this.parseResults[0].id; + apiModifySettings([{ + action: 'set', + path: 'parsing.selectedParser', + value, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } } } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index d69daea6..96e8a70b 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -21,7 +21,7 @@ * Display * QueryParser * apiClipboardGet - * apiOptionsSet + * apiModifySettings * apiTermsFind * wanakana */ @@ -252,13 +252,19 @@ class DisplaySearch extends Display { } onWanakanaEnableChange(e) { - const enableWanakana = e.target.checked; - if (enableWanakana) { + const value = e.target.checked; + if (value) { wanakana.bind(this.query); } else { wanakana.unbind(this.query); } - apiOptionsSet({general: {enableWanakana}}, this.getOptionsContext()); + apiModifySettings([{ + action: 'set', + path: 'general.enableWanakana', + value, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } onClipboardMonitorEnableChange(e) { @@ -268,7 +274,13 @@ class DisplaySearch extends Display { (granted) => { if (granted) { this.clipboardMonitor.start(); - apiOptionsSet({general: {enableClipboardMonitor: true}}, this.getOptionsContext()); + apiModifySettings([{ + action: 'set', + path: 'general.enableClipboardMonitor', + value: true, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } else { e.target.checked = false; } @@ -276,7 +288,13 @@ class DisplaySearch extends Display { ); } else { this.clipboardMonitor.stop(); - apiOptionsSet({general: {enableClipboardMonitor: false}}, this.getOptionsContext()); + apiModifySettings([{ + action: 'set', + path: 'general.enableClipboardMonitor', + value: false, + scope: 'profile', + optionsContext: this.getOptionsContext() + }], 'search'); } } diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index af97ac3d..0bc91759 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -28,10 +28,6 @@ function apiOptionsGetFull() { return _apiInvoke('optionsGetFull'); } -function apiOptionsSet(changedOptions, optionsContext, source) { - return _apiInvoke('optionsSet', {changedOptions, optionsContext, source}); -} - function apiOptionsSave(source) { return _apiInvoke('optionsSave', {source}); } @@ -160,6 +156,10 @@ function apiDeleteDictionary(dictionaryName, onProgress) { return _apiInvokeWithProgress('deleteDictionary', {dictionaryName}, onProgress); } +function apiModifySettings(targets, source) { + return _apiInvoke('modifySettings', {targets, source}); +} + function _apiCreateActionPort(timeout=5000) { return new Promise((resolve, reject) => { let timer = null;