diff --git a/src/app/components/local-plugin.js b/src/app/components/local-plugin.js index 59c1a5bcf5..d760eb0714 100644 --- a/src/app/components/local-plugin.js +++ b/src/app/components/local-plugin.js @@ -35,7 +35,8 @@ module.exports = class LocalPlugin { ...this.profile, icon: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xMjYyIDEwNzVxLTM3IDEyMS0xMzggMTk1dC0yMjggNzQtMjI4LTc0LTEzOC0xOTVxLTgtMjUgNC00OC41dDM4LTMxLjVxMjUtOCA0OC41IDR0MzEuNSAzOHEyNSA4MCA5Mi41IDEyOS41dDE1MS41IDQ5LjUgMTUxLjUtNDkuNSA5Mi41LTEyOS41cTgtMjYgMzItMzh0NDktNCAzNyAzMS41IDQgNDguNXptLTQ5NC00MzVxMCA1My0zNy41IDkwLjV0LTkwLjUgMzcuNS05MC41LTM3LjUtMzcuNS05MC41IDM3LjUtOTAuNSA5MC41LTM3LjUgOTAuNSAzNy41IDM3LjUgOTAuNXptNTEyIDBxMCA1My0zNy41IDkwLjV0LTkwLjUgMzcuNS05MC41LTM3LjUtMzcuNS05MC41IDM3LjUtOTAuNSA5MC41LTM3LjUgOTAuNSAzNy41IDM3LjUgOTAuNXptMjU2IDI1NnEwLTEzMC01MS0yNDguNXQtMTM2LjUtMjA0LTIwNC0xMzYuNS0yNDguNS01MS0yNDguNSA1MS0yMDQgMTM2LjUtMTM2LjUgMjA0LTUxIDI0OC41IDUxIDI0OC41IDEzNi41IDIwNCAyMDQgMTM2LjUgMjQ4LjUgNTEgMjQ4LjUtNTEgMjA0LTEzNi41IDEzNi41LTIwNCA1MS0yNDguNXptMTI4IDBxMCAyMDktMTAzIDM4NS41dC0yNzkuNSAyNzkuNS0zODUuNSAxMDMtMzg1LjUtMTAzLTI3OS41LTI3OS41LTEwMy0zODUuNSAxMDMtMzg1LjUgMjc5LjUtMjc5LjUgMzg1LjUtMTAzIDM4NS41IDEwMyAyNzkuNSAyNzkuNSAxMDMgMzg1LjV6Ii8+PC9zdmc+', methods: [], - events: [] + events: [], + hash: `local-${this.profile.name}` } if (!this.profile.name) throw new Error('Plugin should have a name') if (!this.profile.url) throw new Error('Plugin should have an URL') diff --git a/src/app/tabs/compile-tab.js b/src/app/tabs/compile-tab.js index cf55d583e5..f87d43f61a 100644 --- a/src/app/tabs/compile-tab.js +++ b/src/app/tabs/compile-tab.js @@ -65,7 +65,8 @@ class CompileTab extends ApiFactory { events: ['compilationFinished'], icon: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE2LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIgoJIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMTMwMHB4IiBoZWlnaHQ9IjEzMDBweCIKCSB2aWV3Qm94PSIwIDAgMTMwMCAxMzAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMzAwIDEzMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8dGl0bGU+VmVjdG9yIDE8L3RpdGxlPgo8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KPGcgaWQ9IlBhZ2UtMSIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+Cgk8ZyBpZD0ic29saWRpdHkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQwMi4wMDAwMDAsIDExOC4wMDAwMDApIiBza2V0Y2g6dHlwZT0iTVNMYXllckdyb3VwIj4KCQk8ZyBpZD0iR3JvdXAiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPgoJCQk8cGF0aCBpZD0iU2hhcGUiIG9wYWNpdHk9IjAuNDUiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgICAgIiBkPSJNMzcxLjc3MiwxMzUuMzA4TDI0MS4wNjgsMzY3LjYxSC0yMC4xNThsMTMwLjYxNC0yMzIuMzAyCgkJCQlIMzcxLjc3MiIvPgoJCQk8cGF0aCBpZD0iU2hhcGVfMV8iIG9wYWNpdHk9IjAuNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAgICAiIGQ9Ik0yNDEuMDY4LDM2Ny42MWgyNjEuMzE4TDM3MS43NzIsMTM1LjMwOEgxMTAuNDU2CgkJCQlMMjQxLjA2OCwzNjcuNjF6Ii8+CgkJCTxwYXRoIGlkPSJTaGFwZV8yXyIgb3BhY2l0eT0iMC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3ICAgICIgZD0iTTExMC40NTYsNTk5LjgyMkwyNDEuMDY4LDM2Ny42MUwxMTAuNDU2LDEzNS4zMDgKCQkJCUwtMjAuMTU4LDM2Ny42MUwxMTAuNDU2LDU5OS44MjJ6Ii8+CgkJCTxwYXRoIGlkPSJTaGFwZV8zXyIgb3BhY2l0eT0iMC40NSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAgICAiIGQ9Ik0xMTEuNzIxLDk0OC4yNzVsMTMwLjcwNC0yMzIuMzAzaDI2MS4zMThMMzczLjAzOCw5NDguMjc1CgkJCQlIMTExLjcyMSIvPgoJCQk8cGF0aCBpZD0iU2hhcGVfNF8iIG9wYWNpdHk9IjAuNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAgICAiIGQ9Ik0yNDIuNDI0LDcxNS45NzNILTE4Ljg5M2wxMzAuNjEzLDIzMi4zMDNoMjYxLjMxNwoJCQkJTDI0Mi40MjQsNzE1Ljk3M3oiLz4KCQkJPHBhdGggaWQ9IlNoYXBlXzVfIiBvcGFjaXR5PSIwLjgiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgICAgIiBkPSJNMzczLjAzOCw0ODMuNzYxTDI0Mi40MjQsNzE1Ljk3M2wxMzAuNjE0LDIzMi4zMDMKCQkJCWwxMzAuNzA0LTIzMi4zMDNMMzczLjAzOCw0ODMuNzYxeiIvPgoJCTwvZz4KCTwvZz4KPC9nPgo8L3N2Zz4K', description: 'compile solidity contracts', - kind: 'compile' + kind: 'compile', + permission: true } } diff --git a/src/persmission-handler.js b/src/persmission-handler.js new file mode 100644 index 0000000000..7a6142df03 --- /dev/null +++ b/src/persmission-handler.js @@ -0,0 +1,126 @@ +/* global localStorage */ +const yo = require('yo-yo') +const csjs = require('csjs-inject') +const modalDialog = require('./app/ui/modaldialog') + +const css = csjs` +.permission p { + text-align: center; +} +.images { + display: flex; + justify-content: center; + align-item: center; +} +.images img { + width: 40px; + height: 40px; +} +` + +export class PermissionHandler { + + constructor () { + const permission = localStorage.getItem('plugin-permissions') + this.permissions = permission ? JSON.parse(permission) : {} + } + + persistPermissions () { + const permissions = JSON.stringify(this.permissions) + localStorage.setItem('plugin-permissions', permissions) + } + + clear () { + localStorage.removeItem('plugin-permissions') + } + + /** + * Show a message to ask the user for a permission + * @param {PluginProfile} from The name and hash of the plugin that make the call + * @param {ModuleProfile} to The name of the plugin that receive the call + * @returns {Promise<{ allow: boolean; remember: boolean }} Answer from the user to the permission + */ + async openPermission (from, to) { + return new Promise((resolve, reject) => { + modalDialog( + `Permission needed for ${to.displayName || to.name}`, + this.form(from, to), + { + label: 'Accept', + fn: () => { + if (this.permissions[to.name][from.name]) { + this.permissions[to.name][from.name].allow = true + this.persistPermissions() + } + resolve(true) + } + }, + { + label: 'Decline', + fn: () => { + if (this.permissions[to.name][from.name]) { + this.permissions[to.name][from.name].allow = false + this.persistPermissions() + } + resolve(false) + } + } + ) + }) + } + + /** + * Check if a plugin has the permission to call another plugin and askPermission if needed + * @param {PluginProfile} from the profile of the plugin that make the call + * @param {ModuleProfile} to The profile of the module that receive the call + * @returns {Promise} + */ + async askPermission (from, to) { + if (!this.permissions[to.name]) this.permissions[to.name] = {} + if (!this.permissions[to.name][from.name]) return this.openPermission(from, to) + + const { allow, hash } = this.permissions[to.name][from.name] + if (!allow) return false + return hash === from.hash + ? true // Allow + : this.openPermission(from, to) // New version of a plugin + } + + /** + * The permission form + * @param {PluginProfile} from The name and hash of the plugin that make the call + * @param {ModuleProfile} to The name of the plugin that receive the call + */ + form (from, to) { + const fromName = from.displayName || from.name + const toName = from.displayName || from.name + const remember = this.permissions[to.name][from.name] + + const switchMode = (e) => { + e.target.checked + ? this.permissions[to.name][from.name] = {} + : delete this.permissions[to.name][from.name] + } + const rememberSwitch = remember + ? yo`` + : yo`` + const message = remember + ? `${fromName} has changed and would like to access the plugin ${toName}.` + : `${fromName} would like to access plugin ${toName}.` + + return yo` +
+
+ + -> + +
+

${message}

+
+ ${rememberSwitch} + +
+
+ ` + } +} diff --git a/src/remixAppManager.js b/src/remixAppManager.js index e2d60ce83f..65e8647384 100644 --- a/src/remixAppManager.js +++ b/src/remixAppManager.js @@ -1,11 +1,13 @@ import { AppManagerApi, Plugin } from 'remix-plugin' import { EventEmitter } from 'events' import PluginManagerProxy from './app/components/plugin-manager-proxy' +import { PermissionHandler } from './persmission-handler' export class RemixAppManager extends AppManagerApi { constructor (store) { super(null) + this.permissionHandler = new PermissionHandler() this.store = store this.hiddenServices = {} this.event = new EventEmitter()