From 9f31f035781761c70f67147dc8c6a63734967bac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=BAcio=20Rubens?=
<4539235+luciorubeens@users.noreply.github.com>
Date: Wed, 30 Oct 2019 08:56:31 -0300
Subject: [PATCH] refactor: plugin sandbox composition (#1507)
* wip
* refactor: move component sandbox methods to specific folder
* feat: add PROFILE_ALL permission
* feat: add PROFILE_CURRENT permission
* feat: add PEER_CURRENT permission
* feat: add STORAGE permission
* feat: add AUDIO permission
* feat: add EVENTS permission
* feat: add ALERTS permission
* fix: component validation
* refactor: move THEMES permission to setup
* test: add component validation specs
* refactor: timers and websocket sandbox specs
* test: add audio sandbox specs
* test: add http sandbox specs
* test: add profile-all sandbox specs
* test: add alerts and events sandbox specs
* test: add font awesome and messaging sandbox specs
* test: fix profile all sandbox
* test: add profile current and webframe sandbox specs
* 'test: add ui components and storage sandbox specs
* test: peer current and route sandbox specs
* test: add avatars setup specs
* test: add wallet tabs setup specs
* test: register setup specs
* fix: ui components sandbox
* test: add menu items setup specs
* test: add routes setup specs
* test: add themes setup specs
* test: add prepare context specs
* test: add define context specs
* fix: relative imports
* refactor: create safe component
* test: add components setup specs
* feat: global components
* fix: compile template
* fix: don't load a disabled plugin
* test: add plugin-manager specs
* fix: lint
* fix: remove document object from component sandbox
* fix: props
* fix: sync changes on props
* refactor: block ref methods
* test: blocked ref methods
* fix: component name output in log
* style: validate component method
* fix: global components
* fix: computed data
* test: add methods
* fix: add root path to sandbox external modules
* fix: pass arguments to plugin methods
* fix: resolve method
* fix: regex check for html events
---
.../component/compile-template.js | 6 +
.../unit/services/plugin-manager.spec.js | 194 ++++
.../component/create-component.spec.js | 290 ++++++
.../component/get-context.spec.js | 71 ++
.../plugin-manager/component/validate.spec.js | 69 ++
.../plugin-component-sandbox.spec.js | 118 +++
.../sandbox/alerts-sandbox.spec.js | 21 +
.../sandbox/audio-sandbox.spec.js | 13 +
.../sandbox/events-sandbox.spec.js | 14 +
.../sandbox/font-awesome-sandbox.spec.js | 11 +
.../sandbox/http-sandbox.spec.js | 52 ++
.../sandbox/messaging-sandbox.spec.js | 40 +
.../sandbox/peer-current-sandbox.spec.js | 15 +
.../sandbox/profile-all-sandbox.spec.js | 32 +
.../sandbox/profile-current-sandbox.spec.js | 29 +
.../sandbox/route-sandbox.spec.js | 13 +
.../sandbox/storage-sandbox.spec.js | 60 ++
.../sandbox/timers-sandbox.spec.js | 100 ++
.../websocket-sandbox.spec.js} | 70 +-
.../setup/avatars-setup.spec.js | 44 +
.../setup/components-setup.spec.js | 54 ++
.../setup/menu-items-setup.spec.js | 50 +
.../setup/register-setup.spec.js | 14 +
.../plugin-manager/setup/routes-setup.spec.js | 70 ++
.../plugin-manager/setup/themes-setup.spec.js | 44 +
.../setup/ui-components-setup.spec.js | 17 +
.../setup/wallet-tabs-setup.spec.js | 44 +
.../setup/webframe-setup.spec.js | 17 +
.../services/plugin-manager/timers.spec.js | 103 ---
src/renderer/services/plugin-manager.js | 853 ++----------------
.../component/compile-template.js | 20 +
.../component/create-component.js | 85 ++
.../plugin-manager/component/get-context.js | 115 +++
.../plugin-manager/component/hooks.js | 9 +
.../plugin-manager/component/validate.js | 85 ++
.../plugin-manager/font-awesome-sandbox.js | 7 -
.../plugin-component-sandbox.js | 87 ++
.../plugin-manager/plugin-configuration.js | 38 +
.../plugin-manager/plugin-permission.js | 30 +
.../services/plugin-manager/plugin-sandbox.js | 119 +++
.../services/plugin-manager/plugin-setup.js | 89 ++
.../services/plugin-manager/plugin.js | 44 +
.../plugin-manager/sandbox/alerts-sandbox.js | 10 +
.../plugin-manager/sandbox/audio-sandbox.js | 5 +
.../plugin-manager/sandbox/events-sandbox.js | 5 +
.../sandbox/font-awesome-sandbox.js | 7 +
.../{http.js => sandbox/http-sandbox.js} | 8 +-
.../sandbox/messaging-sandbox.js | 37 +
.../sandbox/peer-current-sandbox.js | 15 +
.../sandbox/profile-all-sandbox.js | 9 +
.../sandbox/profile-current-sandbox.js | 15 +
.../plugin-manager/sandbox/route-sandbox.js | 17 +
.../plugin-manager/sandbox/storage-sandbox.js | 30 +
.../plugin-manager/sandbox/timers-sandbox.js | 66 ++
.../websocket-sandbox.js} | 8 +-
.../plugin-manager/setup/avatars-setup.js | 31 +
.../plugin-manager/setup/components-setup.js | 41 +
.../setup/font-awesome-setup.js | 7 +
.../plugin-manager/setup/menu-items-setup.js | 30 +
.../plugin-manager/setup/register-setup.js | 7 +
.../plugin-manager/setup/routes-setup.js | 32 +
.../plugin-manager/setup/themes-setup.js | 38 +
.../setup/ui-components-setup.js | 26 +
.../plugin-manager/setup/wallet-tabs-setup.js | 26 +
.../plugin-manager/setup/webframe-setup.js | 7 +
.../plugin-manager/utils/get-all-routes.js | 4 +
.../plugin-manager/utils/normalize-json.js | 3 +
.../utils/validate-plugin-path.js | 16 +
.../plugin-manager/wallet-components.js | 30 -
src/renderer/store/modules/plugin.js | 6 +-
70 files changed, 2827 insertions(+), 965 deletions(-)
create mode 100644 __tests__/unit/__mocks__/@/services/plugin-manager/component/compile-template.js
create mode 100644 __tests__/unit/services/plugin-manager.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/component/create-component.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/component/get-context.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/component/validate.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/plugin-component-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/alerts-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/audio-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/events-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/font-awesome-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/http-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/messaging-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/peer-current-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/profile-all-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/profile-current-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/route-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/storage-sandbox.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/sandbox/timers-sandbox.spec.js
rename __tests__/unit/services/plugin-manager/{websocket.spec.js => sandbox/websocket-sandbox.spec.js} (67%)
create mode 100644 __tests__/unit/services/plugin-manager/setup/avatars-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/components-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/menu-items-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/register-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/routes-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/themes-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/ui-components-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/wallet-tabs-setup.spec.js
create mode 100644 __tests__/unit/services/plugin-manager/setup/webframe-setup.spec.js
delete mode 100644 __tests__/unit/services/plugin-manager/timers.spec.js
create mode 100644 src/renderer/services/plugin-manager/component/compile-template.js
create mode 100644 src/renderer/services/plugin-manager/component/create-component.js
create mode 100644 src/renderer/services/plugin-manager/component/get-context.js
create mode 100644 src/renderer/services/plugin-manager/component/hooks.js
create mode 100644 src/renderer/services/plugin-manager/component/validate.js
delete mode 100644 src/renderer/services/plugin-manager/font-awesome-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/plugin-component-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/plugin-configuration.js
create mode 100644 src/renderer/services/plugin-manager/plugin-permission.js
create mode 100644 src/renderer/services/plugin-manager/plugin-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/plugin-setup.js
create mode 100644 src/renderer/services/plugin-manager/plugin.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/alerts-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/audio-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/events-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/font-awesome-sandbox.js
rename src/renderer/services/plugin-manager/{http.js => sandbox/http-sandbox.js} (80%)
create mode 100644 src/renderer/services/plugin-manager/sandbox/messaging-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/peer-current-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/profile-all-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/profile-current-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/route-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/storage-sandbox.js
create mode 100644 src/renderer/services/plugin-manager/sandbox/timers-sandbox.js
rename src/renderer/services/plugin-manager/{websocket.js => sandbox/websocket-sandbox.js} (91%)
create mode 100644 src/renderer/services/plugin-manager/setup/avatars-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/components-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/font-awesome-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/menu-items-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/register-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/routes-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/themes-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/ui-components-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/wallet-tabs-setup.js
create mode 100644 src/renderer/services/plugin-manager/setup/webframe-setup.js
create mode 100644 src/renderer/services/plugin-manager/utils/get-all-routes.js
create mode 100644 src/renderer/services/plugin-manager/utils/normalize-json.js
create mode 100644 src/renderer/services/plugin-manager/utils/validate-plugin-path.js
delete mode 100644 src/renderer/services/plugin-manager/wallet-components.js
diff --git a/__tests__/unit/__mocks__/@/services/plugin-manager/component/compile-template.js b/__tests__/unit/__mocks__/@/services/plugin-manager/component/compile-template.js
new file mode 100644
index 0000000000..a633c9c0d4
--- /dev/null
+++ b/__tests__/unit/__mocks__/@/services/plugin-manager/component/compile-template.js
@@ -0,0 +1,6 @@
+export default jest.fn().mockImplementation(() => ({
+ compileTemplate: jest.fn((vm, template) => {
+ const { compileToFunctions } = require('vue-template-compiler')
+ return compileToFunctions(template)
+ })
+}))
diff --git a/__tests__/unit/services/plugin-manager.spec.js b/__tests__/unit/services/plugin-manager.spec.js
new file mode 100644
index 0000000000..c70f90c437
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager.spec.js
@@ -0,0 +1,194 @@
+import * as fs from 'fs'
+import * as fsExtra from 'fs-extra'
+import { createLocalVue } from '@vue/test-utils'
+import { PluginManager } from '@/services/plugin-manager'
+import { PluginSandbox } from '@/services/plugin-manager/plugin-sandbox'
+import { PluginSetup } from '@/services/plugin-manager/plugin-setup'
+
+jest.mock('@/services/plugin-manager/plugin-sandbox.js')
+jest.mock('@/services/plugin-manager/plugin-setup.js')
+
+jest.mock('fs-extra', () => ({
+ ensureDirSync: jest.fn(),
+ readdirSync: jest.fn(() => []),
+ lstatSync: jest.fn(() => ({
+ isDirectory: jest.fn(() => true)
+ }))
+}))
+
+jest.mock('@/services/plugin-manager/utils/validate-plugin-path.js', () => ({
+ validatePluginPath: jest.fn()
+}))
+
+const mockDispatch = jest.fn()
+const mockSandboxInstall = jest.fn()
+const mockSandboxSetup = jest.fn()
+
+PluginSandbox.mockImplementation(() => ({
+ install: mockSandboxInstall
+}))
+
+PluginSetup.mockImplementation(() => ({
+ install: mockSandboxSetup
+}))
+
+const localVue = createLocalVue()
+
+const pkg = {
+ name: 'plugin-test',
+ description: 'Test',
+ title: 'Plugin Test',
+ version: '0.0.1'
+}
+
+const app = {
+ $store: {
+ dispatch: mockDispatch,
+ getters: {
+ 'plugin/isEnabled': jest.fn((pluginId) => pluginId === 'plugin-test'),
+ 'profile/byId': jest.fn(() => {})
+ }
+ }
+}
+
+let pluginManager
+
+beforeEach(() => {
+ mockDispatch.mockReset()
+ pluginManager = new PluginManager()
+ pluginManager.setVue(localVue)
+})
+
+describe('Plugin Manager', () => {
+ it('should load plugins on init', async () => {
+ await pluginManager.init(app)
+ expect(app.$store.dispatch).toHaveBeenNthCalledWith(1, 'plugin/init')
+ expect(app.$store.dispatch).toHaveBeenNthCalledWith(2, 'plugin/loadPluginsForProfiles')
+ })
+
+ describe('Fetch plugins', () => {
+ it('should read plugins from path', async () => {
+ jest.spyOn(fsExtra, 'readdirSync').mockReturnValue(['plugin-1'])
+ jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(pkg))
+
+ await pluginManager.init(app)
+
+ expect(app.$store.dispatch).toHaveBeenCalledWith('plugin/setAvailable',
+ expect.objectContaining({
+ config: expect.any(Object),
+ fullPath: expect.any(String)
+ })
+ )
+ })
+ })
+
+ describe('Enable plugin', () => {
+ it('should throw not initiated error', async () => {
+ expect.assertions(1)
+ try {
+ await pluginManager.enablePlugin('plugin-1', 'p-1')
+ } catch (e) {
+ expect(e.message).toBe('Plugin Manager not initiated')
+ }
+ })
+
+ it('should throw not found error', async () => {
+ expect.assertions(1)
+ await pluginManager.init(app)
+ try {
+ await pluginManager.enablePlugin('plugin-not-loaded', 'p-1')
+ } catch (e) {
+ expect(e.message).toBe('Plugin not found')
+ }
+ })
+
+ it('should throw not enabled error', async () => {
+ expect.assertions(2)
+ await pluginManager.init(app)
+ pluginManager.plugins = {
+ 'plugin-not-enabled': {
+ config: {
+ id: '1'
+ }
+ }
+ }
+ try {
+ await pluginManager.enablePlugin('plugin-not-enabled', 'p-1')
+ } catch (e) {
+ expect(e.message).toBe('Plugin is not enabled')
+ expect(app.$store.getters['plugin/isEnabled']).toHaveBeenCalled()
+ }
+ })
+
+ it('should enable', async () => {
+ await pluginManager.init(app)
+
+ pluginManager.plugins = {
+ [pkg.name]: {
+ config: {
+ id: pkg.name
+ },
+ fullPath: './test'
+ }
+ }
+
+ await pluginManager.enablePlugin(pkg.name, 'p-1')
+ expect(mockDispatch).toHaveBeenCalledWith('plugin/setLoaded', expect.any(Object))
+ expect(mockSandboxInstall).toHaveBeenCalled()
+ expect(mockSandboxSetup).toHaveBeenCalled()
+ })
+ })
+
+ describe('Disable plugin', () => {
+ it('should throw not initiated error', async () => {
+ expect.assertions(1)
+ try {
+ await pluginManager.disablePlugin('plugin-1', 'p-1')
+ } catch (e) {
+ expect(e.message).toBe('Plugin Manager not initiated')
+ }
+ })
+
+ it('should throw not found error', async () => {
+ expect.assertions(1)
+ await pluginManager.init(app)
+ try {
+ await pluginManager.disablePlugin('plugin-not-loaded', 'p-1')
+ } catch (e) {
+ expect(e.message).toBe('Plugin `plugin-not-loaded` not found')
+ }
+ })
+
+ it('should disable', async () => {
+ await pluginManager.init(app)
+ pluginManager.plugins = {
+ [pkg.name]: {
+ config: {
+ id: pkg.name,
+ permissions: []
+ }
+ }
+ }
+
+ await pluginManager.disablePlugin(pkg.name, 'p-1')
+ expect(mockDispatch).toHaveBeenCalledWith('plugin/deleteLoaded', pkg.name)
+ })
+
+ it('should unload theme', async () => {
+ await pluginManager.init(app)
+ pluginManager.plugins = {
+ [pkg.name]: {
+ config: {
+ id: pkg.name,
+ permissions: ['THEMES']
+ }
+ }
+ }
+
+ await pluginManager.disablePlugin(pkg.name, 'p-1')
+ expect(mockDispatch).toHaveBeenCalledWith('plugin/deleteLoaded', pkg.name)
+ expect(mockDispatch).toHaveBeenCalledWith('session/setTheme', expect.any(String))
+ expect(mockDispatch).toHaveBeenCalledWith('profile/update', expect.any(Object))
+ })
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/component/create-component.spec.js b/__tests__/unit/services/plugin-manager/component/create-component.spec.js
new file mode 100644
index 0000000000..25a3d25892
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/component/create-component.spec.js
@@ -0,0 +1,290 @@
+import { createLocalVue, mount } from '@vue/test-utils'
+import { createSafeComponent } from '@/services/plugin-manager/component/create-component'
+
+const localVue = createLocalVue()
+
+describe('Create Component', () => {
+ it('should return a valid component', () => {
+ const plugin = {
+ template: '
Test
'
+ }
+
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.isVueInstance()).toBe(true)
+ })
+
+ describe('Props', () => {
+ it('should mount with props', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ props: {
+ name: {
+ type: String
+ }
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component, {
+ propsData: {
+ name: 'Test'
+ }
+ })
+ expect(wrapper.html()).toBe('Test
')
+ })
+
+ it('should sync changes', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ props: {
+ name: {
+ type: String
+ }
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component, {
+ propsData: {
+ name: 'Test'
+ }
+ })
+ expect(wrapper.html()).toBe('Test
')
+ wrapper.setProps({ name: 'Jest' })
+ expect(wrapper.html()).toBe('Jest
')
+ })
+ })
+
+ describe('Data', () => {
+ it('should mount with data', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: 'Test'
+ })
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.html()).toBe('Test
')
+ })
+
+ it('should not access the parent element', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: this && this.$parent && this.$parent._uid
+ })
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.vm.name).toBeUndefined()
+ })
+ })
+
+ describe('Computed', () => {
+ it('should mount with computed data', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ computed: {
+ name () {
+ return 'Test'
+ }
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.html()).toBe('Test
')
+ })
+
+ it('should not access parent element', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ computed: {
+ name () {
+ return this.$parent
+ }
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.vm.name).toBeUndefined()
+ })
+ })
+
+ describe('Methods', () => {
+ it('should mount with methods', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: 'Test'
+ }),
+ methods: {
+ change () {
+ this.name = 'Jest'
+ }
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ wrapper.vm.change()
+ expect(wrapper.vm.name).toBe('Jest')
+ })
+
+ it('should call methods from elements', () => {
+ const plugin = {
+ template: '',
+ data: () => ({
+ name: 'Test'
+ }),
+ methods: {
+ change () {
+ this.name = 'Jest'
+ }
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ const btn = wrapper.find({ ref: 'btn' })
+ btn.trigger('click')
+ expect(wrapper.vm.name).toBe('Jest')
+ })
+
+ it('should call methods from elements with params', () => {
+ const plugin = {
+ template: '',
+ data: () => ({
+ name: 'Test',
+ customName: 'Jest'
+ }),
+ methods: {
+ change (name) {
+ this.name = name
+ }
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ const btn = wrapper.find({ ref: 'btn' })
+ btn.trigger('click')
+ expect(wrapper.vm.name).toBe('Jest')
+ })
+ })
+
+ describe('Created', () => {
+ it('should mount with created hook', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: 'Test'
+ }),
+ created () {
+ this.name = 'Jest'
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.vm.name).toBe('Jest')
+ })
+
+ it('should not access the parent element', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: 'Test',
+ uid: undefined
+ }),
+ created () {
+ this.uid = this.$parent && this.$parent._uid
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.vm.uid).toBeUndefined()
+ })
+ })
+
+ describe('Mounted', () => {
+ it('should mount with mounted hook', () => {
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: 'Test'
+ }),
+ mounted () {
+ this.name = 'Jest'
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.vm.name).toBe('Jest')
+ })
+
+ it('should access refs', (done) => {
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: 'Test',
+ id: undefined
+ }),
+ mounted () {
+ this.$nextTick(() => {
+ this.id = this.refs.test.id
+ })
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ localVue.nextTick(() => {
+ expect(wrapper.vm.id).toBe('t1')
+ done()
+ })
+ })
+
+ it('should not set custom properties', (done) => {
+ const spy = jest.spyOn(console, 'error').mockImplementation()
+
+ const plugin = {
+ template: '{{ name }}
',
+ data: () => ({
+ name: 'Test'
+ }),
+ mounted () {
+ this.$nextTick(() => {
+ this.refs.test.innerHTML = 'Jest'
+ this.refs.test.outerHTML = 'Jest'
+ this.refs.test.appendChild(document.createElement('p'))
+ this.refs.test.cloneNode()
+ this.refs.test.getRootNode()
+ this.refs.test.insertBefore()
+ this.refs.test.normalize()
+ this.refs.test.querySelector()
+ this.refs.test.querySelectorAll()
+ this.refs.test.removeChild()
+ this.refs.test.replaceChild()
+ })
+ }
+ }
+ const component = wrapperPlugin(plugin)
+ const wrapper = mount(component)
+ expect(wrapper.html()).toBe('Test
')
+
+ localVue.nextTick(() => {
+ expect(spy).toHaveBeenCalledWith('innerHTML 🚫')
+ expect(spy).toHaveBeenCalledWith('outerHTML 🚫')
+ expect(spy).toHaveBeenCalledWith('appendChild 🚫')
+ expect(spy).toHaveBeenCalledWith('cloneNode 🚫')
+ expect(spy).toHaveBeenCalledWith('getRootNode 🚫')
+ expect(spy).toHaveBeenCalledWith('insertBefore 🚫')
+ expect(spy).toHaveBeenCalledWith('normalize 🚫')
+ expect(spy).toHaveBeenCalledWith('querySelector 🚫')
+ expect(spy).toHaveBeenCalledWith('querySelectorAll 🚫')
+ expect(spy).toHaveBeenCalledWith('removeChild 🚫')
+ expect(spy).toHaveBeenCalledWith('replaceChild 🚫')
+ done()
+ })
+ })
+ })
+})
+
+const wrapperPlugin = (plugin) => {
+ return createSafeComponent('test', plugin, localVue)
+}
diff --git a/__tests__/unit/services/plugin-manager/component/get-context.spec.js b/__tests__/unit/services/plugin-manager/component/get-context.spec.js
new file mode 100644
index 0000000000..a4c9bed153
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/component/get-context.spec.js
@@ -0,0 +1,71 @@
+import { createLocalVue, mount } from '@vue/test-utils'
+import { getSafeContext } from '@/services/plugin-manager/component/get-context'
+
+let localVue
+
+beforeEach(() => {
+ localVue = createLocalVue()
+})
+
+describe('Prepare Component Context', () => {
+ describe('Render', () => {
+ it('should render', () => {
+ const plugin = {
+ render (h) {
+ return h('div', 'Test')
+ }
+ }
+
+ const wrapper = mount(createSafeRender(plugin))
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.html()).toBe('Test
')
+ })
+
+ it('should render with computed properties', () => {
+ const plugin = {
+ render (h) {
+ return h('div', this.name)
+ },
+ computed: {
+ name () {
+ return 'Test'
+ }
+ }
+ }
+
+ const wrapper = mount(createSafeRender(plugin))
+ expect(wrapper.html()).toBe('Test
')
+ })
+
+ it('should not access the parent element', () => {
+ const plugin = {
+ render (h) {
+ return h('div', this.$parent && this.$parent._uid)
+ }
+ }
+
+ const wrapper = mount(createSafeRender(plugin))
+ expect(wrapper.find('div').text()).toBe('')
+ })
+
+ it('should not access the root element', () => {
+ const plugin = {
+ render (h) {
+ return h('div', this.$root && this.$root._uid)
+ }
+ }
+
+ const wrapper = mount(createSafeRender(plugin))
+ expect(wrapper.find('div').text()).toBe('')
+ })
+ })
+})
+
+const createSafeRender = (plugin) => {
+ return localVue.extend({
+ ...plugin,
+ render: function () {
+ return plugin.render.apply(getSafeContext(this, plugin), [...arguments])
+ }
+ })
+}
diff --git a/__tests__/unit/services/plugin-manager/component/validate.spec.js b/__tests__/unit/services/plugin-manager/component/validate.spec.js
new file mode 100644
index 0000000000..3ad5cbb697
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/component/validate.spec.js
@@ -0,0 +1,69 @@
+import { validateComponent } from '@/services/plugin-manager/component/validate'
+
+describe('Validate component', () => {
+ const plugin = {
+ config: {
+ id: 1
+ }
+ }
+
+ it('should have template field', () => {
+ expect(validateComponent({ plugin, component: {} })).toBe(false)
+ expect(validateComponent({ plugin, component: { template: '' } })).toBe(true)
+ })
+
+ it('should not have unauthorized fields', () => {
+ const component = {
+ render: () => {}
+ }
+ expect(validateComponent({ plugin, component })).toBe(false)
+ })
+
+ it('should not have v-html', () => {
+ const component = {
+ template: ''
+ }
+ expect(validateComponent({ plugin, component })).toBe(false)
+ })
+
+ it('should not have javascript inline script', () => {
+ const component = {
+ template: ''
+ }
+ expect(validateComponent({ plugin, component })).toBe(false)
+ })
+
+ it('should not have iframe tag', () => {
+ const component = {
+ template: ''
+ }
+ expect(validateComponent({ plugin, component })).toBe(false)
+ })
+
+ it('should not have webview tag', () => {
+ const component = {
+ template: ''
+ }
+ expect(validateComponent({ plugin, component })).toBe(false)
+ })
+
+ it('should not have script tag', () => {
+ const component = {
+ template: ''
+ }
+ expect(validateComponent({ plugin, component })).toBe(false)
+ })
+
+ it('should not have eval', () => {
+ const component = {
+ template: ''
+ }
+ expect(validateComponent({ plugin, component })).toBe(false)
+ })
+
+ it('should not have inline events', () => {
+ expect(validateComponent({ plugin, component: { template: '' } })).toBe(false)
+ expect(validateComponent({ plugin, component: { template: '' } })).toBe(false)
+ expect(validateComponent({ plugin, component: { template: '' } })).toBe(false)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/plugin-component-sandbox.spec.js b/__tests__/unit/services/plugin-manager/plugin-component-sandbox.spec.js
new file mode 100644
index 0000000000..5ad2f5cf97
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/plugin-component-sandbox.spec.js
@@ -0,0 +1,118 @@
+import { createLocalVue, mount } from '@vue/test-utils'
+import { PluginComponentSandbox } from '@/services/plugin-manager/plugin-component-sandbox'
+
+jest.mock('@/services/plugin-manager/component/compile-template.js', () => ({
+ compileTemplate: jest.fn((vm, template) => {
+ const { compileToFunctions } = require('vue-template-compiler')
+ return compileToFunctions(template)
+ })
+}))
+
+const vue = createLocalVue()
+
+const plugin = {
+ config: {
+ id: 1
+ }
+}
+
+const vm = {
+ run: jest.fn(code => JSON.parse(code))
+}
+
+const options = {
+ vue,
+ plugin,
+ componentVM: vm,
+ pluginVM: vm,
+ name: 'test'
+}
+
+describe('Plugin Component Sandbox', () => {
+ it('should return a valid component', () => {
+ const sandbox = new PluginComponentSandbox({
+ ...options,
+ fullPath: './',
+ source: {
+ template: 'Test
'
+ }
+ })
+
+ const component = sandbox.render()
+ const wrapper = mount(component)
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.html()).toBe('Test
')
+ })
+
+ it('should render from buffer', () => {
+ const sandbox = new PluginComponentSandbox({
+ ...options,
+ fullPath: './',
+ source: Buffer.from(JSON.stringify({
+ template: 'Test
'
+ }))
+ })
+ const component = sandbox.render()
+ const wrapper = mount(component)
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.html()).toBe('Test
')
+ expect(vm.run).toHaveBeenCalled()
+ })
+
+ it('should render with global components', () => {
+ plugin.globalComponents = {
+ Test: {
+ template: 'Global
'
+ }
+ }
+ const sandbox = new PluginComponentSandbox({
+ ...options,
+ fullPath: './',
+ source: {
+ template: ''
+ }
+ })
+ const component = sandbox.render()
+ const wrapper = mount(component)
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.html()).toBe('Global
')
+ })
+
+ it('should parse child components', (done) => {
+ const spy = jest.spyOn(console, 'error').mockImplementation()
+
+ const sandbox = new PluginComponentSandbox({
+ ...options,
+ fullPath: './',
+ source: {
+ template: '',
+ components: {
+ Test: {
+ template: '',
+ data: () => ({
+ count: 0
+ }),
+ methods: {
+ increment () {
+ this.count++
+ }
+ },
+ mounted () {
+ this.$nextTick(() => {
+ this.refs.btn.innerHTML = ''
+ })
+ }
+ }
+ }
+ }
+ })
+
+ const component = sandbox.render()
+ mount(component)
+
+ vue.nextTick(() => {
+ expect(spy).toHaveBeenCalledWith('innerHTML 🚫')
+ done()
+ })
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/alerts-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/alerts-sandbox.spec.js
new file mode 100644
index 0000000000..7a7f0adf6c
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/alerts-sandbox.spec.js
@@ -0,0 +1,21 @@
+import { createAlertsSandbox } from '@/services/plugin-manager/sandbox/alerts-sandbox'
+
+const walletApi = {}
+const app = {
+ $error: jest.fn(),
+ $info: jest.fn(),
+ $success: jest.fn(),
+ $warn: jest.fn()
+}
+const alertsSandbox = createAlertsSandbox(walletApi, app)
+alertsSandbox()
+
+describe('Alerts Sandbox', () => {
+ it('should expose functions', () => {
+ expect(walletApi.alert).toBeTruthy()
+ expect(walletApi.alert.error).toBeTruthy()
+ expect(walletApi.alert.info).toBeTruthy()
+ expect(walletApi.alert.success).toBeTruthy()
+ expect(walletApi.alert.warn).toBeTruthy()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/audio-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/audio-sandbox.spec.js
new file mode 100644
index 0000000000..affe359c82
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/audio-sandbox.spec.js
@@ -0,0 +1,13 @@
+import { createAudioSandbox } from '@/services/plugin-manager/sandbox/audio-sandbox'
+
+global.AudioContext = {}
+
+const api = {}
+const audioSandbox = createAudioSandbox(api)
+audioSandbox()
+
+describe('Audio Sandbox', () => {
+ it('should expose the AudioContext object', () => {
+ expect(api.AudioContext).toBeTruthy()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/events-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/events-sandbox.spec.js
new file mode 100644
index 0000000000..c425c546ac
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/events-sandbox.spec.js
@@ -0,0 +1,14 @@
+import { createEventsSandbox } from '@/services/plugin-manager/sandbox/events-sandbox'
+
+const walletApi = {}
+const app = {
+ $eventBus: jest.fn()
+}
+const eventsSandbox = createEventsSandbox(walletApi, app)
+eventsSandbox()
+
+describe('Events Sandbox', () => {
+ it('should expose functions', () => {
+ expect(walletApi.eventBus).toBeTruthy()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/font-awesome-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/font-awesome-sandbox.spec.js
new file mode 100644
index 0000000000..c05376ad24
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/font-awesome-sandbox.spec.js
@@ -0,0 +1,11 @@
+import { createFontAwesomeSandbox } from '@/services/plugin-manager/sandbox/font-awesome-sandbox'
+
+const walletApi = {}
+const fontAwesomeSandbox = createFontAwesomeSandbox(walletApi)
+fontAwesomeSandbox()
+
+describe('Font Awesome Sandbox', () => {
+ it('should expose functions', () => {
+ expect(walletApi.fontAwesomeIcons).toBeTruthy()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/http-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/http-sandbox.spec.js
new file mode 100644
index 0000000000..a989c2d068
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/http-sandbox.spec.js
@@ -0,0 +1,52 @@
+import { createHttpSandbox } from '@/services/plugin-manager/sandbox/http-sandbox'
+import got from 'got'
+
+jest.mock('got')
+
+const plugin = {
+ config: {
+ urls: ['ark.io']
+ }
+}
+
+let walletApi
+let httpSandbox
+
+describe('Http Sandbox', () => {
+ beforeEach(() => {
+ walletApi = {}
+ httpSandbox = createHttpSandbox(walletApi, plugin)
+ httpSandbox()
+ })
+
+ it('should expose functions', () => {
+ expect(walletApi.http).toBeTruthy()
+ })
+
+ it('should fail when requesting an unauthorized url', () => {
+ expect(() => walletApi.http.get('google.com'))
+ .toThrow('URL "google.com" not allowed')
+ })
+
+ it('should get an authrorized url', () => {
+ walletApi.http.get('ark.io')
+ expect(got.get).toHaveBeenCalledWith('ark.io', undefined)
+ })
+
+ it('should get an authrorized url with options', () => {
+ const options = { agent: 'jest' }
+ walletApi.http.get('ark.io', options)
+ expect(got.get).toHaveBeenCalledWith('ark.io', options)
+ })
+
+ it('should post to an authrorized url', () => {
+ walletApi.http.post('ark.io')
+ expect(got.post).toHaveBeenCalledWith('ark.io', undefined)
+ })
+
+ it('should post to an authrorized url with options', () => {
+ const options = { version: 2 }
+ walletApi.http.post('ark.io', options)
+ expect(got.post).toHaveBeenCalledWith('ark.io', options)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/messaging-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/messaging-sandbox.spec.js
new file mode 100644
index 0000000000..c6e31741b0
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/messaging-sandbox.spec.js
@@ -0,0 +1,40 @@
+import { createMessagingSandbox } from '@/services/plugin-manager/sandbox/messaging-sandbox'
+
+let walletApi
+let app
+
+beforeEach(() => {
+ walletApi = {}
+ app = {
+ $router: {
+ beforeEach: jest.fn()
+ }
+ }
+
+ const messagingSandbox = createMessagingSandbox(walletApi, app)
+ messagingSandbox()
+})
+
+describe('Messaging Sandbox', () => {
+ it('should expose functions', () => {
+ expect(walletApi.messages).toBeTruthy()
+ expect(walletApi.messages.on).toBeTruthy()
+ expect(walletApi.messages.clear).toBeTruthy()
+ })
+
+ it('should define the router hook', () => {
+ expect(app.$router.beforeEach).toHaveBeenCalled()
+ })
+
+ it('should register listeners', () => {
+ walletApi.messages.on('transaction', () => 'test')
+ expect(Object.keys(walletApi.messages.events)).toHaveLength(1)
+ })
+
+ it('should clear listeners', () => {
+ walletApi.messages.on('transaction', () => 'test')
+ expect(Object.keys(walletApi.messages.events)).toHaveLength(1)
+ walletApi.messages.clear()
+ expect(Object.keys(walletApi.messages.events)).toHaveLength(0)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/peer-current-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/peer-current-sandbox.spec.js
new file mode 100644
index 0000000000..c1488ddfab
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/peer-current-sandbox.spec.js
@@ -0,0 +1,15 @@
+import { createPeerCurrentSandbox } from '@/services/plugin-manager/sandbox/peer-current-sandbox'
+
+const walletApi = {}
+const app = {}
+const peerCurrentSandbox = createPeerCurrentSandbox(walletApi, app)
+peerCurrentSandbox()
+
+describe('Peer Current Sandbox', () => {
+ it('should expose functions', () => {
+ expect(walletApi.peers).toBeTruthy()
+ expect(walletApi.peers.current).toBeTruthy()
+ expect(walletApi.peers.current.get).toBeTruthy()
+ expect(walletApi.peers.current.post).toBeTruthy()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/profile-all-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/profile-all-sandbox.spec.js
new file mode 100644
index 0000000000..b26ce01384
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/profile-all-sandbox.spec.js
@@ -0,0 +1,32 @@
+import { createProfileAllSandbox } from '@/services/plugin-manager/sandbox/profile-all-sandbox'
+
+const mockGetter = jest.fn(() => [
+ {
+ id: 1
+ }
+])
+
+let walletApi
+let app
+let profileAllSandbox
+
+describe('Profile All Sandbox', () => {
+ beforeEach(() => {
+ walletApi = {}
+ app = {
+ $store: {
+ getters: {
+ 'profile/public': mockGetter
+ }
+ }
+ }
+ profileAllSandbox = createProfileAllSandbox(walletApi, app)
+ profileAllSandbox()
+ })
+
+ it('should expose functions', () => {
+ expect(mockGetter).toHaveBeenCalled()
+ expect(walletApi.profiles.all).toBeTruthy()
+ expect(walletApi.profiles.all).toHaveLength(1)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/profile-current-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/profile-current-sandbox.spec.js
new file mode 100644
index 0000000000..2b5ab39a8b
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/profile-current-sandbox.spec.js
@@ -0,0 +1,29 @@
+import { createProfileCurrentSandbox } from '@/services/plugin-manager/sandbox/profile-current-sandbox'
+
+const mockGetter = jest.fn(() => ({
+ id: 1
+}))
+
+let walletApi
+let app
+let profileCurrentSandbox
+
+describe('Profile Current Sandbox', () => {
+ beforeEach(() => {
+ walletApi = {}
+ app = {
+ $store: {
+ getters: {
+ 'profile/public': mockGetter
+ }
+ }
+ }
+ profileCurrentSandbox = createProfileCurrentSandbox(walletApi, app)
+ profileCurrentSandbox()
+ })
+
+ it('should expose functions', () => {
+ expect(walletApi.profiles).toBeTruthy()
+ expect(walletApi.profiles.getCurrent).toBeTruthy()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/route-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/route-sandbox.spec.js
new file mode 100644
index 0000000000..95f568f9ee
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/route-sandbox.spec.js
@@ -0,0 +1,13 @@
+import { createRouteSandbox } from '@/services/plugin-manager/sandbox/route-sandbox'
+
+const walletApi = {}
+const routeSandbox = createRouteSandbox(walletApi, {}, {})
+routeSandbox()
+
+describe('Route Sandbox', () => {
+ it('should expose functions', () => {
+ expect(walletApi.route).toBeTruthy()
+ expect(walletApi.route.get).toBeTruthy()
+ expect(walletApi.route.goTo).toBeTruthy()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/storage-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/storage-sandbox.spec.js
new file mode 100644
index 0000000000..b919009cce
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/storage-sandbox.spec.js
@@ -0,0 +1,60 @@
+import { createStorageSandbox } from '@/services/plugin-manager/sandbox/storage-sandbox'
+
+const plugin = {
+ config: {
+ id: 'profile1'
+ }
+}
+
+let walletApi
+let app
+
+beforeAll(() => {
+ walletApi = {}
+ const db = {}
+
+ app = {
+ $store: {
+ getters: {
+ 'plugin/pluginOptions': jest.fn(() => db)
+ },
+ dispatch: jest.fn((_, data) => (db[data.key] = data.value))
+ }
+ }
+
+ const storageSandbox = createStorageSandbox(walletApi, app, plugin)
+ storageSandbox()
+})
+
+describe('Storage Sandbox', () => {
+ const options = {
+ key: 'test',
+ value: 1,
+ pluginId: plugin.config.id,
+ profileId: undefined
+ }
+
+ it('should expose functions', () => {
+ expect(walletApi.storage).toBeTruthy()
+ expect(walletApi.storage.getOptions).toBeTruthy()
+ expect(walletApi.storage.get).toBeTruthy()
+ expect(walletApi.storage.set).toBeTruthy()
+ })
+
+ it('should set a value to key', () => {
+ walletApi.storage.set(options.key, options.value)
+ expect(app.$store.dispatch).toHaveBeenCalledWith('plugin/setPluginOption', options)
+ })
+
+ it('should get the value from key', () => {
+ const result = walletApi.storage.get(options.key)
+ expect(app.$store.getters['plugin/pluginOptions']).toHaveBeenCalledWith(plugin.config.id, undefined)
+ expect(result).toBe(options.value)
+ })
+
+ it('should get all values', () => {
+ const result = walletApi.storage.getOptions()
+ expect(Object.keys(result)).toHaveLength(1)
+ expect(result).toHaveProperty(options.key, options.value)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/sandbox/timers-sandbox.spec.js b/__tests__/unit/services/plugin-manager/sandbox/timers-sandbox.spec.js
new file mode 100644
index 0000000000..0262dc895f
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/sandbox/timers-sandbox.spec.js
@@ -0,0 +1,100 @@
+import { createTimersSandbox } from '@/services/plugin-manager/sandbox/timers-sandbox'
+
+const routerNext = jest.fn()
+
+let walletApi
+let app
+let sandbox
+
+beforeEach(() => {
+ const routeCallbacks = []
+
+ walletApi = {}
+ app = {
+ $router: {
+ beforeEach: jest.fn(callback => {
+ routeCallbacks.push(callback)
+ }),
+
+ push () {
+ for (const callback of routeCallbacks) {
+ callback(null, null, routerNext)
+ }
+ }
+ }
+ }
+
+ sandbox = createTimersSandbox(walletApi, app)
+ sandbox()
+})
+
+describe('Timers Sandbox', () => {
+ it('should expose functions', () => {
+ expect(walletApi.timers.clearInterval).toBeTruthy()
+ expect(walletApi.timers.clearTimeout).toBeTruthy()
+ expect(walletApi.timers.setInterval).toBeTruthy()
+ expect(walletApi.timers.setTimeout).toBeTruthy()
+ })
+
+ it('should clear timer once setTimeout executed', (done) => {
+ walletApi.timers.setTimeout(() => {
+ setTimeout(() => {
+ expect(walletApi.timers.timeouts.length).toEqual(0)
+
+ done()
+ }, 100)
+ }, 1000)
+ expect(walletApi.timers.timeouts.length).toEqual(1)
+ })
+
+ it('should clear timer if clearTimeout called', (done) => {
+ expect(walletApi.timers.timeouts.length).toEqual(0)
+ const id = walletApi.timers.setTimeout(() => {
+ throw new Error('This setTimeout call should never execute')
+ }, 1000)
+
+ walletApi.timers.setTimeout(() => {
+ done()
+ }, 1000)
+
+ expect(walletApi.timers.timeouts.length).toEqual(2)
+ walletApi.timers.clearTimeout(id)
+ expect(walletApi.timers.timeouts.length).toEqual(1)
+ })
+
+ it('should clear timer if clearInterval called', (done) => {
+ expect(walletApi.timers.intervals.length).toEqual(0)
+ let counter = 0
+ const id = walletApi.timers.setInterval(() => {
+ counter++
+
+ if (counter === 5) {
+ walletApi.timers.clearInterval(id)
+ setTimeout(() => {
+ expect(walletApi.timers.intervals.length).toEqual(0)
+ expect(counter).toEqual(5)
+ done()
+ }, 500)
+ }
+ }, 100)
+ expect(walletApi.timers.intervals.length).toEqual(1)
+ })
+
+ it('should clear timers on route change', () => {
+ walletApi.timers.setTimeout(() => {
+ throw new Error('This setTimeout call will never execute')
+ }, 10000)
+
+ walletApi.timers.setInterval(() => {
+ throw new Error('This setInterval call will never execute')
+ }, 10000)
+
+ expect(walletApi.timers.timeouts.length).toEqual(1)
+ expect(walletApi.timers.intervals.length).toEqual(1)
+ expect(app.$router.beforeEach).toHaveBeenCalledTimes(1)
+ app.$router.push()
+ expect(routerNext).toHaveBeenCalledTimes(1)
+ expect(walletApi.timers.timeouts.length).toEqual(0)
+ expect(walletApi.timers.intervals.length).toEqual(0)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/websocket.spec.js b/__tests__/unit/services/plugin-manager/sandbox/websocket-sandbox.spec.js
similarity index 67%
rename from __tests__/unit/services/plugin-manager/websocket.spec.js
rename to __tests__/unit/services/plugin-manager/sandbox/websocket-sandbox.spec.js
index 5d0355489c..e68ec25ce3 100644
--- a/__tests__/unit/services/plugin-manager/websocket.spec.js
+++ b/__tests__/unit/services/plugin-manager/sandbox/websocket-sandbox.spec.js
@@ -1,5 +1,5 @@
import { Server } from 'mock-socket'
-import PluginWebsocket from '@/services/plugin-manager/websocket'
+import { createWebsocketSandbox } from '@/services/plugin-manager/sandbox/websocket-sandbox'
const whitelist = [
/* eslint-disable: no-useless-escape */
@@ -7,11 +7,20 @@ const whitelist = [
]
const host = 'ws://my.test.com:8080'
+const plugin = {
+ config: {
+ urls: whitelist
+ }
+}
+
+let app
+let sandbox
+let walletApi
+
let pongMock
let mockServer
-let router
let routerNext
-let pluginWebsocket
+
beforeEach(() => {
if (mockServer) {
mockServer.stop()
@@ -34,24 +43,34 @@ beforeEach(() => {
const routeCallbacks = []
routerNext = jest.fn()
- router = {
- beforeEach: jest.fn(callback => {
- routeCallbacks.push(callback)
- }),
-
- push () {
- for (const callback of routeCallbacks) {
- callback(null, null, routerNext)
+
+ walletApi = {}
+
+ app = {
+ $router: {
+ beforeEach: jest.fn(callback => {
+ routeCallbacks.push(callback)
+ }),
+
+ push () {
+ for (const callback of routeCallbacks) {
+ callback(null, null, routerNext)
+ }
}
}
}
- pluginWebsocket = new PluginWebsocket(whitelist, router)
+ sandbox = createWebsocketSandbox(walletApi, app, plugin)
+ sandbox()
})
describe('PluginWebsocket', () => {
+ it('should expose functions', () => {
+ expect(walletApi.websocket).toBeTruthy()
+ })
+
it('should connect to websocket', (done) => {
- const socket = pluginWebsocket.connect(host)
+ const socket = walletApi.websocket.connect(host)
expect(socket.isConnecting()).toBeTrue()
setTimeout(() => {
@@ -62,7 +81,7 @@ describe('PluginWebsocket', () => {
})
it('should connect to websocket and receive data', (done) => {
- const socket = pluginWebsocket.connect(host)
+ const socket = walletApi.websocket.connect(host)
socket.on('data', (event) => {
expect(event.data).toEqual('test')
@@ -73,7 +92,7 @@ describe('PluginWebsocket', () => {
})
it('should connect to websocket and send data', (done) => {
- const socket = pluginWebsocket.connect(host)
+ const socket = walletApi.websocket.connect(host)
socket.on('pong', (event) => {
expect(pongMock).toHaveBeenCalledWith('ping')
@@ -87,7 +106,7 @@ describe('PluginWebsocket', () => {
})
it('should close the websocket', (done) => {
- const socket = pluginWebsocket.connect(host)
+ const socket = walletApi.websocket.connect(host)
setTimeout(() => {
expect(socket.isOpen()).toBeTrue()
@@ -106,19 +125,19 @@ describe('PluginWebsocket', () => {
})
it('should reset websockets on route change', () => {
- const socket = pluginWebsocket.connect(host)
+ const socket = walletApi.websocket.connect(host)
socket.on('pong', jest.fn())
expect(socket.events.pong).toBeTruthy()
- expect(router.beforeEach).toHaveBeenCalledTimes(1)
- router.push()
+ expect(app.$router.beforeEach).toHaveBeenCalledTimes(1)
+ app.$router.push()
expect(routerNext).toHaveBeenCalledTimes(1)
expect(socket.events.length).toEqual(0)
})
it('should not connect to websocket', (done) => {
- const socket = pluginWebsocket.connect('ws://failure.test.com:8080')
+ const socket = walletApi.websocket.connect('ws://failure.test.com:8080')
setTimeout(() => {
expect(socket.isClosed()).toBeTrue()
@@ -129,14 +148,19 @@ describe('PluginWebsocket', () => {
it('should not connect to websocket due to whitelist', () => {
expect(() => {
- pluginWebsocket.connect('ws://my.test.com:8081')
+ walletApi.websocket.connect('ws://my.test.com:8081')
}).toThrow('URL "ws://my.test.com:8081" not allowed')
})
it('should ignore an invalid whitelist', () => {
expect(() => {
- pluginWebsocket = new PluginWebsocket('not a whitelist', router)
- pluginWebsocket.connect('ws://my.test.com:8081')
+ const api = {}
+ createWebsocketSandbox(api, app, {
+ config: {
+ url: 'not a whitelist'
+ }
+ })()
+ api.websocket.connect('ws://my.test.com:8081')
}).toThrow('URL "ws://my.test.com:8081" not allowed')
})
})
diff --git a/__tests__/unit/services/plugin-manager/setup/avatars-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/avatars-setup.spec.js
new file mode 100644
index 0000000000..f8f6a0956b
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/avatars-setup.spec.js
@@ -0,0 +1,44 @@
+import { createAvatarsSetup } from '@/services/plugin-manager/setup/avatars-setup'
+import { Plugin } from '@/services/plugin-manager/plugin'
+
+const plugin = new Plugin({
+ config: {
+ id: 1
+ }
+})
+
+plugin.components = {
+ man: {},
+ woman: {}
+}
+
+const pluginObject = {
+ getAvatars: jest.fn(() => ['man', 'woman'])
+}
+
+const sandbox = {
+ app: {
+ $store: {
+ dispatch: jest.fn()
+ }
+ }
+}
+
+const profileId = 'profile1'
+
+const avatarsSetup = createAvatarsSetup(plugin, pluginObject, sandbox, profileId)
+avatarsSetup()
+
+describe('Avatars Setup', () => {
+ it('should call the getAvatars method', () => {
+ expect(pluginObject.getAvatars).toHaveBeenCalled()
+ })
+
+ it('should populate the avatars field', () => {
+ expect(plugin.avatars.length).toBeGreaterThan(0)
+ })
+
+ it('should dispatch to vuex', () => {
+ expect(sandbox.app.$store.dispatch).toHaveBeenCalled()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/components-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/components-setup.spec.js
new file mode 100644
index 0000000000..943666f875
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/components-setup.spec.js
@@ -0,0 +1,54 @@
+import { createLocalVue, mount } from '@vue/test-utils'
+import { createComponentsSetup } from '@/services/plugin-manager/setup/components-setup'
+import { Plugin } from '@/services/plugin-manager/plugin'
+
+jest.mock('fs', () => ({
+ readFileSync: () => ({
+ template: 'Test
'
+ })
+}))
+
+jest.mock('@/services/plugin-manager/component/compile-template.js', () => ({
+ compileTemplate: jest.fn((vm, template) => {
+ const { compileToFunctions } = require('vue-template-compiler')
+ return compileToFunctions(template)
+ })
+}))
+
+const localVue = createLocalVue()
+
+const plugin = new Plugin({
+ fullPath: './',
+ config: {
+ id: 1
+ }
+})
+
+const pluginObject = {
+ getComponentPaths: jest.fn(() => ({
+ test: 'pages/index.js'
+ }))
+}
+
+const sandbox = {
+ getVM: jest.fn(() => ({
+ run: jest.fn()
+ })),
+ app: {}
+}
+
+const componentsSetup = createComponentsSetup(plugin, pluginObject, sandbox, localVue)
+componentsSetup()
+
+describe('Components Setup', () => {
+ it('should call the getComponentPaths method', () => {
+ expect(pluginObject.getComponentPaths).toHaveBeenCalled()
+ })
+
+ it('should populate the components field', () => {
+ const componentNames = Object.keys(plugin.components)
+ expect(componentNames.length).toBeGreaterThan(0)
+ const wrapper = mount(plugin.components[componentNames[0]])
+ expect(wrapper.isVueInstance()).toBe(true)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/menu-items-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/menu-items-setup.spec.js
new file mode 100644
index 0000000000..7497c73b8b
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/menu-items-setup.spec.js
@@ -0,0 +1,50 @@
+import { createMenuItemsSetup } from '@/services/plugin-manager/setup/menu-items-setup'
+import { Plugin } from '@/services/plugin-manager/plugin'
+
+const plugin = new Plugin({
+ config: {
+ id: 1
+ }
+})
+
+plugin.routes = [
+ {
+ name: 'test'
+ }
+]
+
+const pluginObject = {
+ getMenuItems: jest.fn(() => [
+ {
+ routeName: 'test'
+ }
+ ])
+}
+
+const sandbox = {
+ app: {
+ $store: {
+ dispatch: jest.fn()
+ },
+ $router: {
+ options: {
+ routes: []
+ }
+ }
+ }
+}
+
+const profileId = 'profile1'
+
+const menuItemsSetup = createMenuItemsSetup(plugin, pluginObject, sandbox, profileId)
+menuItemsSetup()
+
+describe('Menu Items Setup', () => {
+ it('should call the getMenuItems method', () => {
+ expect(pluginObject.getMenuItems).toHaveBeenCalled()
+ })
+
+ it('should dispatch to vuex', () => {
+ expect(sandbox.app.$store.dispatch).toHaveBeenCalled()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/register-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/register-setup.spec.js
new file mode 100644
index 0000000000..3f4ee15542
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/register-setup.spec.js
@@ -0,0 +1,14 @@
+import { createRegisterSetup } from '@/services/plugin-manager/setup/register-setup'
+
+const pluginObject = {
+ register: jest.fn()
+}
+
+const registerSetup = createRegisterSetup(pluginObject)
+registerSetup()
+
+describe('Register Setup', () => {
+ it('should call the register method', () => {
+ expect(pluginObject.register).toHaveBeenCalled()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/routes-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/routes-setup.spec.js
new file mode 100644
index 0000000000..f0580e02ba
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/routes-setup.spec.js
@@ -0,0 +1,70 @@
+import { createRoutesSetup } from '@/services/plugin-manager/setup/routes-setup'
+import { Plugin } from '@/services/plugin-manager/plugin'
+
+const pluginObject = {
+ getRoutes: jest.fn(() => [
+ {
+ name: 'test',
+ component: 'test'
+ }
+ ])
+}
+
+let plugin
+let sandbox
+let routesSetup
+
+beforeEach(() => {
+ plugin = new Plugin({
+ config: {
+ id: 1
+ }
+ })
+
+ plugin.components = {
+ test: {}
+ }
+
+ sandbox = {
+ app: {
+ $router: {
+ options: {
+ routes: []
+ },
+ addRoutes: jest.fn()
+ }
+ }
+ }
+
+ routesSetup = createRoutesSetup(plugin, pluginObject, sandbox)
+})
+
+describe('Routes Items Setup', () => {
+ it('should call the getRoutes method', () => {
+ routesSetup()
+ expect(pluginObject.getRoutes).toHaveBeenCalled()
+ })
+
+ it('should populate the plugin field', () => {
+ routesSetup()
+ expect(plugin.routes).toHaveLength(1)
+ })
+
+ it('should populate app routes', () => {
+ routesSetup()
+ expect(sandbox.app.$router.addRoutes).toHaveBeenCalled()
+ })
+
+ it('should not override app routes', () => {
+ const customSandbox = { ...sandbox }
+ customSandbox.app.$router.options.routes.push({
+ name: 'test'
+ })
+
+ const customSetup = createRoutesSetup(plugin, pluginObject, sandbox)
+ customSetup()
+
+ expect(plugin.routes.length).toBe(0)
+ expect(sandbox.app.$router.addRoutes).toHaveBeenCalledTimes(0)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/themes-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/themes-setup.spec.js
new file mode 100644
index 0000000000..b8d17ed1b9
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/themes-setup.spec.js
@@ -0,0 +1,44 @@
+import { createThemesSetup } from '@/services/plugin-manager/setup/themes-setup'
+
+jest.mock('fs', () => ({
+ existsSync: jest.fn(() => true)
+}))
+
+const pluginObject = {
+ getThemes: jest.fn(() => ({
+ test: {
+ cssPath: 'styles/themes.css',
+ darkMode: true
+ }
+ }))
+}
+
+const sandbox = {
+ app: {
+ $store: {
+ dispatch: jest.fn()
+ }
+ }
+}
+
+const plugin = {
+ fullPath: __dirname,
+ config: {
+ id: 1
+ }
+}
+
+const profileId = 'profile1'
+
+const themesSetup = createThemesSetup(plugin, pluginObject, sandbox, profileId)
+themesSetup()
+
+describe('Themes Setup', () => {
+ it('should call the getThemes method', () => {
+ expect(pluginObject.getThemes).toHaveBeenCalled()
+ })
+
+ it('should dispatch to vuex', () => {
+ expect(sandbox.app.$store.dispatch).toHaveBeenCalled()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/ui-components-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/ui-components-setup.spec.js
new file mode 100644
index 0000000000..5147608a03
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/ui-components-setup.spec.js
@@ -0,0 +1,17 @@
+import { Plugin } from '@/services/plugin-manager/plugin'
+import { createUiComponentsSetup } from '@/services/plugin-manager/setup/ui-components-setup'
+
+const plugin = new Plugin({
+ config: {
+ id: 1
+ }
+})
+
+const uiComponentsSetup = createUiComponentsSetup(plugin)
+uiComponentsSetup()
+
+describe('UI Components Setup', () => {
+ it('should populate the globalComponents field', () => {
+ expect(Object.keys(plugin.globalComponents).length).toBeGreaterThan(0)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/wallet-tabs-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/wallet-tabs-setup.spec.js
new file mode 100644
index 0000000000..7ab054b30c
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/wallet-tabs-setup.spec.js
@@ -0,0 +1,44 @@
+import { createWalletTabsSetup } from '@/services/plugin-manager/setup/wallet-tabs-setup'
+import { Plugin } from '@/services/plugin-manager/plugin'
+
+const plugin = new Plugin({
+ config: {
+ id: 1
+ }
+})
+
+plugin.components = {
+ test: {}
+}
+
+const pluginObject = {
+ getWalletTabs: jest.fn(() => [
+ {
+ tabTitle: 'Test',
+ componentName: 'test'
+ }
+ ])
+}
+
+const sandbox = {
+ app: {
+ $store: {
+ dispatch: jest.fn()
+ }
+ }
+}
+
+const profileId = 'profile1'
+
+const walletTabsSetup = createWalletTabsSetup(plugin, pluginObject, sandbox, profileId)
+walletTabsSetup()
+
+describe('Wallet Tabs Setup', () => {
+ it('should call the getWalletTabs method', () => {
+ expect(pluginObject.getWalletTabs).toHaveBeenCalled()
+ })
+
+ it('should dispatch to vuex', () => {
+ expect(sandbox.app.$store.dispatch).toHaveBeenCalled()
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/setup/webframe-setup.spec.js b/__tests__/unit/services/plugin-manager/setup/webframe-setup.spec.js
new file mode 100644
index 0000000000..25cca9312f
--- /dev/null
+++ b/__tests__/unit/services/plugin-manager/setup/webframe-setup.spec.js
@@ -0,0 +1,17 @@
+import { Plugin } from '@/services/plugin-manager/plugin'
+import { createWebFrameSetup } from '@/services/plugin-manager/setup/webframe-setup'
+
+const plugin = new Plugin({
+ config: {
+ id: 1
+ }
+})
+
+const webFrameSetup = createWebFrameSetup(plugin)
+webFrameSetup()
+
+describe('Webframe Setup', () => {
+ it('should populate the globalComponents field', () => {
+ expect(Object.keys(plugin.globalComponents)).toHaveLength(1)
+ })
+})
diff --git a/__tests__/unit/services/plugin-manager/timers.spec.js b/__tests__/unit/services/plugin-manager/timers.spec.js
deleted file mode 100644
index ca5f2bf493..0000000000
--- a/__tests__/unit/services/plugin-manager/timers.spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import PluginManager from '@/services/plugin-manager'
-
-const routerNext = jest.fn()
-let router
-let sandboxWithTimers
-let sandboxWithoutTimers
-
-beforeEach(() => {
- const routeCallbacks = []
- PluginManager.app = {
- $router: {
- beforeEach: jest.fn(callback => {
- routeCallbacks.push(callback)
- }),
-
- push () {
- for (const callback of routeCallbacks) {
- callback(null, null, routerNext)
- }
- }
- }
- }
- router = PluginManager.app.$router
-
- sandboxWithTimers = PluginManager.loadSandbox({ permissions: ['TIMERS'] })
- sandboxWithoutTimers = PluginManager.loadSandbox({ permissions: [] })
-})
-
-describe('Timers', () => {
- it('should not expose functions without TIMERS permission', () => {
- expect(sandboxWithoutTimers.walletApi.timers).toEqual(undefined)
- })
-
- it('should expose functions with TIMERS permission', () => {
- expect(sandboxWithTimers.walletApi.timers.clearInterval).toBeTruthy()
- expect(sandboxWithTimers.walletApi.timers.clearTimeout).toBeTruthy()
- expect(sandboxWithTimers.walletApi.timers.setInterval).toBeTruthy()
- expect(sandboxWithTimers.walletApi.timers.setTimeout).toBeTruthy()
- })
-
- it('should clear timer once setTimeout executed', (done) => {
- sandboxWithTimers.walletApi.timers.setTimeout(() => {
- console.log('This is an execution of setTimeout')
- setTimeout(() => {
- expect(sandboxWithTimers.walletApi.timers.timeouts.length).toEqual(0)
-
- done()
- }, 100)
- }, 1000)
- expect(sandboxWithTimers.walletApi.timers.timeouts.length).toEqual(1)
- })
-
- it('should clear timer if clearTimeout called', (done) => {
- expect(sandboxWithTimers.walletApi.timers.timeouts.length).toEqual(0)
- const id = sandboxWithTimers.walletApi.timers.setTimeout(() => {
- console.log('This setTimeout call will never execute')
- }, 1000)
- sandboxWithTimers.walletApi.timers.setTimeout(() => {
- console.log('This is an execution of setTimeout')
-
- done()
- }, 1000)
- expect(sandboxWithTimers.walletApi.timers.timeouts.length).toEqual(2)
- sandboxWithTimers.walletApi.timers.clearTimeout(id)
- expect(sandboxWithTimers.walletApi.timers.timeouts.length).toEqual(1)
- })
-
- it('should clear timer if clearInterval called', (done) => {
- expect(sandboxWithTimers.walletApi.timers.intervals.length).toEqual(0)
- let counter = 0
- const id = sandboxWithTimers.walletApi.timers.setInterval(() => {
- counter++
- console.log(`This is execution #${counter} of setInterval`)
- if (counter === 5) {
- sandboxWithTimers.walletApi.timers.clearInterval(id)
- setTimeout(() => {
- expect(sandboxWithTimers.walletApi.timers.intervals.length).toEqual(0)
- expect(counter).toEqual(5)
- done()
- }, 500)
- }
- }, 100)
- expect(sandboxWithTimers.walletApi.timers.intervals.length).toEqual(1)
- })
-
- it('should clear timers on route change', () => {
- sandboxWithTimers.walletApi.timers.setTimeout(() => {
- console.log('This setTimeout call will never execute')
- }, 10000)
-
- sandboxWithTimers.walletApi.timers.setInterval(() => {
- console.log('This setInterval call will never execute')
- }, 10000)
-
- expect(sandboxWithTimers.walletApi.timers.timeouts.length).toEqual(1)
- expect(sandboxWithTimers.walletApi.timers.intervals.length).toEqual(1)
- expect(router.beforeEach).toHaveBeenCalledTimes(1)
- router.push()
- expect(routerNext).toHaveBeenCalledTimes(1)
- expect(sandboxWithTimers.walletApi.timers.timeouts.length).toEqual(0)
- expect(sandboxWithTimers.walletApi.timers.intervals.length).toEqual(0)
- })
-})
diff --git a/src/renderer/services/plugin-manager.js b/src/renderer/services/plugin-manager.js
index 5575eea999..d785df50cf 100644
--- a/src/renderer/services/plugin-manager.js
+++ b/src/renderer/services/plugin-manager.js
@@ -1,34 +1,23 @@
-import * as fs from 'fs-extra'
+import * as fs from 'fs'
import * as path from 'path'
-import * as vm2 from 'vm2'
-import { ipcRenderer } from 'electron'
-import { camelCase, cloneDeep, isBoolean, isEmpty, isObject, isString, partition, uniq, upperFirst } from 'lodash'
+import * as fsExtra from 'fs-extra'
import { PLUGINS } from '@config'
-import PluginHttp from '@/services/plugin-manager/http'
-import PluginWebsocket from '@/services/plugin-manager/websocket'
-import SandboxFontAwesome from '@/services/plugin-manager/font-awesome-sandbox'
-import WalletComponents from '@/services/plugin-manager/wallet-components'
+import { Plugin } from './plugin-manager/plugin'
+import { PluginConfiguration } from './plugin-manager/plugin-configuration'
+import { PluginSetup } from './plugin-manager/plugin-setup'
+import { PluginSandbox } from './plugin-manager/plugin-sandbox'
+import { validatePluginPath } from './plugin-manager/utils/validate-plugin-path'
let rootPath = path.resolve(__dirname, '../../../')
if (process.env.NODE_ENV === 'production') {
rootPath = path.resolve(__dirname, '../../')
}
-class PluginManager {
+export class PluginManager {
constructor () {
this.plugins = {}
- this.pluginRoutes = []
this.hasInit = false
this.vue = null
- this.hooks = [
- 'created',
- 'beforeMount',
- 'mounted',
- 'beforeUpdate',
- 'updated',
- 'beforeDestroy',
- 'destroyed'
- ]
}
setVue (vue) {
@@ -62,441 +51,27 @@ class PluginManager {
throw new Error('Plugin is not enabled')
}
- const pluginObject = plugin.vm.run(
- fs.readFileSync(path.join(plugin.fullPath, 'src/index.js')),
- path.join(plugin.fullPath, 'src/index.js')
- )
-
- if (Object.prototype.hasOwnProperty.call(pluginObject, 'register')) {
- await pluginObject.register()
- }
-
await this.app.$store.dispatch('plugin/setLoaded', {
config: plugin.config,
fullPath: plugin.fullPath,
profileId
})
- const [first, rest] = partition(plugin.config.permissions, permission => {
- // These permissions could be necessary first to load others
- // The rest does not have dependencies: 'MENU_ITEMS', 'AVATARS', 'WALLET_TABS'
- return ['COMPONENTS', 'ROUTES'].includes(permission)
+ const sandbox = new PluginSandbox({
+ plugin,
+ app: this.app
})
- for (const permission of first) {
- const method = `loadPlugin${upperFirst(camelCase(permission))}`
- if (typeof this[method] === 'function') {
- await this[method](pluginObject, plugin, profileId)
- }
- }
- for (const permission of rest) {
- const method = `loadPlugin${upperFirst(camelCase(permission))}`
- if (typeof this[method] === 'function') {
- await this[method](pluginObject, plugin, profileId)
- }
- }
- }
-
- // TODO hook to clean up and restore or reset values
- async disablePlugin (pluginId, profileId) {
- if (!this.hasInit) {
- throw new Error('Plugin Manager not initiated')
- }
-
- const plugin = this.plugins[pluginId]
- if (!plugin) {
- throw new Error(`Plugin \`${pluginId}\` not found`)
- }
-
- if (plugin.config.permissions.includes('THEMES')) {
- await this.unloadThemes(plugin, profileId)
- }
-
- await this.app.$store.dispatch('plugin/deleteLoaded', plugin.config.id)
- }
-
- async loadPluginComponents (pluginObject, plugin) {
- if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getComponentPaths')) {
- return
- }
-
- const components = []
- const pluginComponents = await pluginObject.getComponentPaths()
- if (pluginComponents && !Array.isArray(pluginComponents) && typeof pluginComponents === 'object') {
- for (const componentName of Object.keys(pluginComponents)) {
- let componentPath = pluginComponents[componentName]
- if (componentPath.indexOf('./') === 0) {
- componentPath = `${componentPath.substring(2)}`
- }
- const fullPath = path.join(plugin.fullPath, 'src', pluginComponents[componentName])
-
- const component = plugin.vm.run(
- fs.readFileSync(fullPath),
- fullPath
- )
-
- if (!this.validateComponent(plugin, component, componentName)) {
- continue
- }
-
- // Inline method to format context based on "this"
- const componentContext = function (that, componentData = component) {
- let context = that._data
- if (!context) {
- context = {}
- }
-
- const keys = ['$nextTick', '$refs', '_c', '_v', '_s', '_e', '_m', '_l', '_u']
- for (let key of keys) {
- let thatObject = that[key]
-
- if (key === '$refs' && thatObject) {
- key = 'refs'
- thatObject = {}
- const badGetters = [
- 'attributes',
- 'children',
- 'childNodes',
- 'contentDocument',
- 'contentWindow',
- 'firstChild',
- 'firstElementChild',
- 'lastChild',
- 'lastElementChild',
- 'nextElementSibling',
- 'nextSibling',
- 'offsetParent',
- 'ownerDocument',
- 'parentElement',
- 'parentNode',
- 'shadowRoot',
- 'previousElementSibling',
- 'previousSibling',
- '$root',
- '__vue__'
- ]
- const badSetters = [
- 'innerHTML',
- 'outerHTML'
- ]
- that.$nextTick(() => {
- for (const elKey in that.$refs) {
- const element = that.$refs[elKey]
-
- if (!element.tagName || element.tagName.toLowerCase() === 'iframe') {
- continue
- }
-
- for (const badGetter of badGetters) {
- element.__defineGetter__(badGetter, () => console.log('🚫'))
- }
-
- for (const badSetter of badSetters) {
- element.__defineSetter__(badSetter, () => console.log('🚫'))
- }
-
- thatObject[elKey] = element
- }
- })
- }
-
- context[key] = thatObject
- }
-
- for (const computedName of Object.keys(componentData.computed || {})) {
- context[computedName] = that[computedName]
- }
-
- for (const methodName of Object.keys(componentData.methods || {})) {
- context[methodName] = function () {
- return componentData.methods[methodName].apply(componentContext(that, componentData))
- }
- }
-
- return context
- }
-
- // Build component object inside vm2 to restrict access for methods
- // TODO: Security checks against running from wallet root instead of plugin root.
- // Make sure additional files/packages aren't accessible.
- const vm = new vm2.NodeVM({
- sandbox: { ...this.loadSandbox(plugin.config), document },
- require: {
- builtin: [],
- context: 'sandbox',
- resolve: function (source) {
- return path.resolve(plugin.fullPath, 'src/', source)
- },
- external: {
- modules: [
- path.resolve(plugin.fullPath, 'src/'),
- 'vue/dist/vue.common.js'
- ],
- transitive: true
- },
- root: [
- rootPath,
- path.resolve(plugin.fullPath, 'src/')
- ]
- }
- })
-
- // TODO: Test accessing 'document' "module.exports =" in the required component file.
- const renderedComponent = vm.run(
- `const Vue = require('vue/dist/vue.common.js')
- const component = require('./${componentPath}')
- const compiled = Vue.compile(component.template)
- const componentContext = ${componentContext.toString()}
- if (compiled.staticRenderFns.length) {
- component.render = compiled.render
- component.staticRenderFns = compiled.staticRenderFns
- } else {
- component.render = function () {
- return compiled.render.apply(componentContext(this, component), [ ...arguments ])
- }
- }
- delete component.template
-
- module.exports = component`,
- path.join(rootPath, 'src/vm-component.js')
- )
-
- for (const methodName of Object.keys(renderedComponent.methods || {})) {
- renderedComponent.methods[methodName] = component.methods[methodName]
- }
-
- // Build Vue component
- const vmComponent = this.vue.component(componentName, renderedComponent)
-
- // Fix context of "data" method
- if (component.data) {
- vmComponent.options.data = function () { return component.data.apply(componentContext(this)) }
- }
-
- // Fix context of "computed" methods - also removes global computed methods
- for (const computedName of Object.keys(vmComponent.options.computed)) {
- vmComponent.options.computed[computedName] = function () {}
- }
-
- vmComponent.options.created = [function () {
- for (const computedName of Object.keys(this.$options.computed)) {
- if (component.computed && component.computed[computedName]) {
- this.$options.computed[computedName] = component.computed[computedName].bind(
- componentContext(this)
- )
- this._computedWatchers[computedName].getter = component.computed[computedName].bind(
- componentContext(this)
- )
-
- try {
- this._computedWatchers[computedName].run()
- } catch (error) {
- console.error(error)
- }
- }
-
- if (!component.computed || !component.computed[computedName]) {
- delete this.$options.computed[computedName]
-
- try {
- this._computedWatchers[computedName].teardown()
- } catch (error) {
- console.error(error)
- }
-
- delete this._computedWatchers[computedName]
-
- for (const watcherId in this._watchers) {
- if (this._watchers[watcherId].getter.name === computedName) {
- try {
- this._watchers[watcherId].teardown()
- } catch (error) {
- console.error(error)
- }
-
- break
- }
- }
- }
- }
-
- if (component.created) {
- return component.created.apply(componentContext(this))
- }
- }]
-
- // Fix context of hooks
- this.hooks
- .filter(hook => Object.prototype.hasOwnProperty.call(component, hook))
- .filter(hook => hook !== 'created')
- .forEach(prop => {
- const hookMethod = function () { return component[prop].apply(componentContext(this)) }
- if (Array.isArray(vmComponent.options[prop])) {
- vmComponent.options[prop] = [hookMethod]
- } else {
- vmComponent.options[prop] = hookMethod
- }
- })
-
- components[componentName] = vmComponent
- }
- }
-
- plugin.components = components
- }
-
- async loadPluginRoutes (pluginObject, plugin) {
- if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getRoutes')) {
- return
- }
-
- const pluginRoutes = this.normalize(await pluginObject.getRoutes())
- if (pluginRoutes && Array.isArray(pluginRoutes) && pluginRoutes.length) {
- const allRoutes = this.getAllRoutes()
-
- const routes = pluginRoutes.reduce((valid, route) => {
- if (typeof route.component === 'string' && plugin.components[route.component]) {
- if (allRoutes.every(loadedRoute => loadedRoute.name !== route.name)) {
- valid.push({
- ...route,
- component: plugin.components[route.component]
- })
- }
- }
- return valid
- }, [])
-
- this.pluginRoutes.push(...routes)
- this.app.$router.addRoutes(routes)
- }
- }
-
- async loadPluginMenuItems (pluginObject, plugin, profileId) {
- if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getMenuItems')) {
- return
- }
-
- const pluginMenuItems = this.normalize(pluginObject.getMenuItems())
- if (pluginMenuItems && Array.isArray(pluginMenuItems) && pluginMenuItems.length) {
- const allRoutes = this.getAllRoutes()
-
- const menuItems = pluginMenuItems.reduce((valid, menuItem) => {
- // Check that the related route exists
- if (allRoutes.some(route => route.name === menuItem.routeName)) {
- valid.push(menuItem)
- }
- return valid
- }, [])
-
- await this.app.$store.dispatch('plugin/setMenuItems', {
- pluginId: plugin.config.id,
- menuItems,
- profileId
- })
- }
- }
-
- async loadPluginAvatars (pluginObject, plugin, profileId) {
- if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getAvatars')) {
- return
- }
-
- const pluginAvatars = this.normalize(await pluginObject.getAvatars())
- if (pluginAvatars && Array.isArray(pluginAvatars) && pluginAvatars.length) {
- const avatars = []
- for (const avatar of pluginAvatars) {
- if (typeof avatar !== 'string' || !plugin.components[avatar]) {
- continue
- }
-
- avatars.push(avatar)
- }
-
- plugin.avatars = avatars
-
- if (avatars.length) {
- await this.app.$store.dispatch('plugin/setAvatars', {
- pluginId: plugin.config.id,
- avatars,
- profileId
- })
- }
- }
- }
+ await sandbox.install()
- getAvatarComponents (pluginId) {
- const plugin = this.plugins[pluginId]
- if (!plugin || !plugin.avatars) {
- return {}
- }
-
- const components = {}
- for (const avatarName of plugin.avatars) {
- if (!plugin.components[avatarName]) {
- continue
- }
-
- components[avatarName] = plugin.components[avatarName]
- }
-
- return components
- }
-
- async loadPluginWalletTabs (pluginObject, plugin, profileId) {
- if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getWalletTabs')) {
- return
- }
-
- const pluginWalletTabs = this.normalize(pluginObject.getWalletTabs())
- if (pluginWalletTabs && Array.isArray(pluginWalletTabs) && pluginWalletTabs.length) {
- // Validate the configuration of each tab
- const walletTabs = pluginWalletTabs.reduce((valid, walletTab) => {
- if (isString(walletTab.tabTitle) && plugin.components[walletTab.componentName]) {
- valid.push(walletTab)
- }
- return valid
- }, [])
-
- if (walletTabs.length) {
- await this.app.$store.dispatch('plugin/setWalletTabs', {
- pluginId: plugin.config.id,
- walletTabs,
- profileId
- })
- }
- }
- }
-
- async loadPluginThemes (pluginObject, plugin, profileId) {
- if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getThemes')) {
- return
- }
+ const setup = new PluginSetup({
+ plugin,
+ sandbox,
+ profileId,
+ vue: this.vue
+ })
- const pluginThemes = this.normalize(pluginObject.getThemes())
- if (pluginThemes && isObject(pluginThemes)) {
- // Validate the configuration of each theme and ensure that their CSS exist
- const themes = Object.keys(pluginThemes).reduce((valid, themeName) => {
- const config = pluginThemes[themeName]
-
- if (isBoolean(config.darkMode) && isString(config.cssPath)) {
- const cssPath = path.join(plugin.fullPath, 'src', config.cssPath)
- if (!fs.existsSync(cssPath)) {
- throw new Error(`No file found on \`${config.cssPath}\` for theme "${themeName}"`)
- }
-
- valid[themeName] = { ...config, cssPath }
- }
- return valid
- }, {})
-
- if (!isEmpty(themes)) {
- await this.app.$store.dispatch('plugin/setThemes', {
- pluginId: plugin.config.id,
- themes,
- profileId
- })
- }
- }
+ await setup.install()
}
async unloadThemes (plugin, profileId) {
@@ -510,122 +85,49 @@ class PluginManager {
})
}
- async loadUnprotectedIframeUrls (pluginObject) {
- if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getUnprotectedIframeUrls')) {
- return
- }
+ getWalletTabComponent (pluginId) {
+ const plugin = this.plugins[pluginId]
- const urls = pluginObject.getUnprotectedIframeUrls()
- if (urls) {
- ipcRenderer.send('disable-iframe-protection', urls)
+ if (!plugin) {
+ return {}
}
- }
- getWalletTabComponent (pluginId, walletTab) {
- const component = this.plugins[pluginId].components[walletTab.componentName]
- if (!component) {
- throw new Error(`The wallet tab component \`${walletTab.componentName}\` has not be found`)
- }
- return component
+ return plugin.getWalletTabComponent()
}
- validateComponent (plugin, component, name) {
- const requiredKeys = ['template']
- const allowedKeys = [
- 'data',
- 'methods',
- 'computed',
- 'components',
- ...this.hooks
- ]
-
- const missingKeys = []
- for (const key of requiredKeys) {
- if (!Object.prototype.hasOwnProperty.call(component, key)) {
- missingKeys.push(key)
- }
- }
-
- const componentError = (error, errorType) => {
- this.app.$logger.error(`Plugin '${plugin.config.id}' component '${name}' ${errorType}: ${error}`)
- }
-
- if (missingKeys.length) {
- componentError(missingKeys.join(', '), 'is missing')
-
- return false
- }
+ getAvatarComponents (pluginId) {
+ const plugin = this.plugins[pluginId]
- const inlineErrors = []
- if (/v-html/i.test(component.template)) {
- inlineErrors.push('uses v-html')
- }
- if (/javascript:/i.test(component.template)) {
- inlineErrors.push('"javascript:"')
- }
- if (/<\s*webview/i.test(component.template)) {
- inlineErrors.push('uses webview tag')
- }
- if (/<\s*script/i.test(component.template)) {
- inlineErrors.push('uses script tag')
- } else if (/[^\w]+eval\(/i.test(component.template)) {
- inlineErrors.push('uses eval')
- }
- if (/<\s*iframe/i.test(component.template)) {
- inlineErrors.push('uses iframe tag')
- }
- if (/srcdoc/i.test(component.template)) {
- inlineErrors.push('uses srcdoc property')
- }
- const inlineEvents = []
- for (const event of PLUGINS.validation.events) {
- if ((new RegExp(`on${event}`, 'i')).test(component.template)) {
- inlineEvents.push(event)
- }
- }
- if (inlineEvents.length) {
- inlineErrors.push('events: ' + inlineEvents.join(', '))
+ if (!plugin) {
+ return {}
}
- if (inlineErrors.length) {
- componentError(inlineErrors.join('; '), 'has inline javascript')
+ return plugin.getAvatarComponents()
+ }
- return false
+ // TODO hook to clean up and restore or reset values
+ async disablePlugin (pluginId, profileId) {
+ if (!this.hasInit) {
+ throw new Error('Plugin Manager not initiated')
}
- const bannedKeys = []
- for (const key of Object.keys(component)) {
- if (![...requiredKeys, ...allowedKeys].includes(key)) {
- bannedKeys.push(key)
- }
+ const plugin = this.plugins[pluginId]
+ if (!plugin) {
+ throw new Error(`Plugin \`${pluginId}\` not found`)
}
- if (bannedKeys.length) {
- componentError(bannedKeys.join(', '), 'has unpermitted keys')
-
- return false
+ if (plugin.config.permissions.includes('THEMES')) {
+ await this.unloadThemes(plugin, profileId)
}
- return true
- }
-
- getAllRoutes () {
- return [...this.app.$router.options.routes, ...this.pluginRoutes]
- }
-
- normalize (data) {
- return JSON.parse(JSON.stringify(data))
+ await this.app.$store.dispatch('plugin/deleteLoaded', plugin.config.id)
}
async fetchPluginsFromPath (pluginsPath) {
- fs.ensureDirSync(pluginsPath)
-
- const entries = fs.readdirSync(pluginsPath).filter(entry => {
- if (fs.lstatSync(`${pluginsPath}/${entry}`).isDirectory()) {
- return true
- }
+ fsExtra.ensureDirSync(pluginsPath)
- return false
+ const entries = fsExtra.readdirSync(pluginsPath).filter(entry => {
+ return fsExtra.lstatSync(`${pluginsPath}/${entry}`).isDirectory()
})
for (const entry of entries) {
@@ -639,275 +141,28 @@ class PluginManager {
}
async fetchPlugin (pluginPath) {
- this.validatePlugin(pluginPath)
+ validatePluginPath(pluginPath)
- let config = JSON.parse(fs.readFileSync(`${pluginPath}/package.json`))
- config = this.sanitizeConfig(config)
+ const packageJson = JSON.parse(fs.readFileSync(`${pluginPath}/package.json`))
+ const pluginConfig = PluginConfiguration.sanitize(packageJson)
- if (!config.id) {
- throw new Error('Plugin ID not found')
- } else if (!/^[@/a-z-0-9-]+$/.test(config.id)) {
- throw new Error('Invalid Plugin ID')
- } else if (this.plugins[config.id]) {
- throw new Error(`Plugin '${config.id}' has already been loaded`)
+ if (this.plugins[pluginConfig.id]) {
+ throw new Error(`Plugin '${pluginConfig.id}' has already been loaded`)
}
const fullPath = pluginPath.substring(0, 1) === '/' ? pluginPath : path.resolve(pluginPath)
await this.app.$store.dispatch('plugin/setAvailable', {
- config,
+ config: pluginConfig,
fullPath
})
- this.plugins[config.id] = {
- config,
- path: pluginPath,
+ this.plugins[pluginConfig.id] = new Plugin({
+ config: pluginConfig,
+ path,
fullPath,
- vm: new vm2.NodeVM({
- sandbox: this.loadSandbox(config),
- require: {
- builtin: [],
- context: 'sandbox',
- external: ['./src', 'vue/dist/vue.common.js'],
- root: fullPath
- }
- })
- }
- }
-
- loadSandbox (config) {
- const sandbox = {
- walletApi: {
- icons: SandboxFontAwesome,
- route: {
- get: () => {
- return { ...this.app.$route, matched: [] }
- },
- goTo: routeName => {
- const route = this.getAllRoutes().find(route => routeName === route.name)
- if (route) {
- this.app.$router.push(route)
- }
- }
- }
- }
- }
-
- if (!config.permissions || !Array.isArray(config.permissions)) {
- return sandbox
- }
-
- if (config.permissions.includes('EVENTS')) {
- sandbox.walletApi.eventBus = this.app.$eventBus
- }
-
- if (config.permissions.includes('AUDIO')) {
- sandbox.AudioContext = AudioContext
- }
-
- if (config.permissions.includes('ALERTS')) {
- sandbox.walletApi.alert = {
- error: this.app.$error,
- success: this.app.$success,
- info: this.app.$info,
- warn: this.app.$warn
- }
- }
-
- if (config.permissions.includes('TIMERS')) {
- const timerArrays = {
- intervals: [],
- timeouts: [],
- timeoutWatchdog: {}
- }
-
- const timers = {
- clearInterval (id) {
- clearInterval(id)
- timerArrays.intervals = timerArrays.intervals.filter(interval => interval !== id)
- },
-
- clearTimeout (id) {
- clearTimeout(id)
- clearTimeout(timerArrays.timeoutWatchdog[id])
- delete timerArrays.timeoutWatchdog[id]
- timerArrays.timeouts = timerArrays.timeouts.filter(timeout => timeout !== id)
- },
-
- get intervals () {
- return timerArrays.intervals
- },
-
- get timeouts () {
- return timerArrays.timeouts
- },
-
- setInterval (method, interval, ...args) {
- const id = setInterval(function () {
- method(...args)
- }, interval)
- timerArrays.intervals.push(id)
- return id
- },
-
- setTimeout (method, interval, ...args) {
- const id = setTimeout(function () {
- method(...args)
- }, interval)
- timerArrays.timeouts.push(id)
- timerArrays.timeoutWatchdog[id] = setTimeout(() => timers.clearTimeout(id), interval)
- return id
- }
- }
-
- this.app.$router.beforeEach((_, __, next) => {
- for (const id of timerArrays.intervals) {
- clearInterval(id)
- }
-
- for (const id of timerArrays.timeouts) {
- clearTimeout(id)
- clearTimeout(timerArrays.timeoutWatchdog[id])
- }
-
- timerArrays.intervals = []
- timerArrays.timeouts = []
- timerArrays.timeoutWatchdog = {}
- next()
- })
-
- sandbox.walletApi.timers = timers
- }
-
- if (config.permissions.includes('MESSAGING')) {
- const messages = {
- events: [],
-
- clear () {
- for (const eventId in this.events) {
- window.removeEventListener('message', this.events[eventId])
- }
-
- this.events = []
- },
-
- on (action, eventCallback) {
- const eventTrigger = event => {
- if (event.data !== Object(event.data) || event.data.action !== action) {
- return
- }
-
- eventCallback(cloneDeep(event.data))
- }
-
- window.addEventListener('message', eventTrigger)
- this.events[action] = eventTrigger
- }
- }
-
- this.app.$router.beforeEach((_, __, next) => {
- messages.clear()
- next()
- })
-
- sandbox.walletApi.messages = messages
- }
-
- if (config.permissions.includes('STORAGE')) {
- sandbox.walletApi.storage = {
- get: (key) => {
- const options = this.app.$store.getters['plugin/pluginOptions'](
- config.id,
- this.app.$store.getters['session/profileId']
- )
-
- return options[key]
- },
-
- set: (key, value) => {
- this.app.$store.dispatch('plugin/setPluginOption', {
- profileId: this.app.$store.getters['session/profileId'],
- pluginId: config.id,
- key,
- value
- })
- },
-
- getOptions: () => {
- return this.app.$store.getters['plugin/pluginOptions'](
- config.id,
- this.app.$store.getters['session/profileId']
- )
- }
- }
- }
-
- sandbox.walletApi.components = WalletComponents(config.permissions)
-
- if (config.permissions.includes('HTTP')) {
- sandbox.walletApi.http = new PluginHttp(config.urls)
- }
-
- if (config.permissions.includes('WEBSOCKETS')) {
- sandbox.walletApi.websocket = new PluginWebsocket(config.urls, this.app.$router)
- }
-
- if (config.permissions.includes('PEER_CURRENT')) {
- sandbox.walletApi.peers = {
- current: {
- get: async (url, timeout = 3000) => {
- return (await this.app.$client.client.get(url, { timeout })).body
- },
-
- post: async (url, timeout = 3000) => {
- return (await this.app.$client.client.post(url, { timeout })).body
- }
- }
- }
- }
-
- if (config.permissions.includes('PROFILE_CURRENT')) {
- sandbox.walletApi.profiles = {
- getCurrent: () => {
- return this.app.$store.getters['profile/public']()
- }
- }
- }
-
- if (config.permissions.includes('PROFILE_ALL')) {
- if (!sandbox.walletApi.profiles) {
- sandbox.walletApi.profiles = {}
- }
-
- sandbox.walletApi.profiles.all = this.app.$store.getters['profile/public'](true)
- }
-
- return sandbox
- }
-
- validatePlugin (pluginPath) {
- const structureExists = [
- 'package.json',
- 'src',
- 'src/index.js'
- ]
-
- for (const pathCheck of structureExists) {
- if (!fs.existsSync(path.resolve(pluginPath, pathCheck))) {
- throw new Error(`'${pathCheck}' does not exist`)
- }
- }
- }
-
- sanitizeConfig (config) {
- return {
- id: config.name,
- name: config.title,
- description: config.description,
- version: config.version,
- permissions: uniq(config.permissions).sort(),
- urls: config.urls || []
- }
+ rootPath
+ })
}
}
diff --git a/src/renderer/services/plugin-manager/component/compile-template.js b/src/renderer/services/plugin-manager/component/compile-template.js
new file mode 100644
index 0000000000..42874265ca
--- /dev/null
+++ b/src/renderer/services/plugin-manager/component/compile-template.js
@@ -0,0 +1,20 @@
+import { getSafeContext } from './get-context'
+
+export function compileTemplate (vm, template) {
+ return vm.run(
+ `const Vue = require('vue/dist/vue.common.js')
+ const compiled = Vue.compile(${JSON.stringify(template)})
+ const prepareContext = ${getSafeContext.toString()}
+ const component = {}
+ if (compiled.staticRenderFns.length) {
+ component.render = compiled.render
+ component.staticRenderFns = compiled.staticRenderFns
+ } else {
+ component.render = function () {
+ return compiled.render.apply(prepareContext(this, component), [ ...arguments ])
+ }
+ }
+ module.exports = component`,
+ 'compile-template.js'
+ )
+}
diff --git a/src/renderer/services/plugin-manager/component/create-component.js b/src/renderer/services/plugin-manager/component/create-component.js
new file mode 100644
index 0000000000..8e626dab04
--- /dev/null
+++ b/src/renderer/services/plugin-manager/component/create-component.js
@@ -0,0 +1,85 @@
+import { getSafeContext } from './get-context'
+import { hooks } from './hooks'
+
+export function createSafeComponent (componentName, baseComponent, vue) {
+ // Build Vue component
+ const vmComponent = vue.extend({
+ ...baseComponent,
+ name: componentName
+ })
+
+ // Fix context of "data" method
+ if (baseComponent.data) {
+ vmComponent.options.data = function safeData () {
+ return baseComponent.data.apply(getSafeContext(this, baseComponent))
+ }
+ }
+
+ vmComponent.options.created = [function safeCreated () {
+ if (this.$options.computed) {
+ for (const computedName of Object.keys(this.$options.computed)) {
+ if (baseComponent.computed && baseComponent.computed[computedName]) {
+ this.$options.computed[computedName] = baseComponent.computed[computedName].bind(
+ getSafeContext(this, baseComponent)
+ )
+ this._computedWatchers[computedName].getter = baseComponent.computed[computedName].bind(
+ getSafeContext(this, baseComponent)
+ )
+
+ try {
+ this._computedWatchers[computedName].run()
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ if (!baseComponent.computed || !baseComponent.computed[computedName]) {
+ delete this.$options.computed[computedName]
+
+ try {
+ this._computedWatchers[computedName].teardown()
+ } catch (error) {
+ console.error(error)
+ }
+
+ delete this._computedWatchers[computedName]
+
+ for (const watcherId in this._watchers) {
+ if (this._watchers[watcherId].getter.name === computedName) {
+ try {
+ this._watchers[watcherId].teardown()
+ } catch (error) {
+ console.error(error)
+ }
+
+ break
+ }
+ }
+ }
+ }
+ }
+
+ if (baseComponent.created) {
+ return baseComponent.created.apply(getSafeContext(this, baseComponent))
+ }
+ }]
+
+ // Fix context of hooks
+ hooks
+ .filter(hook => Object.prototype.hasOwnProperty.call(baseComponent, hook))
+ .filter(hook => hook !== 'created')
+ .forEach(prop => {
+ const componentMethod = baseComponent[prop]
+ const hookMethod = function safeHook () {
+ return componentMethod.apply(getSafeContext(this, baseComponent))
+ }
+
+ if (Array.isArray(vmComponent.options[prop])) {
+ vmComponent.options[prop] = [hookMethod]
+ } else {
+ vmComponent.options[prop] = hookMethod
+ }
+ })
+
+ return vmComponent
+}
diff --git a/src/renderer/services/plugin-manager/component/get-context.js b/src/renderer/services/plugin-manager/component/get-context.js
new file mode 100644
index 0000000000..ffc3f9fe77
--- /dev/null
+++ b/src/renderer/services/plugin-manager/component/get-context.js
@@ -0,0 +1,115 @@
+import isElement from 'lodash/isElement'
+
+export function getSafeContext (vueContext, component) {
+ const context = vueContext._data || {}
+
+ const keys = ['$nextTick', '_c', '_v', '_s', '_e', '_m', '_l', '_u']
+
+ for (const key of keys) {
+ context[key] = vueContext[key]
+ }
+
+ if (vueContext.$refs) {
+ const badGetters = [
+ 'attributes',
+ 'children',
+ 'childNodes',
+ 'contentDocument',
+ 'contentWindow',
+ 'firstChild',
+ 'firstElementChild',
+ 'lastChild',
+ 'lastElementChild',
+ 'nextElementSibling',
+ 'nextSibling',
+ 'offsetParent',
+ 'ownerDocument',
+ 'parentElement',
+ 'parentNode',
+ 'shadowRoot',
+ 'previousElementSibling',
+ 'previousSibling',
+ '__vue__',
+ '$root'
+ ]
+
+ const badSetters = [
+ 'innerHTML',
+ 'outerHTML'
+ ]
+
+ const badMethods = [
+ 'appendChild',
+ 'cloneNode',
+ 'getRootNode',
+ 'insertBefore',
+ 'normalize',
+ 'querySelector',
+ 'querySelectorAll',
+ 'removeChild',
+ 'replaceChild'
+ ]
+
+ const blockElementProperties = (element) => {
+ if (!isElement(element) || !element.tagName || element.tagName.toLowerCase() === 'iframe') {
+ return element
+ }
+
+ for (const badSetter of badSetters) {
+ try {
+ element.__defineSetter__(badSetter, () => console.error(`${badSetter} 🚫`))
+ } catch {
+ throw new Error(`Failed to apply ${badSetter} setter to the element. Try wrapping it with '$nextTick'.`)
+ }
+ }
+
+ for (const badGetter of badGetters) {
+ try {
+ element.__defineGetter__(badGetter, () => console.error(`${badGetter} 🚫`))
+ } catch {
+ throw new Error(`Failed to apply ${badGetter} getter to the element. Try wrapping it with '$nextTick'.`)
+ }
+ }
+
+ for (const badMethod of badMethods) {
+ try {
+ element[badMethod] = () => console.error(`${badMethod} 🚫`)
+ } catch {
+ throw new Error(`Failed to apply ${badMethod} method to the element. Try wrapping it with '$nextTick'.`)
+ }
+ }
+
+ return element
+ }
+
+ context.refs = new Proxy(vueContext.$refs, {
+ get (target, prop) {
+ if (prop in target) {
+ return blockElementProperties(target[prop])
+ }
+ }
+ })
+ }
+
+ if (component.computed) {
+ for (const computedName of Object.keys(component.computed || {})) {
+ context[computedName] = vueContext[computedName]
+ }
+ }
+
+ if (component.methods) {
+ for (const methodName of Object.keys(component.methods || {})) {
+ context[methodName] = function () {
+ return component.methods[methodName].apply(getSafeContext(vueContext, component), [...arguments])
+ }
+ }
+ }
+
+ if (vueContext._props) {
+ for (const propName in vueContext._props) {
+ context[propName] = vueContext._props[propName]
+ }
+ }
+
+ return context
+}
diff --git a/src/renderer/services/plugin-manager/component/hooks.js b/src/renderer/services/plugin-manager/component/hooks.js
new file mode 100644
index 0000000000..fa655f010d
--- /dev/null
+++ b/src/renderer/services/plugin-manager/component/hooks.js
@@ -0,0 +1,9 @@
+export const hooks = [
+ 'created',
+ 'beforeMount',
+ 'mounted',
+ 'beforeUpdate',
+ 'updated',
+ 'beforeDestroy',
+ 'destroyed'
+]
diff --git a/src/renderer/services/plugin-manager/component/validate.js b/src/renderer/services/plugin-manager/component/validate.js
new file mode 100644
index 0000000000..7233ef9950
--- /dev/null
+++ b/src/renderer/services/plugin-manager/component/validate.js
@@ -0,0 +1,85 @@
+import { hooks } from './hooks'
+import { PLUGINS } from '@config'
+
+export function validateComponent ({ plugin, component, name, logger }) {
+ const requiredKeys = ['template']
+ const allowedKeys = [
+ 'data',
+ 'methods',
+ 'computed',
+ 'components',
+ 'props',
+ ...hooks
+ ]
+
+ const missingKeys = []
+ for (const key of requiredKeys) {
+ if (!Object.prototype.hasOwnProperty.call(component, key)) {
+ missingKeys.push(key)
+ }
+ }
+
+ const componentError = (error, errorType) => {
+ if (logger) {
+ logger.error(`Plugin '${plugin.config.id}' component '${name}' ${errorType}: ${error}`)
+ }
+ }
+
+ if (missingKeys.length) {
+ componentError(missingKeys.join(', '), 'is missing')
+
+ return false
+ }
+
+ const inlineErrors = []
+ if (/v-html/i.test(component.template)) {
+ inlineErrors.push('uses v-html')
+ }
+ if (/javascript:/i.test(component.template)) {
+ inlineErrors.push('"javascript:"')
+ }
+ if (/<\s*webview/i.test(component.template)) {
+ inlineErrors.push('uses webview tag')
+ }
+ if (/<\s*script/i.test(component.template)) {
+ inlineErrors.push('uses script tag')
+ } else if (/[^\w]+eval\(/i.test(component.template)) {
+ inlineErrors.push('uses eval')
+ }
+ if (/<\s*iframe/i.test(component.template)) {
+ inlineErrors.push('uses iframe tag')
+ }
+ if (/srcdoc/i.test(component.template)) {
+ inlineErrors.push('uses srcdoc property')
+ }
+ const inlineEvents = []
+ for (const event of PLUGINS.validation.events) {
+ if ((new RegExp(`(^|\\s)+on${event}`, 'i')).test(component.template)) {
+ inlineEvents.push(event)
+ }
+ }
+ if (inlineEvents.length) {
+ inlineErrors.push('events: ' + inlineEvents.join(', '))
+ }
+
+ if (inlineErrors.length) {
+ componentError(inlineErrors.join('; '), 'has inline javascript')
+
+ return false
+ }
+
+ const bannedKeys = []
+ for (const key of Object.keys(component)) {
+ if (![...requiredKeys, ...allowedKeys].includes(key)) {
+ bannedKeys.push(key)
+ }
+ }
+
+ if (bannedKeys.length) {
+ componentError(bannedKeys.join(', '), 'has unpermitted keys')
+
+ return false
+ }
+
+ return true
+}
diff --git a/src/renderer/services/plugin-manager/font-awesome-sandbox.js b/src/renderer/services/plugin-manager/font-awesome-sandbox.js
deleted file mode 100644
index e6e918b89f..0000000000
--- a/src/renderer/services/plugin-manager/font-awesome-sandbox.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import * as FontAwesomeIcons from '@fortawesome/free-solid-svg-icons'
-import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
-
-export default {
- component: FontAwesomeIcon,
- icons: FontAwesomeIcons
-}
diff --git a/src/renderer/services/plugin-manager/plugin-component-sandbox.js b/src/renderer/services/plugin-manager/plugin-component-sandbox.js
new file mode 100644
index 0000000000..325f50bac9
--- /dev/null
+++ b/src/renderer/services/plugin-manager/plugin-component-sandbox.js
@@ -0,0 +1,87 @@
+import { createSafeComponent } from './component/create-component'
+import { validateComponent } from './component/validate'
+import { compileTemplate } from './component/compile-template'
+
+export class PluginComponentSandbox {
+ constructor ({
+ fullPath,
+ name,
+ plugin,
+ source,
+ pluginVM,
+ componentVM,
+ vue,
+ logger
+ }) {
+ this.fullPath = fullPath
+ this.name = name
+ this.plugin = plugin
+ this.source = source
+ this.pluginVM = pluginVM
+ this.componentVM = componentVM
+ this.vue = vue
+ this.logger = logger
+
+ this.compiled = undefined
+
+ this.__compileSource()
+ }
+
+ /**
+ * Child components have already been loaded
+ * by VM and their code is available.
+ */
+ get isFromFilesystem () {
+ return Buffer.isBuffer(this.source)
+ }
+
+ cloneSandbox ({ name, source }) {
+ return new PluginComponentSandbox({
+ source: source,
+ name: name,
+ fullPath: this.fullPath,
+ plugin: this.plugin,
+ pluginVM: this.pluginVM,
+ componentVM: this.componentVM,
+ vue: this.vue,
+ logger: this.logger
+ })
+ }
+
+ /**
+ * The raw component is validated, parsed and generates
+ * a secure component to be mounted by Vue.
+ */
+ render () {
+ if (!validateComponent({ ...this, component: this.compiled })) {
+ return
+ }
+
+ const compiledTemplate = compileTemplate(this.pluginVM, this.compiled.template)
+
+ const lazyComponent = Object.assign(compiledTemplate, this.compiled)
+ delete lazyComponent.template
+
+ const components = this.plugin.globalComponents
+
+ for (const childName of Object.keys(this.compiled.components || {})) {
+ const childSandbox = this.cloneSandbox({ name: childName, source: this.compiled.components[childName] })
+ components[childName] = childSandbox.render()
+ }
+
+ lazyComponent.components = components
+
+ return createSafeComponent(this.name, lazyComponent, this.vue)
+ }
+
+ __compileSource () {
+ if (this.isFromFilesystem) {
+ this.compiled = this.componentVM.run(
+ this.source,
+ this.fullPath
+ )
+ } else {
+ this.compiled = this.source
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/plugin-configuration.js b/src/renderer/services/plugin-manager/plugin-configuration.js
new file mode 100644
index 0000000000..c1cdcaf7be
--- /dev/null
+++ b/src/renderer/services/plugin-manager/plugin-configuration.js
@@ -0,0 +1,38 @@
+import uniq from 'lodash/uniq'
+
+export class PluginConfiguration {
+ constructor ({
+ id,
+ name,
+ description,
+ permissions,
+ urls,
+ version
+ }) {
+ this.id = id
+ this.name = name
+ this.description = description
+ this.permissions = permissions
+ this.urls = urls
+ this.version = version
+ }
+
+ static sanitize (config) {
+ return new PluginConfiguration({
+ id: config.name,
+ name: config.title,
+ description: config.description,
+ version: config.version,
+ permissions: uniq(config.permissions).sort(),
+ urls: config.urls || []
+ })
+ }
+
+ validate () {
+ if (!this.id) {
+ throw new Error('Plugin ID not found')
+ } else if (!/^[@/a-z-0-9-]+$/.test(this.id)) {
+ throw new Error('Invalid Plugin ID')
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/plugin-permission.js b/src/renderer/services/plugin-manager/plugin-permission.js
new file mode 100644
index 0000000000..67f2aecfc9
--- /dev/null
+++ b/src/renderer/services/plugin-manager/plugin-permission.js
@@ -0,0 +1,30 @@
+class Permission {
+ constructor ({
+ name,
+ description
+ }) {
+ this.name = name
+ this.description = description
+ }
+}
+
+export const AVATARS = new Permission({ name: 'AVATARS' })
+export const COMPONENTS = new Permission({ name: 'COMPONENTS' })
+export const HTTP = new Permission({ name: 'HTTP' })
+export const MENU_ITEMS = new Permission({ name: 'MENU_ITEMS' })
+export const MESSAGING = new Permission({ name: 'MESSAGING' })
+export const PUBLIC = new Permission({ name: 'PUBLIC' })
+export const ROUTES = new Permission({ name: 'ROUTES' })
+export const THEMES = new Permission({ name: 'THEMES' })
+export const TIMERS = new Permission({ name: 'TIMERS' })
+export const UI_COMPONENTS = new Permission({ name: 'UI_COMPONENTS' })
+export const WALLET_TABS = new Permission({ name: 'WALLET_TABS' })
+export const WEBFRAME = new Permission({ name: 'WEBFRAME' })
+export const WEBSOCKET = new Permission({ name: 'WEBSOCKET' })
+export const PROFILE_ALL = new Permission({ name: 'PROFILE_ALL' })
+export const PROFILE_CURRENT = new Permission({ name: 'PROFILE_CURRENT' })
+export const PEER_CURRENT = new Permission({ name: 'PEER_CURRENT' })
+export const STORAGE = new Permission({ name: 'STORAGE' })
+export const AUDIO = new Permission({ name: 'AUDIO' })
+export const EVENTS = new Permission({ name: 'EVENTS' })
+export const ALERTS = new Permission({ name: 'ALERTS' })
diff --git a/src/renderer/services/plugin-manager/plugin-sandbox.js b/src/renderer/services/plugin-manager/plugin-sandbox.js
new file mode 100644
index 0000000000..32fd32f007
--- /dev/null
+++ b/src/renderer/services/plugin-manager/plugin-sandbox.js
@@ -0,0 +1,119 @@
+/* eslint-disable no-unused-vars */
+import path from 'path'
+import { castArray } from 'lodash'
+import { NodeVM } from 'vm2'
+import {
+ HTTP,
+ MESSAGING,
+ WEBSOCKET,
+ PUBLIC,
+ TIMERS,
+ PROFILE_ALL,
+ PROFILE_CURRENT,
+ PEER_CURRENT,
+ STORAGE,
+ AUDIO,
+ EVENTS,
+ ALERTS
+} from './plugin-permission'
+import { createHttpSandbox } from './sandbox/http-sandbox'
+import { createMessagingSandbox } from './sandbox/messaging-sandbox'
+import { createWebsocketSandbox } from './sandbox/websocket-sandbox'
+import { createFontAwesomeSandbox } from './sandbox/font-awesome-sandbox'
+import { createRouteSandbox } from './sandbox/route-sandbox'
+import { createTimersSandbox } from './sandbox/timers-sandbox'
+import { createProfileAllSandbox } from './sandbox/profile-all-sandbox'
+import { createProfileCurrentSandbox } from './sandbox/profile-current-sandbox'
+import { createPeerCurrentSandbox } from './sandbox/peer-current-sandbox'
+import { createStorageSandbox } from './sandbox/storage-sandbox'
+import { createAudioSandbox } from './sandbox/audio-sandbox'
+import { createEventsSandbox } from './sandbox/events-sandbox'
+import { createAlertsSandbox } from './sandbox/alerts-sandbox'
+
+export class PluginSandbox {
+ constructor ({
+ app,
+ plugin
+ }) {
+ this.app = app
+ this.plugin = plugin
+
+ this.sandbox = {}
+ this.walletApi = {}
+
+ this.sandboxes = this.__mapPermissionsToSandbox()
+ }
+
+ getSandbox (loadApi = true) {
+ if (loadApi) {
+ return {
+ ...this.sandbox,
+ walletApi: this.walletApi
+ }
+ }
+
+ return {
+ document
+ }
+ }
+
+ getVM ({ loadApi }) {
+ const fullPath = this.plugin.fullPath
+
+ return new NodeVM({
+ sandbox: this.getSandbox(loadApi),
+ require: {
+ builtin: [],
+ context: 'sandbox',
+ resolve: function (source) {
+ return path.resolve(fullPath, source)
+ },
+ external: {
+ modules: [
+ path.resolve(fullPath),
+ 'vue/dist/vue.common.js'
+ ],
+ transitive: true
+ },
+ root: [
+ this.plugin.rootPath,
+ path.resolve(fullPath)
+ ]
+ }
+ })
+ }
+
+ async install () {
+ await this.__run(this.sandboxes[PUBLIC.name])
+
+ for (const permissionName of this.plugin.config.permissions) {
+ await this.__run(this.sandboxes[permissionName])
+ }
+ }
+
+ async __run (sandboxes = []) {
+ for (const sandbox of castArray(sandboxes)) {
+ await sandbox()
+ }
+ }
+
+ __mapPermissionsToSandbox () {
+ return {
+ [ALERTS.name]: createAlertsSandbox(this.walletApi, this.app),
+ [AUDIO.name]: createAudioSandbox(this.sandbox),
+ [EVENTS.name]: createEventsSandbox(this.walletApi, this.app),
+ [HTTP.name]: createHttpSandbox(this.walletApi, this.plugin),
+ [MESSAGING.name]: createMessagingSandbox(this.walletApi, this.app),
+ [PEER_CURRENT.name]: createPeerCurrentSandbox(this.walletApi, this.app),
+ [PROFILE_ALL.name]: createProfileAllSandbox(this.walletApi, this.app),
+ [PROFILE_CURRENT.name]: createProfileCurrentSandbox(this.walletApi, this.app),
+ [PUBLIC.name]: [
+ createFontAwesomeSandbox(this.walletApi),
+ createRouteSandbox(this.walletApi, this.plugin, this.app)
+ ],
+ [STORAGE.name]: createStorageSandbox(this.walletApi, this.app, this.plugin),
+ [TIMERS.name]: createTimersSandbox(this.walletApi, this.app),
+ [WEBSOCKET.name]: createWebsocketSandbox(this.walletApi, this.app, this.plugin)
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/plugin-setup.js b/src/renderer/services/plugin-manager/plugin-setup.js
new file mode 100644
index 0000000000..c28588a2cc
--- /dev/null
+++ b/src/renderer/services/plugin-manager/plugin-setup.js
@@ -0,0 +1,89 @@
+import path from 'path'
+import fs from 'fs'
+import { castArray } from 'lodash'
+import {
+ COMPONENTS,
+ AVATARS,
+ WALLET_TABS,
+ ROUTES,
+ PUBLIC,
+ MENU_ITEMS,
+ THEMES,
+ WEBFRAME,
+ UI_COMPONENTS
+} from './plugin-permission'
+import { createComponentsSetup } from './setup/components-setup'
+import { createAvatarsSetup } from './setup/avatars-setup'
+import { createRoutesSetup } from './setup/routes-setup'
+import { createWalletTabsSetup } from './setup/wallet-tabs-setup'
+import { createRegisterSetup } from './setup/register-setup'
+import { createMenuItemsSetup } from './setup/menu-items-setup'
+import { createThemesSetup } from './setup/themes-setup'
+import { createWebFrameSetup } from './setup/webframe-setup'
+import { createUiComponentsSetup } from './setup/ui-components-setup'
+import { createFontAwesomeSetup } from './setup/font-awesome-setup'
+
+export class PluginSetup {
+ constructor ({
+ plugin,
+ sandbox,
+ vue,
+ profileId
+ }) {
+ this.plugin = plugin
+ this.sandbox = sandbox
+ this.profileId = profileId
+
+ const localVue = vue.extend()
+ localVue.options._base = localVue
+ this.vue = localVue
+
+ this.pluginObject = this.sandbox.getVM(false).run(
+ fs.readFileSync(path.join(plugin.fullPath, 'src/index.js')),
+ path.join(plugin.fullPath, 'src/index.js')
+ )
+
+ this.setups = this.__mapPermissionsToSetup()
+ }
+
+ async install () {
+ await this.__run(this.setups[PUBLIC.name])
+
+ const permissions = this.plugin.config.permissions
+ const priorities = [WEBFRAME.name, UI_COMPONENTS.name, COMPONENTS.name, ROUTES.name]
+
+ const first = priorities.filter(p => permissions.includes(p))
+ const rest = permissions.filter(p => !priorities.includes(p))
+
+ for (const permissionName of first) {
+ await this.__run(this.setups[permissionName])
+ }
+
+ for (const permissionName of rest) {
+ await this.__run(this.setups[permissionName])
+ }
+ }
+
+ async __run (setups = []) {
+ for (const setup of castArray(setups)) {
+ await setup()
+ }
+ }
+
+ __mapPermissionsToSetup () {
+ return {
+ [AVATARS.name]: createAvatarsSetup(this.plugin, this.pluginObject, this.sandbox, this.profileId),
+ [WALLET_TABS.name]: createWalletTabsSetup(this.plugin, this.pluginObject, this.sandbox, this.profileId),
+ [ROUTES.name]: createRoutesSetup(this.plugin, this.pluginObject, this.sandbox),
+ [COMPONENTS.name]: createComponentsSetup(this.plugin, this.pluginObject, this.sandbox, this.vue),
+ [MENU_ITEMS.name]: createMenuItemsSetup(this.plugin, this.pluginObject, this.sandbox, this.profileId),
+ [THEMES.name]: createThemesSetup(this.plugin, this.pluginObject, this.sandbox, this.profileId),
+ [PUBLIC.name]: [
+ createRegisterSetup(this.pluginObject),
+ createFontAwesomeSetup(this.plugin)
+ ],
+ [WEBFRAME.name]: createWebFrameSetup(this.plugin),
+ [UI_COMPONENTS.name]: createUiComponentsSetup(this.plugin)
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/plugin.js b/src/renderer/services/plugin-manager/plugin.js
new file mode 100644
index 0000000000..5cbb388c84
--- /dev/null
+++ b/src/renderer/services/plugin-manager/plugin.js
@@ -0,0 +1,44 @@
+export class Plugin {
+ constructor ({
+ config,
+ path,
+ fullPath,
+ rootPath
+ }) {
+ this.config = config
+ this.path = path
+ this.fullPath = fullPath
+ this.rootPath = rootPath
+
+ this.globalComponents = {}
+ this.components = {}
+ this.avatars = []
+ this.routes = []
+ }
+
+ getAvatarComponents () {
+ if (!this.avatars) {
+ return {}
+ }
+
+ const components = {}
+ for (const avatarName of this.avatars) {
+ if (!this.components[avatarName]) {
+ continue
+ }
+
+ components[avatarName] = this.components[avatarName]
+ }
+
+ return components
+ }
+
+ getWalletTabComponent (walletTab) {
+ const component = this.components[walletTab.componentName]
+ if (!component) {
+ throw new Error(`The wallet tab component \`${walletTab.componentName}\` has not be found`)
+ }
+
+ return component
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/alerts-sandbox.js b/src/renderer/services/plugin-manager/sandbox/alerts-sandbox.js
new file mode 100644
index 0000000000..62dfaa2e9b
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/alerts-sandbox.js
@@ -0,0 +1,10 @@
+export function createAlertsSandbox (walletApi, app) {
+ return () => {
+ walletApi.alert = {
+ error: app.$error,
+ success: app.$success,
+ info: app.$info,
+ warn: app.$warn
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/audio-sandbox.js b/src/renderer/services/plugin-manager/sandbox/audio-sandbox.js
new file mode 100644
index 0000000000..d564120898
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/audio-sandbox.js
@@ -0,0 +1,5 @@
+export function createAudioSandbox (sandbox) {
+ return () => {
+ sandbox.AudioContext = AudioContext
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/events-sandbox.js b/src/renderer/services/plugin-manager/sandbox/events-sandbox.js
new file mode 100644
index 0000000000..38cef71a40
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/events-sandbox.js
@@ -0,0 +1,5 @@
+export function createEventsSandbox (walletApi, app) {
+ return () => {
+ walletApi.eventBus = app.$eventBus
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/font-awesome-sandbox.js b/src/renderer/services/plugin-manager/sandbox/font-awesome-sandbox.js
new file mode 100644
index 0000000000..ab305d943d
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/font-awesome-sandbox.js
@@ -0,0 +1,7 @@
+import * as FontAwesomeIcons from '@fortawesome/free-solid-svg-icons'
+
+export function createFontAwesomeSandbox (walletApi) {
+ return () => {
+ walletApi.fontAwesomeIcons = FontAwesomeIcons
+ }
+}
diff --git a/src/renderer/services/plugin-manager/http.js b/src/renderer/services/plugin-manager/sandbox/http-sandbox.js
similarity index 80%
rename from src/renderer/services/plugin-manager/http.js
rename to src/renderer/services/plugin-manager/sandbox/http-sandbox.js
index 9435da9cd6..bec77b27c4 100644
--- a/src/renderer/services/plugin-manager/http.js
+++ b/src/renderer/services/plugin-manager/sandbox/http-sandbox.js
@@ -1,6 +1,6 @@
import got from 'got'
-export default class PluginHttp {
+class PluginHttp {
constructor (whitelist) {
this.whitelist = []
@@ -38,3 +38,9 @@ export default class PluginHttp {
return got.post(url, opts)
}
}
+
+export function createHttpSandbox (walletApi, plugin) {
+ return () => {
+ walletApi.http = new PluginHttp(plugin.config.urls)
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/messaging-sandbox.js b/src/renderer/services/plugin-manager/sandbox/messaging-sandbox.js
new file mode 100644
index 0000000000..a95e8213b2
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/messaging-sandbox.js
@@ -0,0 +1,37 @@
+import cloneDeep from 'lodash/cloneDeep'
+
+export function createMessagingSandbox (walletApi, app) {
+ return () => {
+ const messages = {
+ events: {},
+
+ clear () {
+ for (const eventId in this.events) {
+ window.removeEventListener('message', this.events[eventId])
+ }
+
+ this.events = {}
+ },
+
+ on (action, eventCallback) {
+ const eventTrigger = event => {
+ if (event.data !== Object(event.data) || event.data.action !== action) {
+ return
+ }
+
+ eventCallback(cloneDeep(event.data))
+ }
+
+ window.addEventListener('message', eventTrigger)
+ this.events[action] = eventTrigger
+ }
+ }
+
+ app.$router.beforeEach((_, __, next) => {
+ messages.clear()
+ next()
+ })
+
+ walletApi.messages = messages
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/peer-current-sandbox.js b/src/renderer/services/plugin-manager/sandbox/peer-current-sandbox.js
new file mode 100644
index 0000000000..f077dfa55d
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/peer-current-sandbox.js
@@ -0,0 +1,15 @@
+export function createPeerCurrentSandbox (walletApi, app) {
+ return () => {
+ walletApi.peers = {
+ current: {
+ get: async (url, timeout = 3000) => {
+ return (await app.$client.client.get(url, { timeout })).body
+ },
+
+ post: async (url, timeout = 3000) => {
+ return (await app.$client.client.post(url, { timeout })).body
+ }
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/profile-all-sandbox.js b/src/renderer/services/plugin-manager/sandbox/profile-all-sandbox.js
new file mode 100644
index 0000000000..1e235abdbc
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/profile-all-sandbox.js
@@ -0,0 +1,9 @@
+export function createProfileAllSandbox (walletApi, app) {
+ return () => {
+ if (!walletApi.profiles) {
+ walletApi.profiles = {}
+ }
+
+ walletApi.profiles.all = app.$store.getters['profile/public'](true)
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/profile-current-sandbox.js b/src/renderer/services/plugin-manager/sandbox/profile-current-sandbox.js
new file mode 100644
index 0000000000..66fe771101
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/profile-current-sandbox.js
@@ -0,0 +1,15 @@
+export function createProfileCurrentSandbox (walletApi, app) {
+ return () => {
+ if (!walletApi.profiles) {
+ walletApi.profiles = {}
+ }
+
+ walletApi.profiles = {
+ ...walletApi.profiles,
+
+ getCurrent: () => {
+ return app.$store.getters['profile/public']()
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/route-sandbox.js b/src/renderer/services/plugin-manager/sandbox/route-sandbox.js
new file mode 100644
index 0000000000..48edb02154
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/route-sandbox.js
@@ -0,0 +1,17 @@
+import { getAllRoutes } from '../utils/get-all-routes'
+
+export function createRouteSandbox (walletApi, plugin, app) {
+ return () => {
+ walletApi.route = {
+ get: () => {
+ return { ...app.$route, matched: [] }
+ },
+ goTo: routeName => {
+ const route = getAllRoutes(app, plugin).find(route => routeName === route.name)
+ if (route) {
+ app.$router.push(route)
+ }
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/storage-sandbox.js b/src/renderer/services/plugin-manager/sandbox/storage-sandbox.js
new file mode 100644
index 0000000000..89bd75d4fa
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/storage-sandbox.js
@@ -0,0 +1,30 @@
+export function createStorageSandbox (walletApi, app, plugin) {
+ return () => {
+ walletApi.storage = {
+ get: (key) => {
+ const options = app.$store.getters['plugin/pluginOptions'](
+ plugin.config.id,
+ app.$store.getters['session/profileId']
+ )
+
+ return options && options[key]
+ },
+
+ set: (key, value) => {
+ app.$store.dispatch('plugin/setPluginOption', {
+ profileId: app.$store.getters['session/profileId'],
+ pluginId: plugin.config.id,
+ key,
+ value
+ })
+ },
+
+ getOptions: () => {
+ return app.$store.getters['plugin/pluginOptions'](
+ plugin.config.id,
+ app.$store.getters['session/profileId']
+ )
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/sandbox/timers-sandbox.js b/src/renderer/services/plugin-manager/sandbox/timers-sandbox.js
new file mode 100644
index 0000000000..b554b134d6
--- /dev/null
+++ b/src/renderer/services/plugin-manager/sandbox/timers-sandbox.js
@@ -0,0 +1,66 @@
+export function createTimersSandbox (walletApi, app) {
+ return () => {
+ const timerArrays = {
+ intervals: [],
+ timeouts: [],
+ timeoutWatchdog: {}
+ }
+
+ const timers = {
+ clearInterval (id) {
+ clearInterval(id)
+ timerArrays.intervals = timerArrays.intervals.filter(interval => interval !== id)
+ },
+
+ clearTimeout (id) {
+ clearTimeout(id)
+ clearTimeout(timerArrays.timeoutWatchdog[id])
+ delete timerArrays.timeoutWatchdog[id]
+ timerArrays.timeouts = timerArrays.timeouts.filter(timeout => timeout !== id)
+ },
+
+ get intervals () {
+ return timerArrays.intervals
+ },
+
+ get timeouts () {
+ return timerArrays.timeouts
+ },
+
+ setInterval (method, interval, ...args) {
+ const id = setInterval(function () {
+ method(...args)
+ }, interval)
+ timerArrays.intervals.push(id)
+ return id
+ },
+
+ setTimeout (method, interval, ...args) {
+ const id = setTimeout(function () {
+ method(...args)
+ }, interval)
+ timerArrays.timeouts.push(id)
+ timerArrays.timeoutWatchdog[id] = setTimeout(() => timers.clearTimeout(id), interval)
+ return id
+ }
+ }
+
+ app.$router.beforeEach((_, __, next) => {
+ for (const id of timerArrays.intervals) {
+ clearInterval(id)
+ }
+
+ for (const id of timerArrays.timeouts) {
+ clearTimeout(id)
+ clearTimeout(timerArrays.timeoutWatchdog[id])
+ }
+
+ timerArrays.intervals = []
+ timerArrays.timeouts = []
+ timerArrays.timeoutWatchdog = {}
+ next()
+ })
+
+ walletApi.timers = timers
+ }
+}
diff --git a/src/renderer/services/plugin-manager/websocket.js b/src/renderer/services/plugin-manager/sandbox/websocket-sandbox.js
similarity index 91%
rename from src/renderer/services/plugin-manager/websocket.js
rename to src/renderer/services/plugin-manager/sandbox/websocket-sandbox.js
index 0e94c8ddea..3158a2035f 100644
--- a/src/renderer/services/plugin-manager/websocket.js
+++ b/src/renderer/services/plugin-manager/sandbox/websocket-sandbox.js
@@ -1,4 +1,4 @@
-export default class PluginWebsocket {
+class PluginWebsocket {
constructor (whitelist, router) {
this.whitelist = []
this.router = router
@@ -97,3 +97,9 @@ export default class PluginWebsocket {
return websocketEvents
}
}
+
+export function createWebsocketSandbox (walletApi, app, plugin) {
+ return () => {
+ walletApi.websocket = new PluginWebsocket(plugin.config.urls, app.$router)
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/avatars-setup.js b/src/renderer/services/plugin-manager/setup/avatars-setup.js
new file mode 100644
index 0000000000..9d4a01693a
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/avatars-setup.js
@@ -0,0 +1,31 @@
+import { normalizeJson } from '../utils/normalize-json'
+
+export function createAvatarsSetup (plugin, pluginObject, sandbox, profileId) {
+ return async () => {
+ if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getAvatars')) {
+ return
+ }
+
+ const pluginAvatars = normalizeJson(await pluginObject.getAvatars())
+ if (pluginAvatars && Array.isArray(pluginAvatars) && pluginAvatars.length) {
+ const avatars = []
+ for (const avatar of pluginAvatars) {
+ if (typeof avatar !== 'string' || !plugin.components[avatar]) {
+ continue
+ }
+
+ avatars.push(avatar)
+ }
+
+ plugin.avatars = avatars
+
+ if (avatars.length) {
+ await sandbox.app.$store.dispatch('plugin/setAvatars', {
+ pluginId: plugin.config.id,
+ avatars,
+ profileId
+ })
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/components-setup.js b/src/renderer/services/plugin-manager/setup/components-setup.js
new file mode 100644
index 0000000000..16997e2e9a
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/components-setup.js
@@ -0,0 +1,41 @@
+
+import fs from 'fs'
+import path from 'path'
+import { PluginComponentSandbox } from '../plugin-component-sandbox'
+
+export function createComponentsSetup (plugin, pluginObject, sandbox, vue) {
+ return () => {
+ if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getComponentPaths')) {
+ return
+ }
+
+ const pluginComponents = pluginObject.getComponentPaths()
+ const components = {}
+
+ for (const componentName of Object.keys(pluginComponents)) {
+ const fullPath = path.join(plugin.fullPath, 'src', pluginComponents[componentName])
+ const source = fs.readFileSync(fullPath)
+
+ if (source) {
+ const component = new PluginComponentSandbox({
+ source,
+ plugin,
+ vue,
+ fullPath,
+ name: componentName,
+ pluginVM: sandbox.getVM({ loadApi: false }),
+ componentVM: sandbox.getVM({ loadApi: true }),
+ logger: sandbox.app.$logger
+ })
+
+ const vmComponent = component.render()
+
+ if (vmComponent) {
+ components[componentName] = vmComponent
+ }
+ }
+ }
+
+ plugin.components = components
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/font-awesome-setup.js b/src/renderer/services/plugin-manager/setup/font-awesome-setup.js
new file mode 100644
index 0000000000..8273b62a69
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/font-awesome-setup.js
@@ -0,0 +1,7 @@
+import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
+
+export function createFontAwesomeSetup (plugin) {
+ return () => {
+ plugin.globalComponents[FontAwesomeIcon.name] = FontAwesomeIcon
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/menu-items-setup.js b/src/renderer/services/plugin-manager/setup/menu-items-setup.js
new file mode 100644
index 0000000000..97016aee65
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/menu-items-setup.js
@@ -0,0 +1,30 @@
+import { normalizeJson } from '../utils/normalize-json'
+import { getAllRoutes } from '../utils/get-all-routes'
+
+export function createMenuItemsSetup (plugin, pluginObject, sandbox, profileId) {
+ return async () => {
+ if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getMenuItems')) {
+ return
+ }
+
+ const pluginMenuItems = normalizeJson(pluginObject.getMenuItems())
+ if (pluginMenuItems && Array.isArray(pluginMenuItems) && pluginMenuItems.length) {
+ const allRoutes = getAllRoutes(sandbox.app, plugin)
+ const menuItems = pluginMenuItems.reduce((valid, menuItem) => {
+ // Check that the related route exists
+ if (allRoutes.some(route => route.name === menuItem.routeName)) {
+ valid.push(menuItem)
+ }
+ return valid
+ }, [])
+
+ if (menuItems.length) {
+ await sandbox.app.$store.dispatch('plugin/setMenuItems', {
+ pluginId: plugin.config.id,
+ menuItems,
+ profileId
+ })
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/register-setup.js b/src/renderer/services/plugin-manager/setup/register-setup.js
new file mode 100644
index 0000000000..1bfbd3ac7e
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/register-setup.js
@@ -0,0 +1,7 @@
+export function createRegisterSetup (pluginObject) {
+ return async () => {
+ if (Object.prototype.hasOwnProperty.call(pluginObject, 'register')) {
+ await pluginObject.register()
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/routes-setup.js b/src/renderer/services/plugin-manager/setup/routes-setup.js
new file mode 100644
index 0000000000..a462326c8e
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/routes-setup.js
@@ -0,0 +1,32 @@
+import { normalizeJson } from '../utils/normalize-json'
+import { getAllRoutes } from '../utils/get-all-routes'
+
+export function createRoutesSetup (plugin, pluginObject, sandbox) {
+ return () => {
+ if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getRoutes')) {
+ return
+ }
+
+ const pluginRoutes = normalizeJson(pluginObject.getRoutes())
+ if (pluginRoutes && Array.isArray(pluginRoutes) && pluginRoutes.length) {
+ const allRoutes = getAllRoutes(sandbox.app)
+
+ const routes = pluginRoutes.reduce((valid, route) => {
+ if (typeof route.component === 'string' && plugin.components[route.component]) {
+ if (allRoutes.every(loadedRoute => loadedRoute.name !== route.name)) {
+ valid.push({
+ ...route,
+ component: plugin.components[route.component]
+ })
+ }
+ }
+ return valid
+ }, [])
+
+ if (routes.length) {
+ plugin.routes.push(...routes)
+ sandbox.app.$router.addRoutes(routes)
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/themes-setup.js b/src/renderer/services/plugin-manager/setup/themes-setup.js
new file mode 100644
index 0000000000..caf67e2a76
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/themes-setup.js
@@ -0,0 +1,38 @@
+import path from 'path'
+import fs from 'fs'
+import { normalizeJson } from '../utils/normalize-json'
+import { isEmpty, isString, isObject, isBoolean } from 'lodash'
+
+export function createThemesSetup (plugin, pluginObject, sandbox, profileId) {
+ return async () => {
+ if (!Object.prototype.hasOwnProperty.call(pluginObject, 'getThemes')) {
+ return
+ }
+
+ const pluginThemes = normalizeJson(pluginObject.getThemes())
+ if (pluginThemes && isObject(pluginThemes)) {
+ // Validate the configuration of each theme and ensure that their CSS exist
+ const themes = Object.keys(pluginThemes).reduce((valid, themeName) => {
+ const config = pluginThemes[themeName]
+
+ if (isBoolean(config.darkMode) && isString(config.cssPath)) {
+ const cssPath = path.join(plugin.fullPath, 'src', config.cssPath)
+ if (!fs.existsSync(cssPath)) {
+ throw new Error(`No file found on \`${config.cssPath}\` for theme "${themeName}"`)
+ }
+
+ valid[themeName] = { ...config, cssPath }
+ }
+ return valid
+ }, {})
+
+ if (!isEmpty(themes)) {
+ await sandbox.app.$store.dispatch('plugin/setThemes', {
+ pluginId: plugin.config.id,
+ themes,
+ profileId
+ })
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/ui-components-setup.js b/src/renderer/services/plugin-manager/setup/ui-components-setup.js
new file mode 100644
index 0000000000..fdf205a600
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/ui-components-setup.js
@@ -0,0 +1,26 @@
+import * as ButtonComponents from '@/components/Button'
+import * as CollapseComponents from '@/components/Collapse'
+import * as InputComponents from '@/components/Input'
+import * as ListDividedComponents from '@/components/ListDivided'
+import * as MenuComponents from '@/components/Menu'
+import Loader from '@/components/utils/Loader'
+import TableWrapper from '@/components/utils/TableWrapper'
+
+export function createUiComponentsSetup (plugin) {
+ return () => {
+ const components = {
+ ...ButtonComponents,
+ ...CollapseComponents,
+ ...InputComponents,
+ ...ListDividedComponents,
+ ...MenuComponents,
+ Loader,
+ TableWrapper
+ }
+
+ plugin.globalComponents = {
+ ...plugin.globalComponents,
+ ...components
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/wallet-tabs-setup.js b/src/renderer/services/plugin-manager/setup/wallet-tabs-setup.js
new file mode 100644
index 0000000000..8b212858d3
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/wallet-tabs-setup.js
@@ -0,0 +1,26 @@
+import { normalizeJson } from '../utils/normalize-json'
+import isString from 'lodash/isString'
+
+export function createWalletTabsSetup (plugin, pluginObject, sandbox, profileId) {
+ return async () => {
+ const pluginWalletTabs = normalizeJson(pluginObject.getWalletTabs())
+
+ if (pluginWalletTabs && Array.isArray(pluginWalletTabs) && pluginWalletTabs.length) {
+ // Validate the configuration of each tab
+ const walletTabs = pluginWalletTabs.reduce((valid, walletTab) => {
+ if (isString(walletTab.tabTitle) && plugin.components[walletTab.componentName]) {
+ valid.push(walletTab)
+ }
+ return valid
+ }, [])
+
+ if (walletTabs.length) {
+ await sandbox.app.$store.dispatch('plugin/setWalletTabs', {
+ pluginId: plugin.config.id,
+ walletTabs,
+ profileId
+ })
+ }
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/setup/webframe-setup.js b/src/renderer/services/plugin-manager/setup/webframe-setup.js
new file mode 100644
index 0000000000..04082de507
--- /dev/null
+++ b/src/renderer/services/plugin-manager/setup/webframe-setup.js
@@ -0,0 +1,7 @@
+import WebFrame from '@/components/utils/WebFrame'
+
+export function createWebFrameSetup (plugin) {
+ return () => {
+ plugin.globalComponents[WebFrame.name] = WebFrame
+ }
+}
diff --git a/src/renderer/services/plugin-manager/utils/get-all-routes.js b/src/renderer/services/plugin-manager/utils/get-all-routes.js
new file mode 100644
index 0000000000..558261f81c
--- /dev/null
+++ b/src/renderer/services/plugin-manager/utils/get-all-routes.js
@@ -0,0 +1,4 @@
+export function getAllRoutes (app, plugin) {
+ const routes = plugin ? plugin.routes : []
+ return [...app.$router.options.routes, ...routes]
+}
diff --git a/src/renderer/services/plugin-manager/utils/normalize-json.js b/src/renderer/services/plugin-manager/utils/normalize-json.js
new file mode 100644
index 0000000000..cf92596705
--- /dev/null
+++ b/src/renderer/services/plugin-manager/utils/normalize-json.js
@@ -0,0 +1,3 @@
+export function normalizeJson (data) {
+ return JSON.parse(JSON.stringify(data))
+}
diff --git a/src/renderer/services/plugin-manager/utils/validate-plugin-path.js b/src/renderer/services/plugin-manager/utils/validate-plugin-path.js
new file mode 100644
index 0000000000..b68ead4281
--- /dev/null
+++ b/src/renderer/services/plugin-manager/utils/validate-plugin-path.js
@@ -0,0 +1,16 @@
+import path from 'path'
+import fs from 'fs'
+
+export function validatePluginPath (pluginPath) {
+ const structureExists = [
+ 'package.json',
+ 'src',
+ 'src/index.js'
+ ]
+
+ for (const pathCheck of structureExists) {
+ if (!fs.existsSync(path.resolve(pluginPath, pathCheck))) {
+ throw new Error(`'${pathCheck}' does not exist`)
+ }
+ }
+}
diff --git a/src/renderer/services/plugin-manager/wallet-components.js b/src/renderer/services/plugin-manager/wallet-components.js
deleted file mode 100644
index 639fdc0469..0000000000
--- a/src/renderer/services/plugin-manager/wallet-components.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import * as ButtonComponents from '@/components/Button'
-import * as CollapseComponents from '@/components/Collapse'
-import * as InputComponents from '@/components/Input'
-import * as ListDividedComponents from '@/components/ListDivided'
-import * as MenuComponents from '@/components/Menu'
-import Loader from '@/components/utils/Loader'
-import TableWrapper from '@/components/utils/TableWrapper'
-import WebFrame from '@/components/utils/WebFrame'
-
-export default (permissions) => {
- let components = {}
-
- if (permissions.includes('UI_COMPONENTS')) {
- components = {
- Button: ButtonComponents,
- Collapse: CollapseComponents,
- Input: InputComponents,
- ListDivided: ListDividedComponents,
- Loader,
- Menu: MenuComponents,
- TableWrapper
- }
- }
-
- if (permissions.includes('WEBFRAME')) {
- components = { ...components, WebFrame }
- }
-
- return components
-}
diff --git a/src/renderer/store/modules/plugin.js b/src/renderer/store/modules/plugin.js
index dc4289d576..d85bd8ab89 100644
--- a/src/renderer/store/modules/plugin.js
+++ b/src/renderer/store/modules/plugin.js
@@ -225,7 +225,11 @@ export default {
return
}
- for (const pluginId of Object.keys(state.enabled[profile.id])) {
+ for (const pluginId in state.enabled[profile.id]) {
+ if (!getters.isEnabled(pluginId)) {
+ continue
+ }
+
if (!getters.isAvailable(pluginId)) {
continue
}