diff --git a/.eslintignore b/.eslintignore index be91913656..8cad126801 100644 --- a/.eslintignore +++ b/.eslintignore @@ -91,10 +91,12 @@ CliClient/tests/support/plugins/withExternalModules/src/index.js CliClient/tests/synchronizer_LockHandler.js CliClient/tests/synchronizer_MigrationHandler.js ElectronClient/app.js +ElectronClient/bridge.js ElectronClient/commands/copyDevCommand.js ElectronClient/commands/focusElement.js ElectronClient/commands/startExternalEditing.js ElectronClient/commands/stopExternalEditing.js +ElectronClient/ElectronAppWrapper.js ElectronClient/global.d.js ElectronClient/gui/Button/Button.js ElectronClient/gui/ConfigScreen/ButtonBar.js @@ -199,9 +201,11 @@ ElectronClient/gui/ToolbarBase.js ElectronClient/gui/ToolbarButton/styles/index.js ElectronClient/gui/ToolbarButton/ToolbarButton.js ElectronClient/gui/utils/NoteListUtils.js +ElectronClient/services/bridge.js ElectronClient/services/plugins/hooks/useThemeCss.js ElectronClient/services/plugins/hooks/useViewIsReady.js ElectronClient/services/plugins/PlatformImplementation.js +ElectronClient/services/plugins/PluginRunner.js ElectronClient/services/plugins/UserWebview.js ElectronClient/services/plugins/UserWebviewDialog.js ElectronClient/services/plugins/UserWebviewDialogButtonBar.js @@ -221,6 +225,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js ReactNativeClient/lib/JoplinServerApi.js +ReactNativeClient/lib/Logger.js ReactNativeClient/lib/models/Setting.js ReactNativeClient/lib/ntpDate.js ReactNativeClient/lib/reducer.js @@ -259,6 +264,7 @@ ReactNativeClient/lib/services/plugins/sandboxProxy.js ReactNativeClient/lib/services/plugins/ToolbarButtonController.js ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.js ReactNativeClient/lib/services/plugins/utils/manifestFromObject.js +ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.js ReactNativeClient/lib/services/plugins/utils/types.js ReactNativeClient/lib/services/plugins/ViewController.js ReactNativeClient/lib/services/plugins/WebviewController.js diff --git a/.gitignore b/.gitignore index fae7b7e5b5..92b87be6bd 100644 --- a/.gitignore +++ b/.gitignore @@ -84,10 +84,12 @@ CliClient/tests/support/plugins/withExternalModules/src/index.js CliClient/tests/synchronizer_LockHandler.js CliClient/tests/synchronizer_MigrationHandler.js ElectronClient/app.js +ElectronClient/bridge.js ElectronClient/commands/copyDevCommand.js ElectronClient/commands/focusElement.js ElectronClient/commands/startExternalEditing.js ElectronClient/commands/stopExternalEditing.js +ElectronClient/ElectronAppWrapper.js ElectronClient/global.d.js ElectronClient/gui/Button/Button.js ElectronClient/gui/ConfigScreen/ButtonBar.js @@ -192,9 +194,11 @@ ElectronClient/gui/ToolbarBase.js ElectronClient/gui/ToolbarButton/styles/index.js ElectronClient/gui/ToolbarButton/ToolbarButton.js ElectronClient/gui/utils/NoteListUtils.js +ElectronClient/services/bridge.js ElectronClient/services/plugins/hooks/useThemeCss.js ElectronClient/services/plugins/hooks/useViewIsReady.js ElectronClient/services/plugins/PlatformImplementation.js +ElectronClient/services/plugins/PluginRunner.js ElectronClient/services/plugins/UserWebview.js ElectronClient/services/plugins/UserWebviewDialog.js ElectronClient/services/plugins/UserWebviewDialogButtonBar.js @@ -214,6 +218,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js ReactNativeClient/lib/JoplinServerApi.js +ReactNativeClient/lib/Logger.js ReactNativeClient/lib/models/Setting.js ReactNativeClient/lib/ntpDate.js ReactNativeClient/lib/reducer.js @@ -252,6 +257,7 @@ ReactNativeClient/lib/services/plugins/sandboxProxy.js ReactNativeClient/lib/services/plugins/ToolbarButtonController.js ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.js ReactNativeClient/lib/services/plugins/utils/manifestFromObject.js +ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.js ReactNativeClient/lib/services/plugins/utils/types.js ReactNativeClient/lib/services/plugins/ViewController.js ReactNativeClient/lib/services/plugins/WebviewController.js diff --git a/CliClient/app/ResourceServer.js b/CliClient/app/ResourceServer.js index b2e9aea84c..bdf2815625 100644 --- a/CliClient/app/ResourceServer.js +++ b/CliClient/app/ResourceServer.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { netUtils } = require('lib/net-utils.js'); const http = require('http'); diff --git a/CliClient/app/app-gui.js b/CliClient/app/app-gui.js index d57ccfcc95..360ddcd703 100644 --- a/CliClient/app/app-gui.js +++ b/CliClient/app/app-gui.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Folder = require('lib/models/Folder.js'); const BaseItem = require('lib/models/BaseItem.js'); const Tag = require('lib/models/Tag.js'); diff --git a/CliClient/app/cli-integration-tests.js b/CliClient/app/cli-integration-tests.js index d8c74a18ae..e07b858e40 100644 --- a/CliClient/app/cli-integration-tests.js +++ b/CliClient/app/cli-integration-tests.js @@ -1,7 +1,7 @@ 'use strict'; const fs = require('fs-extra'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { dirname } = require('lib/path-utils.js'); const { DatabaseDriverNode } = require('lib/database-driver-node.js'); const { JoplinDatabase } = require('lib/joplin-database.js'); diff --git a/CliClient/app/cli-utils.js b/CliClient/app/cli-utils.js index 2e25b7ac60..ab049b9b68 100644 --- a/CliClient/app/cli-utils.js +++ b/CliClient/app/cli-utils.js @@ -2,7 +2,7 @@ const yargParser = require('yargs-parser'); const { _ } = require('lib/locale.js'); const { time } = require('lib/time-utils.js'); const stringPadding = require('string-padding'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const cliUtils = {}; diff --git a/CliClient/app/command-server.js b/CliClient/app/command-server.js index 33b81937e5..af809e7505 100644 --- a/CliClient/app/command-server.js +++ b/CliClient/app/command-server.js @@ -1,7 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { _ } = require('lib/locale.js'); const Setting = require('lib/models/Setting').default; -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const shim = require('lib/shim'); class Command extends BaseCommand { diff --git a/CliClient/app/fuzzing.js b/CliClient/app/fuzzing.js index e45a070205..534a0830b6 100644 --- a/CliClient/app/fuzzing.js +++ b/CliClient/app/fuzzing.js @@ -1,7 +1,7 @@ 'use strict'; const { time } = require('lib/time-utils.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Resource = require('lib/models/Resource.js'); const { dirname } = require('lib/path-utils.js'); const { FsDriverNode } = require('./fs-driver-node.js'); diff --git a/CliClient/app/main.js b/CliClient/app/main.js index 4722a8a526..ef5956650f 100644 --- a/CliClient/app/main.js +++ b/CliClient/app/main.js @@ -23,7 +23,7 @@ const NoteTag = require('lib/models/NoteTag.js'); const MasterKey = require('lib/models/MasterKey'); const Setting = require('lib/models/Setting').default; const Revision = require('lib/models/Revision.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { FsDriverNode } = require('lib/fs-driver-node.js'); const { shimInit } = require('lib/shim-init-node.js'); const { _ } = require('lib/locale.js'); diff --git a/CliClient/app/services/plugins/PluginRunner.ts b/CliClient/app/services/plugins/PluginRunner.ts index c6a95177f4..1e06dc320d 100644 --- a/CliClient/app/services/plugins/PluginRunner.ts +++ b/CliClient/app/services/plugins/PluginRunner.ts @@ -4,6 +4,7 @@ import sandboxProxy from 'lib/services/plugins/sandboxProxy'; import BasePluginRunner from 'lib/services/plugins/BasePluginRunner'; import executeSandboxCall from 'lib/services/plugins/utils/executeSandboxCall'; import Global from 'lib/services/plugins/api/Global'; +import mapEventHandlersToIds, { EventHandlers } from 'lib/services/plugins/utils/mapEventHandlersToIds'; function createConsoleWrapper(pluginId:string) { const wrapper:any = {}; @@ -29,6 +30,8 @@ function createConsoleWrapper(pluginId:string) { export default class PluginRunner extends BasePluginRunner { + private eventHandlers_:EventHandlers = {}; + constructor() { super(); @@ -37,20 +40,12 @@ export default class PluginRunner extends BasePluginRunner { private async eventHandler(eventHandlerId:string, args:any[]) { const cb = this.eventHandlers_[eventHandlerId]; - delete this.eventHandlers_[eventHandlerId]; return cb(...args); } private newSandboxProxy(pluginId:string, sandbox:Global) { - - // Note: for desktop, the implementation should be like so: - // In the target, we post an IPC message with the path, args, etc. as well as a callbackId to the host - // The target saves this callbackId and associate it with a Promise that it returns. - // When the host responds back (via IPC), we get the promise back using the callbackId, then call resolve - // with what was sent from the host. - const target = async (path:string, args:any[]) => { - return executeSandboxCall(pluginId, sandbox, `joplin.${path}`, this.mapEventHandlersToIds(args), this.eventHandler); + return executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler); }; return { diff --git a/CliClient/tests/services_PluginService.ts b/CliClient/tests/services_PluginService.ts index 741f8d15da..7cdffd964d 100644 --- a/CliClient/tests/services_PluginService.ts +++ b/CliClient/tests/services_PluginService.ts @@ -85,7 +85,7 @@ describe('services_PluginService', function() { // const folder = await Folder.save({ title: 'folder' }); - // const folderFromApi = await service.executeSandboxCall(plugin.id, 'joplin.api.get', ['folders/' + folder.id]); + // const folderFromApi = await service.executeSandboxCall(plugin.id, 'joplin.data.get', ['folders/' + folder.id]); // expect(folder.id).toBe(folderFromApi.id); // expect(folder.title).toBe(folderFromApi.title); // })); diff --git a/CliClient/tests/support/plugins/multi_plugins/simple1/index.js b/CliClient/tests/support/plugins/multi_plugins/simple1/index.js index ffb3109749..6bba2c7788 100644 --- a/CliClient/tests/support/plugins/multi_plugins/simple1/index.js +++ b/CliClient/tests/support/plugins/multi_plugins/simple1/index.js @@ -1,5 +1,5 @@ joplin.plugins.register({ onStart: async function() { - await joplin.api.post('folders', null, { title: "multi - simple1" }); + await joplin.data.post('folders', null, { title: "multi - simple1" }); }, }); \ No newline at end of file diff --git a/CliClient/tests/support/plugins/multi_plugins/simple2/index.js b/CliClient/tests/support/plugins/multi_plugins/simple2/index.js index b76301e7b7..4c9e4a5035 100644 --- a/CliClient/tests/support/plugins/multi_plugins/simple2/index.js +++ b/CliClient/tests/support/plugins/multi_plugins/simple2/index.js @@ -1,5 +1,5 @@ joplin.plugins.register({ onStart: async function() { - await joplin.api.post('folders', null, { title: "multi - simple2" }); + await joplin.data.post('folders', null, { title: "multi - simple2" }); }, }); \ No newline at end of file diff --git a/CliClient/tests/support/plugins/multi_selection/src/index.ts b/CliClient/tests/support/plugins/multi_selection/src/index.ts index 3ce78a3a0f..d2fec65824 100644 --- a/CliClient/tests/support/plugins/multi_selection/src/index.ts +++ b/CliClient/tests/support/plugins/multi_selection/src/index.ts @@ -11,7 +11,7 @@ joplin.plugins.register({ let parentId = null; for (const noteId of noteIds) { - const note = await joplin.api.get('notes/' + noteId, { fields: ['title', 'body', 'parent_id']}); + const note = await joplin.data.get('notes/' + noteId, { fields: ['title', 'body', 'parent_id']}); newNoteBody.push([ '# ' + note.title, '', @@ -27,7 +27,7 @@ joplin.plugins.register({ parent_id: parentId, }; - await joplin.api.post('notes', null, newNote); + await joplin.data.post('notes', null, newNote); }, }); diff --git a/CliClient/tests/support/plugins/simple/index.js b/CliClient/tests/support/plugins/simple/index.js index e498227b37..f6991be823 100644 --- a/CliClient/tests/support/plugins/simple/index.js +++ b/CliClient/tests/support/plugins/simple/index.js @@ -1,6 +1,6 @@ joplin.plugins.register({ onStart: async function() { - const folder = await joplin.api.post('folders', null, { title: "my plugin folder" }); - await joplin.api.post('notes', null, { parent_id: folder.id, title: "testing plugin!" }); + const folder = await joplin.data.post('folders', null, { title: "my plugin folder" }); + await joplin.data.post('notes', null, { parent_id: folder.id, title: "testing plugin!" }); }, }); \ No newline at end of file diff --git a/CliClient/tests/support/plugins/testImport/index.js b/CliClient/tests/support/plugins/testImport/index.js index 5545bc3451..8f05976225 100644 --- a/CliClient/tests/support/plugins/testImport/index.js +++ b/CliClient/tests/support/plugins/testImport/index.js @@ -2,6 +2,6 @@ const testImport = require('./testImport'); joplin.plugins.register({ onStart: async function() { - await joplin.api.post('folders', null, { title: testImport() }); + await joplin.data.post('folders', null, { title: testImport() }); }, }); \ No newline at end of file diff --git a/CliClient/tests/support/plugins/withExternalModules/dist/index.js b/CliClient/tests/support/plugins/withExternalModules/dist/index.js index 4876384c79..31cefbda54 100644 --- a/CliClient/tests/support/plugins/withExternalModules/dist/index.js +++ b/CliClient/tests/support/plugins/withExternalModules/dist/index.js @@ -1 +1 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,u){function i(e){try{a(r.next(e))}catch(e){u(e)}}function l(e){try{a(r.throw(e))}catch(e){u(e)}}function a(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(i,l)}a((r=r.apply(e,t||[])).next())}))},o=this&&this.__generator||function(e,t){var n,r,o,u,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return u={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(u[Symbol.iterator]=function(){return this}),u;function l(u){return function(l){return function(u){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&u[0]?r.return:u[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,u[1])).done)return o;switch(r=0,o&&(u=[2&u[0],o.value]),u[0]){case 0:case 1:o=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,r=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(o=i.trys,(o=o.length>0&&o[o.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!o||u[1]>o[0]&&u[1]>=1;)n+=n;return o+e};var r=[""," "," "," "," "," "," "," "," "," "]}]); \ No newline at end of file +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,u){function i(e){try{a(r.next(e))}catch(e){u(e)}}function l(e){try{a(r.throw(e))}catch(e){u(e)}}function a(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(i,l)}a((r=r.apply(e,t||[])).next())}))},o=this&&this.__generator||function(e,t){var n,r,o,u,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return u={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(u[Symbol.iterator]=function(){return this}),u;function l(u){return function(l){return function(u){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&u[0]?r.return:u[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,u[1])).done)return o;switch(r=0,o&&(u=[2&u[0],o.value]),u[0]){case 0:case 1:o=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,r=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(o=i.trys,(o=o.length>0&&o[o.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!o||u[1]>o[0]&&u[1]>=1;)n+=n;return o+e};var r=[""," "," "," "," "," "," "," "," "," "]}]); \ No newline at end of file diff --git a/CliClient/tests/support/plugins/withExternalModules/src/index.ts b/CliClient/tests/support/plugins/withExternalModules/src/index.ts index 62476127d3..7881218d90 100644 --- a/CliClient/tests/support/plugins/withExternalModules/src/index.ts +++ b/CliClient/tests/support/plugins/withExternalModules/src/index.ts @@ -2,6 +2,6 @@ const leftPad = require('left-pad'); joplin.plugins.register({ onStart: async function() { - await joplin.api.post('folders', null, { title: leftPad('foo', 5) }); + await joplin.data.post('folders', null, { title: leftPad('foo', 5) }); }, }); \ No newline at end of file diff --git a/CliClient/tests/test-utils.js b/CliClient/tests/test-utils.js index 71ed5be0d8..93f5ba4dc4 100644 --- a/CliClient/tests/test-utils.js +++ b/CliClient/tests/test-utils.js @@ -12,7 +12,7 @@ const Resource = require('lib/models/Resource.js'); const Tag = require('lib/models/Tag.js'); const NoteTag = require('lib/models/NoteTag.js'); const Revision = require('lib/models/Revision.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Setting = require('lib/models/Setting').default; const MasterKey = require('lib/models/MasterKey'); const BaseItem = require('lib/models/BaseItem.js'); diff --git a/ElectronClient/ElectronAppWrapper.js b/ElectronClient/ElectronAppWrapper.ts similarity index 83% rename from ElectronClient/ElectronAppWrapper.js rename to ElectronClient/ElectronAppWrapper.ts index 2d71be70b6..52f952c60f 100644 --- a/ElectronClient/ElectronAppWrapper.js +++ b/ElectronClient/ElectronAppWrapper.ts @@ -1,3 +1,6 @@ +import Logger from "lib/Logger"; +import { PluginMessage } from './services/plugins/PluginRunner' + const { BrowserWindow, Tray, screen } = require('electron'); const shim = require('lib/shim'); const url = require('url'); @@ -6,31 +9,40 @@ const { dirname } = require('lib/path-utils'); const fs = require('fs-extra'); const { ipcMain } = require('electron'); -// const { ipcMain } = require('electron') -// ipcMain.on('pluginMessage', (event, arg) => { -// console.info('PPPPPPPPPPPPPPPPPPPPPP', arg); -// }) +interface RendererProcessQuitReply { + canClose: boolean, +} +interface PluginWindows { + [key: string]: any, +} -class ElectronAppWrapper { +export default class ElectronAppWrapper { - constructor(electronApp, env, profilePath, isDebugMode) { + private logger_:Logger = null; + private electronApp_:any; + private env_:string; + private isDebugMode_:boolean; + private profilePath_:string; + private win_:any = null; + private willQuitApp_:boolean = false; + private tray_:any = null; + private buildDir_:string = null; + private rendererProcessQuitReply_:RendererProcessQuitReply = null; + private pluginWindows_:PluginWindows = {}; + + constructor(electronApp:any, env:string, profilePath:string, isDebugMode:boolean) { this.electronApp_ = electronApp; this.env_ = env; this.isDebugMode_ = isDebugMode; this.profilePath_ = profilePath; - this.win_ = null; - this.willQuitApp_ = false; - this.tray_ = null; - this.buildDir_ = null; - this.rendererProcessQuitReply_ = null; } electronApp() { return this.electronApp_; } - setLogger(v) { + setLogger(v:Logger) { this.logger_ = v; } @@ -53,7 +65,7 @@ class ElectronAppWrapper { const windowStateKeeper = require('electron-window-state'); - const stateOptions = { + const stateOptions:any = { defaultWidth: Math.round(0.8 * screen.getPrimaryDisplay().workArea.width), defaultHeight: Math.round(0.8 * screen.getPrimaryDisplay().workArea.height), file: `window-state-${this.env_}.json`, @@ -64,7 +76,7 @@ class ElectronAppWrapper { // Load the previous state with fallback to defaults const windowState = windowStateKeeper(stateOptions); - const windowOptions = { + const windowOptions:any = { x: windowState.x, y: windowState.y, width: windowState.width, @@ -86,7 +98,7 @@ class ElectronAppWrapper { if (shim.isLinux()) windowOptions.icon = path.join(__dirname, '..', 'build/icons/128x128.png'); require('electron-context-menu')({ - shouldShowMenu: (event, params) => { + shouldShowMenu: (_event:any, params:any) => { // params.inputFieldType === 'none' when right-clicking the text editor. This is a bit of a hack to detect it because in this // case we don't want to use the built-in context menu but a custom one. return params.isEditable && params.inputFieldType !== 'none'; @@ -123,7 +135,7 @@ class ElectronAppWrapper { }, 3000); } - this.win_.on('close', (event) => { + this.win_.on('close', (event:any) => { // If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true) // otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the // user clicks on the icon in the task bar). @@ -171,7 +183,7 @@ class ElectronAppWrapper { } }); - ipcMain.on('asynchronous-message', (event, message, args) => { + ipcMain.on('asynchronous-message', (_event:any, message:string, args:any) => { if (message === 'appCloseReply') { // We got the response from the renderer process: // save the response and try quit again. @@ -180,10 +192,21 @@ class ElectronAppWrapper { } }); - ipcMain.on('pluginMessage', (event, data) => { - // Forward message - if (data.target === 'mainWindow') { - this.win_.webContents.send('pluginMessage', data); + // This handler receives IPC messages from a plugin or from the main window, + // and forwards it to the main window or the plugin window. + ipcMain.on('pluginMessage', (_event:any, message:PluginMessage) => { + if (message.target === 'mainWindow') { + this.win_.webContents.send('pluginMessage', message); + } + + if (message.target === 'plugin') { + const win = this.pluginWindows_[message.pluginId]; + if (!win) { + this.logger().error('Trying to send IPC message to non-existing plugin window: ' + message.pluginId); + return; + } + + win.webContents.send('pluginMessage', message); } }); @@ -200,6 +223,10 @@ class ElectronAppWrapper { } } + registerPluginWindow(pluginId:string, window:any) { + this.pluginWindows_[pluginId] = window; + } + async waitForElectronAppReady() { if (this.electronApp().isReady()) return Promise.resolve(); @@ -258,7 +285,7 @@ class ElectronAppWrapper { } // Note: this must be called only after the "ready" event of the app has been dispatched - createTray(contextMenu) { + createTray(contextMenu:any) { try { this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`); this.tray_.setToolTip(this.electronApp_.name); @@ -325,5 +352,3 @@ class ElectronAppWrapper { } } - -module.exports = { ElectronAppWrapper }; diff --git a/ElectronClient/InteropServiceHelper.js b/ElectronClient/InteropServiceHelper.js index 7d514252a1..3a4738ef82 100644 --- a/ElectronClient/InteropServiceHelper.js +++ b/ElectronClient/InteropServiceHelper.js @@ -1,5 +1,5 @@ const { _ } = require('lib/locale'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const InteropService = require('lib/services/interop/InteropService').default; const CommandService = require('lib/services/CommandService').default; const Setting = require('lib/models/Setting').default; diff --git a/ElectronClient/app.ts b/ElectronClient/app.ts index e2b650f409..339e7f272b 100644 --- a/ElectronClient/app.ts +++ b/ElectronClient/app.ts @@ -8,6 +8,8 @@ import { utils as pluginUtils } from 'lib/services/plugins/reducer'; // import SandboxImplementation from './plugins/SandboxImplementation'; import { MenuItemLocation } from 'lib/services/plugins/MenuItemController'; import { defaultState, State } from 'lib/reducer'; +import PluginRunner from './services/plugins/PluginRunner'; +import PlatformImplementation from './services/plugins/PlatformImplementation'; require('app-module-path').addPath(__dirname); @@ -18,7 +20,7 @@ const shim = require('lib/shim'); const MasterKey = require('lib/models/MasterKey'); const Folder = require('lib/models/Folder'); const { _, setLocale } = require('lib/locale.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const fs = require('fs-extra'); const Tag = require('lib/models/Tag.js'); const { reg } = require('lib/registry.js'); @@ -31,7 +33,7 @@ const ResourceService = require('lib/services/ResourceService'); const ClipperServer = require('lib/ClipperServer'); const actionApi = require('lib/services/rest/actionApi.desktop').default; const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { shell, webFrame, clipboard } = require('electron'); const Menu = bridge().Menu; const PluginManager = require('lib/services/PluginManager'); @@ -1357,23 +1359,24 @@ class Application extends BaseApplication { pluginLogger.addTarget('console', { prefix: 'Plugin Service:' }); pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO); + const pluginRunner = new PluginRunner(); PluginService.instance().setLogger(pluginLogger); - // PluginService.instance().initialize(SandboxImplementation.instance(), this.store()); + PluginService.instance().initialize(PlatformImplementation.instance(), pluginRunner, this.store()); - // try { - // if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir')); - // } catch (error) { - // this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error); - // } + try { + if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir')); + } catch (error) { + this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error); + } - // try { - // if (Setting.value('plugins.devPluginPaths')) { - // const paths = Setting.value('plugins.devPluginPaths').split(',').map((p:string) => p.trim()); - // await PluginService.instance().loadAndRunPlugins(paths); - // } - // } catch (error) { - // this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error); - // } + try { + if (Setting.value('plugins.devPluginPaths')) { + const paths = Setting.value('plugins.devPluginPaths').split(',').map((p:string) => p.trim()); + await PluginService.instance().loadAndRunPlugins(paths); + } + } catch (error) { + this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error); + } // const url = require('url'); @@ -1395,7 +1398,7 @@ class Application extends BaseApplication { // const ipcRenderer = require('electron').ipcRenderer; // ipcRenderer.on('pluginMessage', (event:any, data:any) => { - // console.info('GOT MESSAGE ON MAIN WINDOW', event, data); + // console.info('RENDERER PROCESS got message', data); // }); diff --git a/ElectronClient/bridge.js b/ElectronClient/bridge.ts similarity index 80% rename from ElectronClient/bridge.js rename to ElectronClient/bridge.ts index 87e43133ac..41dbfe06eb 100644 --- a/ElectronClient/bridge.js +++ b/ElectronClient/bridge.ts @@ -1,13 +1,26 @@ +import ElectronAppWrapper from "./ElectronAppWrapper"; + const { _, setLocale } = require('lib/locale.js'); const shim = require('lib/shim'); const { dirname, toSystemSlashes } = require('lib/path-utils.js'); const { BrowserWindow, nativeTheme } = require('electron'); -class Bridge { +interface LastSelectedPath { + file: string, + directory: string, +} - constructor(electronWrapper) { +interface LastSelectedPaths { + [key:string]: LastSelectedPath, +} + +export class Bridge { + + private electronWrapper_:ElectronAppWrapper; + private lastSelectedPaths_:LastSelectedPaths; + + constructor(electronWrapper:ElectronAppWrapper) { this.electronWrapper_ = electronWrapper; - this.autoUpdateLogger_ = null; this.lastSelectedPaths_ = { file: null, directory: null, @@ -30,11 +43,11 @@ class Bridge { return this.electronWrapper_.window(); } - showItemInFolder(fullPath) { + showItemInFolder(fullPath:string) { return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath)); } - newBrowserWindow(options) { + newBrowserWindow(options:any) { return new BrowserWindow(options); } @@ -50,7 +63,7 @@ class Bridge { return { width: s[0], height: s[1] }; } - windowSetSize(width, height) { + windowSetSize(width:number, height:number) { if (!this.window()) return; return this.window().setSize(width, height); } @@ -63,7 +76,7 @@ class Bridge { return this.window().webContents.closeDevTools(); } - showSaveDialog(options) { + showSaveDialog(options:any) { const { dialog } = require('electron'); if (!options) options = {}; if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file; @@ -74,7 +87,7 @@ class Bridge { return filePath; } - showOpenDialog(options) { + showOpenDialog(options:any) { const { dialog } = require('electron'); if (!options) options = {}; let fileType = 'file'; @@ -89,13 +102,13 @@ class Bridge { } // Don't use this directly - call one of the showXxxxxxxMessageBox() instead - showMessageBox_(window, options) { + showMessageBox_(window:any, options:any) { const { dialog } = require('electron'); if (!window) window = this.window(); return dialog.showMessageBoxSync(window, options); } - showErrorMessageBox(message) { + showErrorMessageBox(message:string) { return this.showMessageBox_(this.window(), { type: 'error', message: message, @@ -103,7 +116,7 @@ class Bridge { }); } - showConfirmMessageBox(message, options = null) { + showConfirmMessageBox(message:string, options:any = null) { if (options === null) options = {}; const result = this.showMessageBox_(this.window(), Object.assign({}, { @@ -117,7 +130,7 @@ class Bridge { } /* returns the index of the clicked button */ - showMessageBox(message, options = null) { + showMessageBox(message:string, options:any = null) { if (options === null) options = {}; const result = this.showMessageBox_(this.window(), Object.assign({}, { @@ -129,7 +142,7 @@ class Bridge { return result; } - showInfoMessageBox(message, options = {}) { + showInfoMessageBox(message:string, options:any = {}) { const result = this.showMessageBox_(this.window(), Object.assign({}, { type: 'info', message: message, @@ -138,7 +151,7 @@ class Bridge { return result === 0; } - setLocale(locale) { + setLocale(locale:string) { setLocale(locale); } @@ -150,15 +163,15 @@ class Bridge { return require('electron').MenuItem; } - openExternal(url) { + openExternal(url:string) { return require('electron').shell.openExternal(url); } - openItem(fullPath) { + openItem(fullPath:string) { return require('electron').shell.openItem(fullPath); } - checkForUpdates(inBackground, window, logFilePath, options) { + checkForUpdates(inBackground:boolean, window:any, logFilePath:string, options:any) { const { checkForUpdates } = require('./checkForUpdates.js'); checkForUpdates(inBackground, window, logFilePath, options); } @@ -175,7 +188,7 @@ class Bridge { return nativeTheme.shouldUseDarkColors; } - addEventListener(name, fn) { + addEventListener(name:string, fn:Function) { if (name === 'nativeThemeUpdated') { nativeTheme.on('updated', fn); } else { @@ -205,17 +218,15 @@ class Bridge { } -let bridge_ = null; +let bridge_:Bridge = null; -function initBridge(wrapper) { +export function initBridge(wrapper:ElectronAppWrapper) { if (bridge_) throw new Error('Bridge already initialized'); bridge_ = new Bridge(wrapper); return bridge_; } -function bridge() { +export default function bridge() { if (!bridge_) throw new Error('Bridge not initialized'); return bridge_; } - -module.exports = { bridge, initBridge }; diff --git a/ElectronClient/checkForUpdates.js b/ElectronClient/checkForUpdates.js index d5933b17e5..768bb6d8a8 100644 --- a/ElectronClient/checkForUpdates.js +++ b/ElectronClient/checkForUpdates.js @@ -1,6 +1,6 @@ const { dialog } = require('electron'); const shim = require('lib/shim'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { _ } = require('lib/locale.js'); const fetch = require('node-fetch'); const { fileExtension } = require('lib/path-utils.js'); diff --git a/ElectronClient/commands/startExternalEditing.ts b/ElectronClient/commands/startExternalEditing.ts index c6736b9bc1..ee7d7a0f1b 100644 --- a/ElectronClient/commands/startExternalEditing.ts +++ b/ElectronClient/commands/startExternalEditing.ts @@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandServi const { _ } = require('lib/locale'); const Note = require('lib/models/Note'); const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; interface Props { noteId: string diff --git a/ElectronClient/gui/ClipperConfigScreen.jsx b/ElectronClient/gui/ClipperConfigScreen.jsx index c4415283ca..9b276074a2 100644 --- a/ElectronClient/gui/ClipperConfigScreen.jsx +++ b/ElectronClient/gui/ClipperConfigScreen.jsx @@ -1,6 +1,6 @@ const React = require('react'); const { connect } = require('react-redux'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { themeStyle } = require('lib/theme'); const { _ } = require('lib/locale.js'); const ClipperServer = require('lib/ClipperServer'); diff --git a/ElectronClient/gui/ConfigScreen/ConfigScreen.tsx b/ElectronClient/gui/ConfigScreen/ConfigScreen.tsx index 3236fd50a9..d1cfab2651 100644 --- a/ElectronClient/gui/ConfigScreen/ConfigScreen.tsx +++ b/ElectronClient/gui/ConfigScreen/ConfigScreen.tsx @@ -9,7 +9,7 @@ const pathUtils = require('lib/path-utils.js'); const { _ } = require('lib/locale.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry'); const shared = require('lib/components/shared/config-shared.js'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min'); const { ClipperConfigScreen } = require('../ClipperConfigScreen.min'); const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); diff --git a/ElectronClient/gui/DropboxLoginScreen.tsx b/ElectronClient/gui/DropboxLoginScreen.tsx index 2c316a6f15..b5dedc9d20 100644 --- a/ElectronClient/gui/DropboxLoginScreen.tsx +++ b/ElectronClient/gui/DropboxLoginScreen.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import ButtonBar from './ConfigScreen/ButtonBar'; const { connect } = require('react-redux'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { themeStyle } = require('lib/theme'); const { _ } = require('lib/locale.js'); const Shared = require('lib/components/shared/dropbox-login-shared'); diff --git a/ElectronClient/gui/EncryptionConfigScreen.jsx b/ElectronClient/gui/EncryptionConfigScreen.jsx index 8125c9ca53..a1ab44814e 100644 --- a/ElectronClient/gui/EncryptionConfigScreen.jsx +++ b/ElectronClient/gui/EncryptionConfigScreen.jsx @@ -8,7 +8,7 @@ const { time } = require('lib/time-utils.js'); const shim = require('lib/shim'); const dialogs = require('./dialogs'); const shared = require('lib/components/shared/encryption-config-shared.js'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; class EncryptionConfigScreenComponent extends React.Component { constructor() { diff --git a/ElectronClient/gui/ExtensionBadge.jsx b/ElectronClient/gui/ExtensionBadge.jsx index 0d4a03b300..4afa38bc3e 100644 --- a/ElectronClient/gui/ExtensionBadge.jsx +++ b/ElectronClient/gui/ExtensionBadge.jsx @@ -1,5 +1,5 @@ const React = require('react'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const styleSelector = require('./style/ExtensionBadge'); const { _ } = require('lib/locale.js'); diff --git a/ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx b/ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx index abfae06479..f9718e7e6f 100644 --- a/ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx +++ b/ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx @@ -8,7 +8,7 @@ import useKeymap from './utils/useKeymap'; import useCommandStatus from './utils/useCommandStatus'; import styles_ from './styles'; -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const shim = require('lib/shim'); const { _ } = require('lib/locale'); diff --git a/ElectronClient/gui/MainScreen/MainScreen.tsx b/ElectronClient/gui/MainScreen/MainScreen.tsx index 93c6b237b9..6d238352a8 100644 --- a/ElectronClient/gui/MainScreen/MainScreen.tsx +++ b/ElectronClient/gui/MainScreen/MainScreen.tsx @@ -23,7 +23,7 @@ const Setting = require('lib/models/Setting').default; const shim = require('lib/shim'); const { themeStyle } = require('lib/theme.js'); const { _ } = require('lib/locale.js'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const PluginManager = require('lib/services/PluginManager'); const EncryptionService = require('lib/services/EncryptionService'); const ipcRenderer = require('electron').ipcRenderer; diff --git a/ElectronClient/gui/MainScreen/commands/exportPdf.ts b/ElectronClient/gui/MainScreen/commands/exportPdf.ts index 81054d0713..b3977efe80 100644 --- a/ElectronClient/gui/MainScreen/commands/exportPdf.ts +++ b/ElectronClient/gui/MainScreen/commands/exportPdf.ts @@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration } from '../../../lib/services/Comman const Note = require('lib/models/Note'); const { _ } = require('lib/locale'); const shim = require('lib/shim'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const InteropServiceHelper = require('../../../InteropServiceHelper.js'); export const declaration:CommandDeclaration = { diff --git a/ElectronClient/gui/MainScreen/commands/newFolder.ts b/ElectronClient/gui/MainScreen/commands/newFolder.ts index 6a9cae5562..76b2e329e2 100644 --- a/ElectronClient/gui/MainScreen/commands/newFolder.ts +++ b/ElectronClient/gui/MainScreen/commands/newFolder.ts @@ -1,7 +1,7 @@ import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; const { _ } = require('lib/locale'); const Folder = require('lib/models/Folder'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; export const declaration:CommandDeclaration = { name: 'newFolder', diff --git a/ElectronClient/gui/MainScreen/commands/print.ts b/ElectronClient/gui/MainScreen/commands/print.ts index 5ab98e9874..e57f1b0b2f 100644 --- a/ElectronClient/gui/MainScreen/commands/print.ts +++ b/ElectronClient/gui/MainScreen/commands/print.ts @@ -1,6 +1,6 @@ import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; const { _ } = require('lib/locale'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; export const declaration:CommandDeclaration = { name: 'print', diff --git a/ElectronClient/gui/MainScreen/commands/renameFolder.ts b/ElectronClient/gui/MainScreen/commands/renameFolder.ts index 28e25d075f..f020059182 100644 --- a/ElectronClient/gui/MainScreen/commands/renameFolder.ts +++ b/ElectronClient/gui/MainScreen/commands/renameFolder.ts @@ -1,7 +1,7 @@ import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; const Folder = require('lib/models/Folder'); const { _ } = require('lib/locale'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; export const declaration:CommandDeclaration = { name: 'renameFolder', diff --git a/ElectronClient/gui/MainScreen/commands/renameTag.ts b/ElectronClient/gui/MainScreen/commands/renameTag.ts index 52d55a5de9..da273b133c 100644 --- a/ElectronClient/gui/MainScreen/commands/renameTag.ts +++ b/ElectronClient/gui/MainScreen/commands/renameTag.ts @@ -1,7 +1,7 @@ import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; const Tag = require('lib/models/Tag'); const { _ } = require('lib/locale'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; export const declaration:CommandDeclaration = { name: 'renameTag', diff --git a/ElectronClient/gui/MultiNoteActions.tsx b/ElectronClient/gui/MultiNoteActions.tsx index 764b628bba..e108b40ddc 100644 --- a/ElectronClient/gui/MultiNoteActions.tsx +++ b/ElectronClient/gui/MultiNoteActions.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import NoteListUtils from './utils/NoteListUtils'; const { buildStyle } = require('lib/theme'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; interface MultiNoteActionsProps { themeId: number, diff --git a/ElectronClient/gui/Navigator.jsx b/ElectronClient/gui/Navigator.jsx index e467defd1d..fa89f74955 100644 --- a/ElectronClient/gui/Navigator.jsx +++ b/ElectronClient/gui/Navigator.jsx @@ -2,7 +2,7 @@ const React = require('react'); const Component = React.Component; const Setting = require('lib/models/Setting').default; const { connect } = require('react-redux'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; class NavigatorComponent extends Component { UNSAFE_componentWillReceiveProps(newProps) { diff --git a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx index 093bedb5b9..cf18842020 100644 --- a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx +++ b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx @@ -15,7 +15,7 @@ import usePluginServiceRegistration from '../../utils/usePluginServiceRegistrati import Setting from 'lib/models/Setting'; // @ts-ignore -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; // @ts-ignore const Note = require('lib/models/Note.js'); const { clipboard } = require('electron'); diff --git a/ElectronClient/gui/NoteEditor/NoteEditor.tsx b/ElectronClient/gui/NoteEditor/NoteEditor.tsx index fb89021e1f..dc45c993bb 100644 --- a/ElectronClient/gui/NoteEditor/NoteEditor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteEditor.tsx @@ -35,7 +35,7 @@ const usePrevious = require('lib/hooks/usePrevious').default; const Setting = require('lib/models/Setting').default; const { _ } = require('lib/locale'); const Note = require('lib/models/Note.js'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); const NoteRevisionViewer = require('../NoteRevisionViewer.min'); const TagList = require('../TagList.min.js'); diff --git a/ElectronClient/gui/NoteEditor/utils/contextMenu.ts b/ElectronClient/gui/NoteEditor/utils/contextMenu.ts index a8f523a003..299c2f481b 100644 --- a/ElectronClient/gui/NoteEditor/utils/contextMenu.ts +++ b/ElectronClient/gui/NoteEditor/utils/contextMenu.ts @@ -1,6 +1,6 @@ import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher/index'; -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; const Resource = require('lib/models/Resource.js'); diff --git a/ElectronClient/gui/NoteEditor/utils/resourceHandling.ts b/ElectronClient/gui/NoteEditor/utils/resourceHandling.ts index b5036d027d..9c86585992 100644 --- a/ElectronClient/gui/NoteEditor/utils/resourceHandling.ts +++ b/ElectronClient/gui/NoteEditor/utils/resourceHandling.ts @@ -3,7 +3,7 @@ const Note = require('lib/models/Note.js'); const BaseModel = require('lib/BaseModel.js'); const Resource = require('lib/models/Resource.js'); const shim = require('lib/shim'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const ResourceFetcher = require('lib/services/ResourceFetcher.js'); const { reg } = require('lib/registry.js'); const joplinRendererUtils = require('lib/joplin-renderer').utils; diff --git a/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts b/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts index a45be63efb..c9208dd6aa 100644 --- a/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts +++ b/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts @@ -6,7 +6,7 @@ const BaseItem = require('lib/models/BaseItem'); const { _ } = require('lib/locale'); const BaseModel = require('lib/BaseModel.js'); const Resource = require('lib/models/Resource.js'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { urlDecode } = require('lib/string-utils'); const urlUtils = require('lib/urlUtils'); const ResourceFetcher = require('lib/services/ResourceFetcher.js'); diff --git a/ElectronClient/gui/NoteList/NoteList.tsx b/ElectronClient/gui/NoteList/NoteList.tsx index 7b10979497..f7fc20305d 100644 --- a/ElectronClient/gui/NoteList/NoteList.tsx +++ b/ElectronClient/gui/NoteList/NoteList.tsx @@ -8,7 +8,7 @@ const { time } = require('lib/time-utils.js'); const { themeStyle } = require('lib/theme'); const BaseModel = require('lib/BaseModel'); const { _ } = require('lib/locale.js'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const Note = require('lib/models/Note'); const Setting = require('lib/models/Setting').default; const NoteListItem = require('../NoteListItem').default; diff --git a/ElectronClient/gui/NotePropertiesDialog.jsx b/ElectronClient/gui/NotePropertiesDialog.jsx index b976765080..a3a1c8d041 100644 --- a/ElectronClient/gui/NotePropertiesDialog.jsx +++ b/ElectronClient/gui/NotePropertiesDialog.jsx @@ -6,7 +6,7 @@ const DialogButtonRow = require('./DialogButtonRow.min'); const Datetime = require('react-datetime'); const Note = require('lib/models/Note'); const formatcoords = require('formatcoords'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const shim = require('lib/shim'); class NotePropertiesDialog extends React.Component { diff --git a/ElectronClient/gui/NoteRevisionViewer.jsx b/ElectronClient/gui/NoteRevisionViewer.jsx index c2d7f26168..74d2eb1c52 100644 --- a/ElectronClient/gui/NoteRevisionViewer.jsx +++ b/ElectronClient/gui/NoteRevisionViewer.jsx @@ -14,7 +14,7 @@ const { MarkupToHtml } = require('lib/joplin-renderer'); const { time } = require('lib/time-utils.js'); const ReactTooltip = require('react-tooltip'); const { urlDecode, substrWithEllipsis } = require('lib/string-utils'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const markupLanguageUtils = require('lib/markupLanguageUtils'); class NoteRevisionViewerComponent extends React.PureComponent { diff --git a/ElectronClient/gui/OneDriveLoginScreen.tsx b/ElectronClient/gui/OneDriveLoginScreen.tsx index 80e0802ce3..53b9932998 100644 --- a/ElectronClient/gui/OneDriveLoginScreen.tsx +++ b/ElectronClient/gui/OneDriveLoginScreen.tsx @@ -4,7 +4,7 @@ import ButtonBar from './ConfigScreen/ButtonBar'; const { connect } = require('react-redux'); const { reg } = require('lib/registry.js'); const Setting = require('lib/models/Setting').default; -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { themeStyle } = require('lib/theme'); const { _ } = require('lib/locale.js'); const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js'); diff --git a/ElectronClient/gui/ResourceScreen.tsx b/ElectronClient/gui/ResourceScreen.tsx index 653ce9673f..d223c960c2 100644 --- a/ElectronClient/gui/ResourceScreen.tsx +++ b/ElectronClient/gui/ResourceScreen.tsx @@ -4,7 +4,7 @@ import ButtonBar from './ConfigScreen/ButtonBar'; const { connect } = require('react-redux'); const { _ } = require('lib/locale.js'); const { themeStyle } = require('lib/theme'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const prettyBytes = require('pretty-bytes'); const Resource = require('lib/models/Resource.js'); diff --git a/ElectronClient/gui/Root.tsx b/ElectronClient/gui/Root.tsx index 609fdf4317..38a34697ef 100644 --- a/ElectronClient/gui/Root.tsx +++ b/ElectronClient/gui/Root.tsx @@ -19,7 +19,7 @@ const { ResourceScreen } = require('./ResourceScreen.js'); const { Navigator } = require('./Navigator.min.js'); const WelcomeUtils = require('lib/WelcomeUtils'); const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; interface Props { themeId: number, diff --git a/ElectronClient/gui/Root_UpgradeSyncTarget.tsx b/ElectronClient/gui/Root_UpgradeSyncTarget.tsx index 2b37cf35d0..cd578ce152 100644 --- a/ElectronClient/gui/Root_UpgradeSyncTarget.tsx +++ b/ElectronClient/gui/Root_UpgradeSyncTarget.tsx @@ -5,7 +5,7 @@ import useSyncTargetUpgrade, { SyncTargetUpgradeResult } from 'lib/services/sync const { render } = require('react-dom'); const ipcRenderer = require('electron').ipcRenderer; const Setting = require('lib/models/Setting').default; -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; function useAppCloseHandler(upgradeResult:SyncTargetUpgradeResult) { useEffect(function() { diff --git a/ElectronClient/gui/SideBar/SideBar.tsx b/ElectronClient/gui/SideBar/SideBar.tsx index 5bcb30e03a..d8ac85ee42 100644 --- a/ElectronClient/gui/SideBar/SideBar.tsx +++ b/ElectronClient/gui/SideBar/SideBar.tsx @@ -15,7 +15,7 @@ const Tag = require('lib/models/Tag.js'); const shim = require('lib/shim'); const { _ } = require('lib/locale.js'); const { themeStyle } = require('lib/theme'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; const InteropServiceHelper = require('../../InteropServiceHelper.js'); diff --git a/ElectronClient/gui/StatusScreen/StatusScreen.tsx b/ElectronClient/gui/StatusScreen/StatusScreen.tsx index 53f1233671..c792f0f10f 100644 --- a/ElectronClient/gui/StatusScreen/StatusScreen.tsx +++ b/ElectronClient/gui/StatusScreen/StatusScreen.tsx @@ -4,7 +4,7 @@ import ButtonBar from '../ConfigScreen/ButtonBar'; const { connect } = require('react-redux'); const Setting = require('lib/models/Setting').default; -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { themeStyle } = require('lib/theme'); const { _ } = require('lib/locale.js'); const { ReportService } = require('lib/services/report.js'); diff --git a/ElectronClient/gui/VerticalResizer.jsx b/ElectronClient/gui/VerticalResizer.jsx index a10eab6989..27bf432a58 100644 --- a/ElectronClient/gui/VerticalResizer.jsx +++ b/ElectronClient/gui/VerticalResizer.jsx @@ -1,5 +1,5 @@ const React = require('react'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; class VerticalResizer extends React.PureComponent { constructor() { diff --git a/ElectronClient/gui/utils/NoteListUtils.ts b/ElectronClient/gui/utils/NoteListUtils.ts index c3ff071ee0..24cf7956d0 100644 --- a/ElectronClient/gui/utils/NoteListUtils.ts +++ b/ElectronClient/gui/utils/NoteListUtils.ts @@ -3,7 +3,7 @@ import { utils as pluginUtils, PluginStates } from 'lib/services/plugins/reducer const BaseModel = require('lib/BaseModel'); const { _ } = require('lib/locale.js'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; const eventManager = require('lib/eventManager').default; diff --git a/ElectronClient/main-html.js b/ElectronClient/main-html.js index f88fee82bc..d80056ea6f 100644 --- a/ElectronClient/main-html.js +++ b/ElectronClient/main-html.js @@ -23,11 +23,11 @@ const NoteTag = require('lib/models/NoteTag.js'); const MasterKey = require('lib/models/MasterKey'); const Setting = require('lib/models/Setting').default; const Revision = require('lib/models/Revision.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { FsDriverNode } = require('lib/fs-driver-node.js'); const { shimInit } = require('lib/shim-init-node.js'); const EncryptionService = require('lib/services/EncryptionService'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { FileApiDriverLocal } = require('lib/file-api-driver-local.js'); if (bridge().env() === 'dev') { diff --git a/ElectronClient/main.js b/ElectronClient/main.js index 10d0c51aae..b80480cdfc 100644 --- a/ElectronClient/main.js +++ b/ElectronClient/main.js @@ -4,9 +4,9 @@ require('app-module-path').addPath(__dirname); const electronApp = require('electron').app; -const { ElectronAppWrapper } = require('./ElectronAppWrapper'); +const ElectronAppWrapper = require('./ElectronAppWrapper').default; const { initBridge } = require('./bridge'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { FsDriverNode } = require('lib/fs-driver-node.js'); const envFromArgs = require('lib/envFromArgs'); diff --git a/ElectronClient/services/bridge.ts b/ElectronClient/services/bridge.ts new file mode 100644 index 0000000000..1419140185 --- /dev/null +++ b/ElectronClient/services/bridge.ts @@ -0,0 +1,9 @@ +// Just a convenient wrapper to get a typed bridge in TypeScript + +import { Bridge } from '../bridge'; + +const remoteBridge = require('electron').remote.require('./bridge').default; + +export default function bridge():Bridge { + return remoteBridge(); +} diff --git a/ElectronClient/services/plugins/PluginRunner.ts b/ElectronClient/services/plugins/PluginRunner.ts new file mode 100644 index 0000000000..69203844a4 --- /dev/null +++ b/ElectronClient/services/plugins/PluginRunner.ts @@ -0,0 +1,116 @@ +import Plugin from 'lib/services/plugins/Plugin'; +import BasePluginRunner from 'lib/services/plugins/BasePluginRunner'; +import executeSandboxCall from 'lib/services/plugins/utils/executeSandboxCall'; +import Global from 'lib/services/plugins/api/Global'; +import bridge from '../bridge'; +import Setting from 'lib/models/Setting'; +import { EventHandlers } from 'lib/services/plugins/utils/mapEventHandlersToIds'; +const shim = require('lib/shim'); +const ipcRenderer = require('electron').ipcRenderer; + +enum PluginMessageTarget { + MainWindow = 'mainWindow', + Plugin = 'plugin', +} + +export interface PluginMessage { + target: PluginMessageTarget, + pluginId: string, + callbackId?: string, + path?: string, + args?: any[], + result?: any, + error?: any, +} + +function mapEventIdsToHandlers(pluginId:string, arg:any) { + if (Array.isArray(arg)) { + for (let i = 0; i < arg.length; i++) { + arg[i] = mapEventIdsToHandlers(pluginId, arg[i]); + } + return arg; + } else if (typeof arg === 'string' && arg.indexOf('___plugin_event_') === 0) { + const eventId = arg; + + return async (...args:any[]) => { + ipcRenderer.send('pluginMessage', { + target: PluginMessageTarget.Plugin, + pluginId: pluginId, + eventId: eventId, + args: args, + }); + }; + } else if (arg === null) { + return null; + } else if (arg === undefined) { + return undefined; + } else if (typeof arg === 'object') { + for (const n in arg) { + arg[n] = mapEventIdsToHandlers(pluginId, arg[n]); + } + } + + return arg; +} + +export default class PluginRunner extends BasePluginRunner { + + protected eventHandlers_:EventHandlers = {}; + + constructor() { + super(); + + this.eventHandler = this.eventHandler.bind(this); + } + + private async eventHandler(eventHandlerId:string, args:any[]) { + const cb = this.eventHandlers_[eventHandlerId]; + return cb(...args); + } + + async run(plugin:Plugin, pluginApi:Global) { + const scriptPath = `${Setting.value('tempDir')}/plugin_${plugin.id}.js`; + await shim.fsDriver().writeFile(scriptPath, plugin.scriptText, 'utf8'); + + const pluginWindow = bridge().newBrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + }, + }); + + bridge().electronApp().registerPluginWindow(plugin.id, pluginWindow); + + pluginWindow.loadURL(`${require('url').format({ + pathname: require('path').join(__dirname, 'plugin_index.html'), + protocol: 'file:', + slashes: true, + })}?pluginId=${plugin.id}&pluginScript=file://${scriptPath}`); // TODO: escape!! + + pluginWindow.webContents.openDevTools(); + + ipcRenderer.on('pluginMessage', async (_event:any, message:PluginMessage) => { + if (message.target !== PluginMessageTarget.MainWindow) return; + if (message.pluginId !== plugin.id) return; + + const mappedArgs = mapEventIdsToHandlers(plugin.id, message.args); + + let result:any = null; + let error:any = null; + try { + result = await executeSandboxCall(plugin.id, pluginApi, `joplin.${message.path}`, mappedArgs, this.eventHandler); + } catch (e) { + error = e ? e : new Error('Unknown error'); + } + + ipcRenderer.send('pluginMessage', { + target: PluginMessageTarget.Plugin, + pluginId: plugin.id, + callbackId: message.callbackId, + result: result, + error: error, + }); + }); + } + +} diff --git a/ElectronClient/services/plugins/plugin_index.js b/ElectronClient/services/plugins/plugin_index.js index 09f3e890ff..a5b9db8781 100644 --- a/ElectronClient/services/plugins/plugin_index.js +++ b/ElectronClient/services/plugins/plugin_index.js @@ -1,24 +1,24 @@ (function(globalObject) { // TODO: Not sure if that will work once packaged in Electron - const sandboxProxy = require('../lib/services/plugins/sandboxProxy.js').default; + const sandboxProxy = require('../../lib/services/plugins/sandboxProxy.js').default; const ipcRenderer = require('electron').ipcRenderer; const urlParams = new URLSearchParams(window.location.search); const pluginId = urlParams.get('pluginId'); - let callbackId_ = 1; - const callbacks_ = {}; + let eventId_ = 1; + const eventHandlers_ = {}; - function mapFunctionsToCallbacks(arg) { + function mapEventHandlersToIds(argName, arg) { if (Array.isArray(arg)) { for (let i = 0; i < arg.length; i++) { - arg[i] = mapFunctionsToCallbacks(arg[i]); + arg[i] = mapEventHandlersToIds(`${i}`, arg[i]); } return arg; } else if (typeof arg === 'function') { - const id = `__event#${callbackId_}`; - callbackId_++; - callbacks_[id] = arg; + const id = `___plugin_event_${argName}_${eventId_}`; + eventId_++; + eventHandlers_[id] = arg; return id; } else if (arg === null) { return null; @@ -26,16 +26,68 @@ return undefined; } else if (typeof arg === 'object') { for (const n in arg) { - arg[n] = mapFunctionsToCallbacks(arg[n]); + arg[n] = mapEventHandlersToIds(n, arg[n]); } } return arg; } + const callbackPromises = {}; + let callbackIndex = 1; + const target = (path, args) => { - ipcRenderer.send('pluginMessage', { target: 'mainWindow', pluginId: pluginId, path: path, args: mapFunctionsToCallbacks(args) }); + const callbackId = `cb_${pluginId}_${Date.now()}_${callbackIndex++}`; + const promise = new Promise((resolve, reject) => { + callbackPromises[callbackId] = { resolve, reject }; + }); + + ipcRenderer.send('pluginMessage', { + target: 'mainWindow', + pluginId: pluginId, + callbackId: callbackId, + path: path, + args: mapEventHandlersToIds(null, args), + }); + + return promise; }; + ipcRenderer.on('pluginMessage', (event, message) => { + if (message.eventId) { + const eventHandler = eventHandlers_[message.eventId]; + + if (!eventHandler) { + console.error('Got an event ID but no matching event handler: ', message); + return; + } + + eventHandler(...message.args); + return; + } + + if (message.callbackId) { + const promise = callbackPromises[message.callbackId]; + if (!promise) { + console.error('Got a callback without matching promise: ', message); + return; + } + + if (message.error) { + promise.reject(message.error); + } else { + promise.resolve(message.result); + } + return; + } + + console.warn('Unhandled plugin message:', message); + }); + + const pluginScriptPath = urlParams.get('pluginScript'); + const script = document.createElement('script'); + script.src = pluginScriptPath; + document.head.appendChild(script); + globalObject.joplin = sandboxProxy(target); })(window); diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js index 6e6eb93105..414ca17760 100644 --- a/ReactNativeClient/lib/BaseApplication.js +++ b/ReactNativeClient/lib/BaseApplication.js @@ -10,7 +10,7 @@ const BaseItem = require('lib/models/BaseItem.js'); const Note = require('lib/models/Note.js'); const Tag = require('lib/models/Tag.js'); const Setting = require('lib/models/Setting').default; -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { splitCommandString } = require('lib/string-utils.js'); const { reg } = require('lib/registry.js'); const { time } = require('lib/time-utils.js'); diff --git a/ReactNativeClient/lib/ClipperServer.js b/ReactNativeClient/lib/ClipperServer.js index f9c1ad4d3b..840a167d25 100644 --- a/ReactNativeClient/lib/ClipperServer.js +++ b/ReactNativeClient/lib/ClipperServer.js @@ -1,6 +1,6 @@ const urlParser = require('url'); const Setting = require('lib/models/Setting').default; -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { randomClipperPort, startPort } = require('lib/randomClipperPort'); const enableServerDestroy = require('server-destroy'); const Api = require('lib/services/rest/Api'); diff --git a/ReactNativeClient/lib/DropboxApi.js b/ReactNativeClient/lib/DropboxApi.js index 9ff84065c6..59b0ab61c5 100644 --- a/ReactNativeClient/lib/DropboxApi.js +++ b/ReactNativeClient/lib/DropboxApi.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const shim = require('lib/shim'); const JoplinError = require('lib/JoplinError'); const { time } = require('lib/time-utils'); diff --git a/ReactNativeClient/lib/JoplinServerApi.ts b/ReactNativeClient/lib/JoplinServerApi.ts index 291044559a..d8df0670c2 100644 --- a/ReactNativeClient/lib/JoplinServerApi.ts +++ b/ReactNativeClient/lib/JoplinServerApi.ts @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const shim = require('lib/shim'); const JoplinError = require('lib/JoplinError'); const { rtrimSlashes } = require('lib/path-utils.js'); diff --git a/ReactNativeClient/lib/Logger_2.js b/ReactNativeClient/lib/Logger.ts similarity index 60% rename from ReactNativeClient/lib/Logger_2.js rename to ReactNativeClient/lib/Logger.ts index f10313efe8..5045f233d0 100644 --- a/ReactNativeClient/lib/Logger_2.js +++ b/ReactNativeClient/lib/Logger.ts @@ -1,22 +1,52 @@ const moment = require('moment'); -const { _ } = require('lib/locale.js'); const { time } = require('lib/time-utils.js'); const { FsDriverDummy } = require('lib/fs-driver-dummy.js'); +enum TargetType { + Database = 'database', + File = 'file', + Console = 'console', +} + +enum LogLevel { + None = 0, + Error = 10, + Warn = 20, + Info = 30, + Debug = 40, +} + +interface Target { + type: TargetType, + level?: LogLevel, + database?: any, + console?: any, + prefix?: string, + path?: string, + source?: string, +} + class Logger { - constructor() { - this.targets_ = []; - this.level_ = Logger.LEVEL_INFO; - this.fileAppendQueue_ = []; - this.lastDbCleanup_ = time.unixMs(); - } + + // For backward compatibility + public static LEVEL_NONE = LogLevel.None; + public static LEVEL_ERROR = LogLevel.Error; + public static LEVEL_WARN = LogLevel.Warn; + public static LEVEL_INFO = LogLevel.Info; + public static LEVEL_DEBUG = LogLevel.Debug; + + public static fsDriver_:any = null; + + private targets_:Target[] = []; + private level_:LogLevel = LogLevel.Info; + private lastDbCleanup_:number = time.unixMs(); static fsDriver() { if (!Logger.fsDriver_) Logger.fsDriver_ = new FsDriverDummy(); return Logger.fsDriver_; } - setLevel(level) { + setLevel(level:LogLevel) { this.level_ = level; } @@ -28,25 +58,22 @@ class Logger { return this.targets_; } - clearTargets() { - this.targets_.clear(); - } - - addTarget(type, options = null) { + addTarget(type:TargetType, options:any = null) { const target = { type: type }; for (const n in options) { if (!options.hasOwnProperty(n)) continue; - target[n] = options[n]; + (target as any)[n] = options[n]; } this.targets_.push(target); } - objectToString(object) { + objectToString(object:any) { let output = ''; if (typeof object === 'object') { if (object instanceof Error) { + object = object as any; output = object.toString(); if (object.code) output += `\nCode: ${object.code}`; if (object.headers) output += `\nHeader: ${JSON.stringify(object.headers)}`; @@ -62,7 +89,7 @@ class Logger { return output; } - objectsToString(...object) { + objectsToString(...object:any[]) { const output = []; for (let i = 0; i < object.length; i++) { output.push(`"${this.objectToString(object[i])}"`); @@ -84,9 +111,9 @@ class Logger { } // Only for database at the moment - async lastEntries(limit = 100, options = null) { + async lastEntries(limit:number = 100, options:any = null) { if (options === null) options = {}; - if (!options.levels) options.levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR]; + if (!options.levels) options.levels = [LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error]; if (!options.levels.length) return []; for (let i = 0; i < this.targets_.length; i++) { @@ -100,12 +127,12 @@ class Logger { return []; } - targetLevel(target) { + targetLevel(target:Target) { if ('level' in target) return target.level; return this.level(); } - log(level, ...object) { + log(level:LogLevel, ...object:any[]) { if (!this.targets_.length) return; const timestamp = moment().format('YYYY-MM-DD HH:mm:ss'); @@ -118,9 +145,9 @@ class Logger { if (target.type == 'console') { let fn = 'log'; - if (level == Logger.LEVEL_ERROR) fn = 'error'; - if (level == Logger.LEVEL_WARN) fn = 'warn'; - if (level == Logger.LEVEL_INFO) fn = 'info'; + if (level == LogLevel.Error) fn = 'error'; + if (level == LogLevel.Warn) fn = 'warn'; + if (level == LogLevel.Info) fn = 'info'; const consoleObj = target.console ? target.console : console; let items = [moment().format('HH:mm:ss')]; if (target.prefix) { @@ -160,55 +187,41 @@ class Logger { } } - error(...object) { - return this.log(Logger.LEVEL_ERROR, ...object); + error(...object:any[]) { + return this.log(LogLevel.Error, ...object); } - warn(...object) { - return this.log(Logger.LEVEL_WARN, ...object); + warn(...object:any[]) { + return this.log(LogLevel.Warn, ...object); } - info(...object) { - return this.log(Logger.LEVEL_INFO, ...object); + info(...object:any[]) { + return this.log(LogLevel.Info, ...object); } - debug(...object) { - return this.log(Logger.LEVEL_DEBUG, ...object); + debug(...object:any[]) { + return this.log(LogLevel.Debug, ...object); } - static levelStringToId(s) { - if (s == 'none') return Logger.LEVEL_NONE; - if (s == 'error') return Logger.LEVEL_ERROR; - if (s == 'warn') return Logger.LEVEL_WARN; - if (s == 'info') return Logger.LEVEL_INFO; - if (s == 'debug') return Logger.LEVEL_DEBUG; - throw new Error(_('Unknown log level: %s', s)); + static levelStringToId(s:string) { + if (s == 'none') return LogLevel.None; + if (s == 'error') return LogLevel.Error; + if (s == 'warn') return LogLevel.Warn; + if (s == 'info') return LogLevel.Info; + if (s == 'debug') return LogLevel.Debug; + throw new Error('Unknown log level: ' + s); } - static levelIdToString(id) { - if (id == Logger.LEVEL_NONE) return 'none'; - if (id == Logger.LEVEL_ERROR) return 'error'; - if (id == Logger.LEVEL_WARN) return 'warn'; - if (id == Logger.LEVEL_INFO) return 'info'; - if (id == Logger.LEVEL_DEBUG) return 'debug'; - throw new Error(_('Unknown level ID: %s', id)); + static levelIdToString(id:LogLevel) { + if (id == LogLevel.None) return 'none'; + if (id == LogLevel.Error) return 'error'; + if (id == LogLevel.Warn) return 'warn'; + if (id == LogLevel.Info) return 'info'; + if (id == LogLevel.Debug) return 'debug'; + throw new Error('Unknown level ID: ' + id); } static levelIds() { - return [Logger.LEVEL_NONE, Logger.LEVEL_ERROR, Logger.LEVEL_WARN, Logger.LEVEL_INFO, Logger.LEVEL_DEBUG]; + return [LogLevel.None, LogLevel.Error, LogLevel.Warn, LogLevel.Info, LogLevel.Debug]; } - static levelEnum() { - const output = {}; - const ids = this.levelIds(); - for (let i = 0; i < ids.length; i++) { - output[ids[i]] = this.levelIdToString(ids[i]); - } - return output; - } } -Logger.LEVEL_NONE = 0; -Logger.LEVEL_ERROR = 10; -Logger.LEVEL_WARN = 20; -Logger.LEVEL_INFO = 30; -Logger.LEVEL_DEBUG = 40; - -module.exports = { Logger }; +export default Logger; diff --git a/ReactNativeClient/lib/TaskQueue.js b/ReactNativeClient/lib/TaskQueue.js index c08493ddae..8276d95d09 100644 --- a/ReactNativeClient/lib/TaskQueue.js +++ b/ReactNativeClient/lib/TaskQueue.js @@ -1,6 +1,6 @@ const { time } = require('lib/time-utils.js'); const Setting = require('lib/models/Setting').default; -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; class TaskQueue { constructor(name) { diff --git a/ReactNativeClient/lib/WebDavApi.js b/ReactNativeClient/lib/WebDavApi.js index a21f819eb3..3c2d060b35 100644 --- a/ReactNativeClient/lib/WebDavApi.js +++ b/ReactNativeClient/lib/WebDavApi.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const shim = require('lib/shim'); const parseXmlString = require('xml2js').parseString; const JoplinError = require('lib/JoplinError'); diff --git a/ReactNativeClient/lib/components/screens/log.js b/ReactNativeClient/lib/components/screens/log.js index 2546e8f164..218e57f8d4 100644 --- a/ReactNativeClient/lib/components/screens/log.js +++ b/ReactNativeClient/lib/components/screens/log.js @@ -6,7 +6,7 @@ const { reg } = require('lib/registry.js'); const { ScreenHeader } = require('lib/components/screen-header.js'); const { time } = require('lib/time-utils'); const { themeStyle } = require('lib/components/global-style.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { BaseScreenComponent } = require('lib/components/base-screen.js'); const { _ } = require('lib/locale.js'); diff --git a/ReactNativeClient/lib/database.js b/ReactNativeClient/lib/database.js index 41fe48c400..0adf9a0b4c 100644 --- a/ReactNativeClient/lib/database.js +++ b/ReactNativeClient/lib/database.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { time } = require('lib/time-utils.js'); const Mutex = require('async-mutex').Mutex; const shim = require('lib/shim'); diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index 297e47ee63..174b570bb4 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -1,5 +1,5 @@ const { isHidden } = require('lib/path-utils.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const shim = require('lib/shim'); const BaseItem = require('lib/models/BaseItem.js'); const JoplinError = require('lib/JoplinError'); diff --git a/ReactNativeClient/lib/ntpDate.ts b/ReactNativeClient/lib/ntpDate.ts index 554c4a613a..489ee5d285 100644 --- a/ReactNativeClient/lib/ntpDate.ts +++ b/ReactNativeClient/lib/ntpDate.ts @@ -1,5 +1,5 @@ const ntpClient = require('lib/vendor/ntp-client'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Mutex = require('async-mutex').Mutex; let nextSyncTime = 0; diff --git a/ReactNativeClient/lib/onedrive-api.js b/ReactNativeClient/lib/onedrive-api.js index d3c0922261..d874f728e1 100644 --- a/ReactNativeClient/lib/onedrive-api.js +++ b/ReactNativeClient/lib/onedrive-api.js @@ -1,7 +1,7 @@ const shim = require('lib/shim'); const { stringify } = require('query-string'); const { time } = require('lib/time-utils.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { _ } = require('lib/locale.js'); class OneDriveApi { diff --git a/ReactNativeClient/lib/react-logger.js b/ReactNativeClient/lib/react-logger.js index e0876d109c..202b6026db 100644 --- a/ReactNativeClient/lib/react-logger.js +++ b/ReactNativeClient/lib/react-logger.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; class ReactLogger extends Logger {} diff --git a/ReactNativeClient/lib/registry.js b/ReactNativeClient/lib/registry.js index 0b441c4aaf..4ccce9ca96 100644 --- a/ReactNativeClient/lib/registry.js +++ b/ReactNativeClient/lib/registry.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Setting = require('lib/models/Setting').default; const shim = require('lib/shim'); const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); diff --git a/ReactNativeClient/lib/services/AlarmServiceDriverNode.js b/ReactNativeClient/lib/services/AlarmServiceDriverNode.js index a0441bdf40..57fca12a33 100644 --- a/ReactNativeClient/lib/services/AlarmServiceDriverNode.js +++ b/ReactNativeClient/lib/services/AlarmServiceDriverNode.js @@ -1,5 +1,5 @@ const notifier = require('node-notifier'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const shim = require('lib/shim'); class AlarmServiceDriverNode { diff --git a/ReactNativeClient/lib/services/BaseService.ts b/ReactNativeClient/lib/services/BaseService.ts index 1e4af1d3dd..41cca22b93 100644 --- a/ReactNativeClient/lib/services/BaseService.ts +++ b/ReactNativeClient/lib/services/BaseService.ts @@ -1,15 +1,17 @@ +import Logger from 'lib/Logger'; + export default class BaseService { - static logger_:any = null; - protected instanceLogger_:any = null; + static logger_:Logger = null; + protected instanceLogger_:Logger = null; - logger() { + logger():Logger { if (this.instanceLogger_) return this.instanceLogger_; if (!BaseService.logger_) throw new Error('BaseService.logger_ not set!!'); return BaseService.logger_; } - setLogger(v:any) { + setLogger(v:Logger) { this.instanceLogger_ = v; } } diff --git a/ReactNativeClient/lib/services/DecryptionWorker.js b/ReactNativeClient/lib/services/DecryptionWorker.js index f8e504a79b..896d1e30b2 100644 --- a/ReactNativeClient/lib/services/DecryptionWorker.js +++ b/ReactNativeClient/lib/services/DecryptionWorker.js @@ -3,7 +3,7 @@ const BaseModel = require('lib/BaseModel'); const MasterKey = require('lib/models/MasterKey'); const Resource = require('lib/models/Resource'); const ResourceService = require('lib/services/ResourceService'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const EventEmitter = require('events'); const shim = require('lib/shim'); diff --git a/ReactNativeClient/lib/services/EncryptionService.js b/ReactNativeClient/lib/services/EncryptionService.js index 780ba7e9c6..1228005ec7 100644 --- a/ReactNativeClient/lib/services/EncryptionService.js +++ b/ReactNativeClient/lib/services/EncryptionService.js @@ -1,5 +1,5 @@ const { padLeft } = require('lib/string-utils.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const shim = require('lib/shim'); const Setting = require('lib/models/Setting').default; const MasterKey = require('lib/models/MasterKey'); diff --git a/ReactNativeClient/lib/services/ExternalEditWatcher.js b/ReactNativeClient/lib/services/ExternalEditWatcher.js index 744d097a45..72095b1800 100644 --- a/ReactNativeClient/lib/services/ExternalEditWatcher.js +++ b/ReactNativeClient/lib/services/ExternalEditWatcher.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Note = require('lib/models/Note'); const Setting = require('lib/models/Setting').default; const shim = require('lib/shim'); @@ -7,7 +7,7 @@ const { splitCommandString } = require('lib/string-utils'); const { fileExtension, basename } = require('lib/path-utils'); const spawn = require('child_process').spawn; const chokidar = require('chokidar'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { time } = require('lib/time-utils.js'); const { ErrorNotFound } = require('./rest/errors'); diff --git a/ReactNativeClient/lib/services/PluginManager.js b/ReactNativeClient/lib/services/PluginManager.js index 87fb497714..ce3e08c961 100644 --- a/ReactNativeClient/lib/services/PluginManager.js +++ b/ReactNativeClient/lib/services/PluginManager.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const KeymapService = require('lib/services/KeymapService').default; class PluginManager { diff --git a/ReactNativeClient/lib/services/ResourceEditWatcher/index.ts b/ReactNativeClient/lib/services/ResourceEditWatcher/index.ts index 0aeecbe656..8f4848bb20 100644 --- a/ReactNativeClient/lib/services/ResourceEditWatcher/index.ts +++ b/ReactNativeClient/lib/services/ResourceEditWatcher/index.ts @@ -1,11 +1,11 @@ import AsyncActionQueue from '../../AsyncActionQueue'; -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Setting = require('lib/models/Setting').default; const Resource = require('lib/models/Resource'); const shim = require('lib/shim'); const EventEmitter = require('events'); const chokidar = require('chokidar'); -const { bridge } = require('electron').remote.require('./bridge'); +const bridge = require('electron').remote.require('./bridge').default; const { _ } = require('lib/locale'); interface WatchedItem { diff --git a/ReactNativeClient/lib/services/ResourceFetcher.js b/ReactNativeClient/lib/services/ResourceFetcher.js index d42f1bf437..5bbd59318e 100644 --- a/ReactNativeClient/lib/services/ResourceFetcher.js +++ b/ReactNativeClient/lib/services/ResourceFetcher.js @@ -3,7 +3,7 @@ const Setting = require('lib/models/Setting').default; const BaseService = require('lib/services/BaseService').default; const ResourceService = require('lib/services/ResourceService'); const { Dirnames } = require('lib/services/synchronizer/utils/types'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const EventEmitter = require('events'); const shim = require('lib/shim'); diff --git a/ReactNativeClient/lib/services/plugins/BasePluginRunner.ts b/ReactNativeClient/lib/services/plugins/BasePluginRunner.ts index d96d67a3d7..46d383db6c 100644 --- a/ReactNativeClient/lib/services/plugins/BasePluginRunner.ts +++ b/ReactNativeClient/lib/services/plugins/BasePluginRunner.ts @@ -2,39 +2,8 @@ import Plugin from './Plugin'; import BaseService from '../BaseService'; import Global from './api/Global'; -interface EventHandlers { - [key:string]: Function; -} - export default abstract class BasePluginRunner extends BaseService { - private eventHandlerIndex_:number = 0; - protected eventHandlers_:EventHandlers = {}; - - protected mapEventHandlersToIds(arg:any) { - if (Array.isArray(arg)) { - for (let i = 0; i < arg.length; i++) { - arg[i] = this.mapEventHandlersToIds(arg[i]); - } - return arg; - } else if (typeof arg === 'function') { - const id = `__event#${this.eventHandlerIndex_}`; - this.eventHandlerIndex_++; - this.eventHandlers_[id] = arg; - return id; - } else if (arg === null) { - return null; - } else if (arg === undefined) { - return undefined; - } else if (typeof arg === 'object') { - for (const n in arg) { - arg[n] = this.mapEventHandlersToIds(arg[n]); - } - } - - return arg; - } - async run(plugin:Plugin, sandbox:Global) { throw new Error(`Not implemented: ${plugin} / ${sandbox}`); } diff --git a/ReactNativeClient/lib/services/plugins/PluginService.ts b/ReactNativeClient/lib/services/plugins/PluginService.ts index 28d55938fc..c8a57cd582 100644 --- a/ReactNativeClient/lib/services/plugins/PluginService.ts +++ b/ReactNativeClient/lib/services/plugins/PluginService.ts @@ -1,8 +1,6 @@ import Plugin from 'lib/services/plugins/Plugin'; import manifestFromObject from 'lib/services/plugins/utils/manifestFromObject'; import Global from 'lib/services/plugins/api/Global'; -import { SandboxContext } from 'lib/services/plugins/utils/types'; -// import sandboxProxy from 'lib/services/plugins/sandboxProxy'; import BasePluginRunner from 'lib/services/plugins/BasePluginRunner'; import BaseService from '../BaseService'; const shim = require('lib/shim'); @@ -13,10 +11,6 @@ interface Plugins { [key:string]: Plugin } -// interface Sandboxes { -// [key:string]: Sandbox; -// } - function makePluginId(source:string):string { // https://www.npmjs.com/package/slug#options return nodeSlug(source, nodeSlug.defaults.modes['rfc3986']).substr(0,32); @@ -37,7 +31,6 @@ export default class PluginService extends BaseService { private store_:any = null; private platformImplementation_:any = null; private plugins_:Plugins = {}; - // private sandboxes_:Sandboxes = {}; private runner_:BasePluginRunner = null; initialize(platformImplementation:any, runner:BasePluginRunner, store:any) { @@ -117,134 +110,10 @@ export default class PluginService extends BaseService { } } - // cliSandbox(pluginId:string) { - // let callbackId_ = 1; - // const callbacks_:any = {}; - - // function mapFunctionsToCallbacks(arg:any) { - // if (Array.isArray(arg)) { - // for (let i = 0; i < arg.length; i++) { - // arg[i] = mapFunctionsToCallbacks(arg[i]); - // } - // return arg; - // } else if (typeof arg === 'function') { - // const id = '__event#' + callbackId_; - // callbackId_++; - // callbacks_[id] = arg; - // return id; - // } else if (arg === null) { - // return null; - // } else if (arg === undefined) { - // return undefined; - // } else if (typeof arg === 'object') { - // for (const n in arg) { - // arg[n] = mapFunctionsToCallbacks(arg[n]); - // } - // } - - // return arg; - // } - - // const target = (path:string, args:any[]) => { - // console.info('GOT PATH', path, mapFunctionsToCallbacks(args)); - // this.executeSandboxCall(pluginId, 'joplin.' + path, mapFunctionsToCallbacks(args)); - // }; - - // return { - // joplin: sandboxProxy(target), - // } - // } - async runPlugin(plugin:Plugin) { this.plugins_[plugin.id] = plugin; - - // Context contains the data that is sent from the plugin to the app - // Currently it only contains the object that's registered when - // the plugin calls `joplin.plugins.register()` - const context:SandboxContext = { - runtime: null, - }; - - const sandbox = new Global(this.platformImplementation_, plugin, this.store_, context); - // this.sandboxes_[plugin.id] = sandbox; - - await this.runner_.run(plugin, sandbox); - - // const vmSandbox = vm.createContext(this.cliSandbox(plugin.id)); - - // try { - // vm.runInContext(plugin.scriptText, vmSandbox); - // } catch (error) { - // this.logger().error(`In plugin ${plugin.id}:`, error); - // return; - // } - - - - - if (!context.runtime) { - throw new Error(`Plugin ${plugin.id}: The plugin was not registered! Call joplin.plugins.register({.....}) from within the plugin.`); - } - - if (context.runtime.onStart) { - const startTime = Date.now(); - - this.logger().info(`Starting plugin: ${plugin.id}`); - - // We don't use `await` when calling onStart because the plugin might be awaiting - // in that call too (for example, when opening a dialog on startup) so we don't - // want to get stuck here. - context.runtime.onStart({}).catch((error) => { - // For some reason, error thrown from the executed script do not have the type "Error" - // but are instead plain object. So recreate the Error object here so that it can - // be handled correctly by loggers, etc. - const newError:Error = new Error(error.message); - newError.stack = error.stack; - this.logger().error(`In plugin ${plugin.id}:`, newError); - }).then(() => { - this.logger().info(`Finished running onStart handler: ${plugin.id} (Took ${Date.now() - startTime}ms)`); - }); - } - - - - - - - - - // vm.createContext(sandbox); - - // try { - // vm.runInContext(plugin.scriptText, sandbox); - // } catch (error) { - // this.logger().error(`In plugin ${plugin.id}:`, error); - // return; - // } - - // if (!context.runtime) { - // throw new Error(`Plugin ${plugin.id}: The plugin was not registered! Call joplin.plugins.register({.....}) from within the plugin.`); - // } - - // if (context.runtime.onStart) { - // const startTime = Date.now(); - - // this.logger().info(`Starting plugin: ${plugin.id}`); - - // // We don't use `await` when calling onStart because the plugin might be awaiting - // // in that call too (for example, when opening a dialog on startup) so we don't - // // want to get stuck here. - // context.runtime.onStart({}).catch((error) => { - // // For some reason, error thrown from the executed script do not have the type "Error" - // // but are instead plain object. So recreate the Error object here so that it can - // // be handled correctly by loggers, etc. - // const newError:Error = new Error(error.message); - // newError.stack = error.stack; - // this.logger().error(`In plugin ${plugin.id}:`, newError); - // }).then(() => { - // this.logger().info(`Finished running onStart handler: ${plugin.id} (Took ${Date.now() - startTime}ms)`); - // }); - // } + const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_); + return this.runner_.run(plugin, pluginApi); } } diff --git a/ReactNativeClient/lib/services/plugins/api/Global.ts b/ReactNativeClient/lib/services/plugins/api/Global.ts index a388cc9fde..ac1de821b0 100644 --- a/ReactNativeClient/lib/services/plugins/api/Global.ts +++ b/ReactNativeClient/lib/services/plugins/api/Global.ts @@ -1,6 +1,6 @@ -import { SandboxContext } from '../utils/types'; import Plugin from '../Plugin'; import Joplin from './Joplin'; +import Logger from 'lib/Logger'; const builtinModules = require('builtin-modules'); const shim = require('lib/shim'); @@ -16,14 +16,11 @@ function requireWhiteList():string[] { export default class Global { - private context: SandboxContext; private joplin_: Joplin; private consoleWrapper_:any = null; - // private :string[] = null; - constructor(implementation:any, plugin: Plugin, store: any, context: SandboxContext) { - this.context = context; - this.joplin_ = new Joplin(implementation.joplin, plugin, store, this.context); + constructor(logger:Logger, implementation:any, plugin: Plugin, store: any) { + this.joplin_ = new Joplin(logger, implementation.joplin, plugin, store); this.consoleWrapper_ = this.createConsoleWrapper(plugin.id); } diff --git a/ReactNativeClient/lib/services/plugins/api/Joplin.ts b/ReactNativeClient/lib/services/plugins/api/Joplin.ts index 4f42626450..b6f367946d 100644 --- a/ReactNativeClient/lib/services/plugins/api/Joplin.ts +++ b/ReactNativeClient/lib/services/plugins/api/Joplin.ts @@ -1,4 +1,3 @@ -import { SandboxContext } from '../utils/types'; import Plugin from '../Plugin'; import JoplinData from './JoplinData'; import JoplinPlugins from './JoplinPlugins'; @@ -9,10 +8,11 @@ import JoplinViews from './JoplinViews'; import JoplinUtils from './JoplinUtils'; import JoplinInterop from './JoplinInterop'; import JoplinSettings from './JoplinSettings'; +import Logger from 'lib/Logger'; export default class Joplin { - private api_: JoplinData = null; + private data_: JoplinData = null; private plugins_: JoplinPlugins = null; private workspace_: JoplinWorkspace = null; private filters_: JoplinFilters = null; @@ -22,9 +22,9 @@ export default class Joplin { private interop_: JoplinInterop = null; private settings_: JoplinSettings = null; - constructor(implementation:any, plugin: Plugin, store: any, context: SandboxContext) { - this.api_ = new JoplinData(); - this.plugins_ = new JoplinPlugins(context); + constructor(logger:Logger, implementation:any, plugin: Plugin, store: any) { + this.data_ = new JoplinData(); + this.plugins_ = new JoplinPlugins(logger, plugin); this.workspace_ = new JoplinWorkspace(implementation.workspace, store); this.filters_ = new JoplinFilters(); this.commands_ = new JoplinCommands(); @@ -34,8 +34,8 @@ export default class Joplin { this.settings_ = new JoplinSettings(plugin); } - get api(): JoplinData { - return this.api_; + get data(): JoplinData { + return this.data_; } get plugins(): JoplinPlugins { diff --git a/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts b/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts index 2ad0516d7a..72b4fe34c2 100644 --- a/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts +++ b/ReactNativeClient/lib/services/plugins/api/JoplinPlugins.ts @@ -1,13 +1,35 @@ -import { SandboxContext } from '../utils/types'; +import Plugin from '../Plugin'; +import Logger from 'lib/Logger'; + export default class JoplinPlugins { - private context: SandboxContext; + private logger: Logger; + private plugin: Plugin; - constructor(context: SandboxContext) { - this.context = context; + constructor(logger:Logger, plugin:Plugin) { + this.logger = logger; + this.plugin = plugin; } register(script: any) { - this.context.runtime = script; + if (script.onStart) { + const startTime = Date.now(); + + this.logger.info(`Starting plugin: ${this.plugin.id}`); + + // We don't use `await` when calling onStart because the plugin might be awaiting + // in that call too (for example, when opening a dialog on startup) so we don't + // want to get stuck here. + script.onStart({}).catch((error:any) => { + // For some reason, error thrown from the executed script do not have the type "Error" + // but are instead plain object. So recreate the Error object here so that it can + // be handled correctly by loggers, etc. + const newError:Error = new Error(error.message); + newError.stack = error.stack; + this.logger.error(`In plugin ${this.plugin.id}:`, newError); + }).then(() => { + this.logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`); + }); + } } } diff --git a/ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.ts b/ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.ts index 7b6a7610d3..87abc8cb8d 100644 --- a/ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.ts +++ b/ReactNativeClient/lib/services/plugins/utils/executeSandboxCall.ts @@ -8,7 +8,7 @@ function createEventHandlers(arg:any, eventHandler:EventHandler) { arg[i] = createEventHandlers(arg[i], eventHandler); } return arg; - } else if (typeof arg === 'string' && arg.indexOf('__event#') === 0) { + } else if (typeof arg === 'string' && arg.indexOf('___plugin_event_') === 0) { const callbackId = arg; return async (...args:any[]) => { const result = await eventHandler(callbackId, args); @@ -36,6 +36,7 @@ export default async function executeSandboxCall(pluginId:string, sandbox:Global for (const pathFragment of pathFragments) { parent = fn; fn = fn[pathFragment]; + if (!fn) throw new Error(`Invalid method call: ${path}`); } const convertedArgs = createEventHandlers(args, eventHandler); diff --git a/ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.ts b/ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.ts new file mode 100644 index 0000000000..4323fe4729 --- /dev/null +++ b/ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.ts @@ -0,0 +1,29 @@ +let eventHandlerIndex_ = 1; + +export interface EventHandlers { + [key:string]: Function; +} + +export default function mapEventHandlersToIds(arg:any, eventHandlers:EventHandlers) { + if (Array.isArray(arg)) { + for (let i = 0; i < arg.length; i++) { + arg[i] = mapEventHandlersToIds(arg[i], eventHandlers); + } + return arg; + } else if (typeof arg === 'function') { + const id = `___plugin_event_${eventHandlerIndex_}`; + eventHandlerIndex_++; + eventHandlers[id] = arg; + return id; + } else if (arg === null) { + return null; + } else if (arg === undefined) { + return undefined; + } else if (typeof arg === 'object') { + for (const n in arg) { + arg[n] = mapEventHandlersToIds(arg[n], eventHandlers); + } + } + + return arg; +} diff --git a/ReactNativeClient/lib/services/rest/Api.js b/ReactNativeClient/lib/services/rest/Api.js index 68e39c2c67..e7fdbfe45c 100644 --- a/ReactNativeClient/lib/services/rest/Api.js +++ b/ReactNativeClient/lib/services/rest/Api.js @@ -10,7 +10,7 @@ const Setting = require('lib/models/Setting').default; const htmlUtils = require('lib/htmlUtils'); const markupLanguageUtils = require('lib/markupLanguageUtils'); const mimeUtils = require('lib/mime-utils.js').mime; -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const md5 = require('md5'); const shim = require('lib/shim'); const HtmlToMd = require('lib/HtmlToMd'); diff --git a/ReactNativeClient/lib/services/searchengine/SearchEngine.js b/ReactNativeClient/lib/services/searchengine/SearchEngine.js index 4cc2e09f11..04c494666c 100644 --- a/ReactNativeClient/lib/services/searchengine/SearchEngine.js +++ b/ReactNativeClient/lib/services/searchengine/SearchEngine.js @@ -1,4 +1,4 @@ -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const ItemChange = require('lib/models/ItemChange.js'); const Setting = require('lib/models/Setting').default; const Note = require('lib/models/Note.js'); diff --git a/ReactNativeClient/lib/shim-init-node.js b/ReactNativeClient/lib/shim-init-node.js index bfb7efa79b..925d8f5c36 100644 --- a/ReactNativeClient/lib/shim-init-node.js +++ b/ReactNativeClient/lib/shim-init-node.js @@ -67,7 +67,7 @@ function shimInit() { shim.showMessageBox = (message, options = null) => { if (shim.isElectron()) { - const { bridge } = require('electron').remote.require('./bridge'); + const bridge = require('electron').remote.require('./bridge').default; return bridge().showMessageBox(message, options); } else { throw new Error('Not implemented'); @@ -402,7 +402,7 @@ function shimInit() { shim.Buffer = Buffer; shim.openUrl = url => { - const { bridge } = require('electron').remote.require('./bridge'); + const bridge = require('electron').remote.require('./bridge').default; // Returns true if it opens the file successfully; returns false if it could // not find the file. return bridge().openExternal(url); diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index 69ff917c44..1bd0ad6d84 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -9,7 +9,7 @@ const MasterKey = require('lib/models/MasterKey.js'); const BaseModel = require('lib/BaseModel.js'); const { sprintf } = require('sprintf-js'); const { time } = require('lib/time-utils.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const { _ } = require('lib/locale.js'); const shim = require('lib/shim'); // const { filename, fileExtension } = require('lib/path-utils'); diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js index 11f8fd1838..28945cb7bd 100644 --- a/ReactNativeClient/root.js +++ b/ReactNativeClient/root.js @@ -16,7 +16,7 @@ const { shimInit } = require('lib/shim-init-react.js'); const shim = require('lib/shim'); const { time } = require('lib/time-utils.js'); const { AppNav } = require('lib/components/app-nav.js'); -const { Logger } = require('lib/logger.js'); +const Logger = require('lib/Logger').default; const Note = require('lib/models/Note.js'); const Folder = require('lib/models/Folder.js'); const BaseSyncTarget = require('lib/BaseSyncTarget.js'); diff --git a/joplin.code-workspace b/joplin.code-workspace index 6859b851d4..72e9068ac3 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -637,7 +637,13 @@ "ReactNativeClient/lib/services/plugins/api/JoplinViews.js": true, "ReactNativeClient/lib/services/plugins/api/JoplinWorkspace.js": true, "ElectronClient/services/plugins/PlatformImplementation.js": true, - "ElectronClient/gui/NoteEditor/utils/usePluginServiceRegistration.js": true + "ElectronClient/gui/NoteEditor/utils/usePluginServiceRegistration.js": true, + "ElectronClient/bridge.js": true, + "ElectronClient/ElectronAppWrapper.js": true, + "ElectronClient/services/bridge.js": true, + "ElectronClient/services/plugins/PluginRunner.js": true, + "ReactNativeClient/lib/Logger.js": true, + "ReactNativeClient/lib/services/plugins/utils/mapEventHandlersToIds.js": true }, "spellright.language": [ "en" diff --git a/package.json b/package.json index 6ae41eab56..013ae29b9d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.dev.json", "build": "gulp build", "updateIgnored": "gulp updateIgnoredTypeScriptBuild", + "copyLib": "gulp copyLib", "buildPluginDoc": "typedoc --name 'Joplin Plugin Documentation' --mode file -theme 'Modules/PluginDocTheme/' --readme 'Modules/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/plugin/api/ ReactNativeClient/lib", "setupNewRelease": "node ./Tools/setupNewRelease", "postinstall": "cd Tools && npm i && cd .. && cd ReactNativeClient && npm i && cd .. && cd ElectronClient && npm i && cd .. && cd CliClient && npm i && cd .. && gulp build" diff --git a/readme/spec/plugins.md b/readme/spec/plugins.md index d8bb7834cd..b82da5a8ea 100644 --- a/readme/spec/plugins.md +++ b/readme/spec/plugins.md @@ -28,4 +28,76 @@ The plugin runner also initialises the sandbox proxy and injects it into the plu ### Plugin API -The plugin API is a light wrapper over Joplin's internal functions and services. All the platforms share some of the plugin API but there can also be some differences. For example, the desktop app exposes the text editor component commands, and so this part of the plugin API is available only on desktop. The difference between platforms is implemented using the PlatformImplementation class, which is injected in the plugin service on startup. \ No newline at end of file +The plugin API is a light wrapper over Joplin's internal functions and services. All the platforms share some of the plugin API but there can also be some differences. For example, the desktop app exposes the text editor component commands, and so this part of the plugin API is available only on desktop. The difference between platforms is implemented using the PlatformImplementation class, which is injected in the plugin service on startup. + +## Handling events between the plugin and the host + +Handling events in plugins is relatively complicated due to the need to send IPC messages and the limitations of the IPC protocol, which in particular cannot transfer functions. + +For example, let's say we define a command in the plugin: + +```typescript +joplin.commands.register({ + name: 'testCommand1', + label: 'My Test Command 1', +}, { + onExecute: (args:any) => { + alert('Testing plugin command 1'); + }, +}); +``` + +The "onExecute" event handler needs to be called whenever, for example, a toolbar button associated with this command is clicked. The problem is that it is not possible to send a function via IPC (which can only transfer plain objects), so there has to be a translation layer in between. + +The way it is done in Joplin is like so: + +In the **sandbox proxy**, the event handlers are converted to string event IDs and the original event handler is stored in a map before being sent to host via IPC. So in the example above, the command would be converted to this plain object: + +```typescript +{ + name: 'testCommand1', + label: 'My Test Command 1', +}, { + onExecute: '___event_handler_123', +} +``` + +Then, still in the sandbox proxy, we'll have a map called something like `eventHandlers`, which now will have this content: + +```typescript +eventHandlers['___event_handler_123'] = (args:any) => { + alert('Testing plugin command 1'); +} +``` + +In the **plugin runner** (Host side), all the event IDs are converted to functions again, but instead of performing the action directly, it posts an IPC message back to the sandbox proxy using the provided event ID. + +So in the host, the command will now look like this: + +```typescript +{ + name: 'testCommand1', + label: 'My Test Command 1', +}, { + onExecute: (args:any) => { + postMessage('pluginMessage', { eventId: '___event_handler_123', args: args }); + }; +} +``` + +At this point, any code in the Joplin application can call the `onExecute` function as normal without having to know about the IPC translation layer. + +When the function onExecute is eventually called, the IPC message is sent back to the sandbox proxy, which will decode it and execute it. + +So on the **sandbox proxy**, we'll have something like this: + +```typescript +window.addEventListener('message', ((event) => { + const eventId = getEventId(event); // Get back the event ID (implementation might be different) + const eventArgs = getEventArgs(event); // Get back the args (implementation might be different) + if (eventId) { + // And call the event handler + eventHandlers[eventId](...eventArgs); + } +})); +``` \ No newline at end of file