Merge pull request #2340 from ethereum/fix_save

Better checks when saving a file
pull/5370/head
yann300 3 years ago committed by GitHub
commit c8490e53b0
  1. 6
      .circleci/config.yml
  2. 31
      apps/remix-ide-e2e/src/tests/plugin_api.ts
  3. 1
      apps/remix-ide-e2e/src/tests/terminal.test.ts
  4. 11
      apps/remix-ide/src/app/plugins/permission-handler-plugin.tsx
  5. 58
      apps/remix-ide/src/app/tabs/web3-provider.js
  6. 56
      apps/remix-ide/src/remixAppManager.js
  7. 6
      libs/remix-core-plugin/src/lib/compiler-content-imports.ts
  8. 3
      libs/remix-ui/permission-handler/src/interface/index.ts
  9. 8
      libs/remix-ui/permission-handler/src/lib/permission-dialog.tsx

@ -107,7 +107,7 @@ jobs:
- COMMIT_AUTHOR: "Circle CI"
working_directory: ~/remix-project
parallelism: 70
parallelism: 80
steps:
- browser-tools/install-chrome
- browser-tools/install-chromedriver
@ -150,7 +150,7 @@ jobs:
- COMMIT_AUTHOR: "Circle CI"
working_directory: ~/remix-project
parallelism: 70
parallelism: 80
steps:
- browser-tools/install-firefox
- browser-tools/install-geckodriver
@ -192,7 +192,7 @@ jobs:
- COMMIT_AUTHOR_EMAIL: "yann@ethereum.org"
- COMMIT_AUTHOR: "Circle CI"
working_directory: ~/remix-project
parallelism: 7
parallelism: 10
steps:
- browser-tools/install-chrome
- browser-tools/install-chromedriver

@ -83,10 +83,35 @@ const checkForAcceptAndRemember = async function (browser: NightwatchBrowser) {
browser.frameParent(() => {
browser.pause(1000).element('xpath', '//*[@data-id="permissionHandlerRememberUnchecked"]', (visible: any) => {
if (visible.status && visible.status === -1) {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
browser.pause(1000).element('xpath', '//*[@data-id="PermissionHandler-modal-footer-ok-react"]', (okPresent: any) => {
if ((okPresent.status && okPresent.status === -1) || okPresent.value === false) {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
} else {
browser
.useXpath()
.isVisible('//*[@data-id="PermissionHandler-modal-footer-ok-react"]', (okVisible: any) => {
if (okVisible.value) {
browser.click('//*[@data-id="PermissionHandler-modal-footer-ok-react"]', () => {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
})
} else {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
}
})
}
})
} else {
browser.waitForElementVisible('//*[@data-id="permissionHandlerRememberUnchecked"]').click('//*[@data-id="permissionHandlerRememberUnchecked"]').waitForElementVisible('//*[@data-id="PermissionHandler-modal-footer-ok-react"]').click('//*[@data-id="PermissionHandler-modal-footer-ok-react"]', () => {
browser.waitForElementVisible('//*[@data-id="permissionHandlerRememberUnchecked"]')
.click('//*[@data-id="permissionHandlerRememberUnchecked"]')
.waitForElementVisible('//*[@data-id="PermissionHandler-modal-footer-ok-react"]')
.click('//*[@data-id="PermissionHandler-modal-footer-ok-react"]', () => {
// @ts-ignore
browser.frame(0, () => { resolve(true) })
})

@ -88,6 +88,7 @@ module.exports = {
.executeScript('remix.execute(\'resolveExternalUrlAndSaveToaPath.js\')')
.waitForElementContainsText('*[data-id="terminalJournal"]', 'abstract contract ERC20Burnable', 60000)
.openFile('.deps/github/newFile.sol')
},
'Deploy "Owner" using an ether.js script, listen to event and check event are logged in the terminal #group4': function (browser: NightwatchBrowser) {

@ -70,12 +70,12 @@ export class PermissionHandlerPlugin extends Plugin {
* @param {string} message from the caller plugin to add more details if needed
* @returns {Promise<boolean>}
*/
async askPermission(from: Profile, to: Profile, method: string, message: string) {
async askPermission(from: Profile, to: Profile, method: string, message: string, sensitiveCall: boolean) {
try {
this.permissions = this._getFromLocal()
if (!this.permissions[to.name]) this.permissions[to.name] = {}
if (!this.permissions[to.name][method]) this.permissions[to.name][method] = {}
if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message)
if (!this.permissions[to.name][method][from.name]) return this.openPermission(from, to, method, message, sensitiveCall)
const { allow, hash } = this.permissions[to.name][method][from.name]
if (!allow) {
@ -85,20 +85,21 @@ export class PermissionHandlerPlugin extends Plugin {
}
return hash === from.hash
? true // Allow
: await this.openPermission(from, to, method, message)
: await this.openPermission(from, to, method, message, sensitiveCall)
} catch (err) {
throw new Error(err)
}
}
async openPermission(from: Profile, to: Profile, method: string, message: string) {
async openPermission(from: Profile, to: Profile, method: string, message: string, sensitiveCall: boolean) {
const remember = this.permissions[to.name][method][from.name]
const value: PermissionHandlerValue = {
from,
to,
method,
message,
remember
remember,
sensitiveCall
}
const modal: AppModal = {
id: 'PermissionHandler',

@ -11,7 +11,7 @@ export const profile = {
}
export class Web3ProviderModule extends Plugin {
constructor (blockchain) {
constructor(blockchain) {
super(profile)
this.blockchain = blockchain
}
@ -20,31 +20,41 @@ export class Web3ProviderModule extends Plugin {
that is used by plugins to call the current ethereum provider.
Should be taken carefully and probably not be release as it is now.
*/
sendAsync (payload) {
sendAsync(payload) {
return new Promise((resolve, reject) => {
const provider = this.blockchain.web3().currentProvider
// see https://github.com/ethereum/web3.js/pull/1018/files#diff-d25786686c1053b786cc2626dc6e048675050593c0ebaafbf0814e1996f22022R129
provider[provider.sendAsync ? 'sendAsync' : 'send'](payload, async (error, message) => {
if (error) return reject(error)
if (payload.method === 'eth_sendTransaction') {
if (payload.params.length && !payload.params[0].to && message.result) {
setTimeout(async () => {
const receipt = await this.tryTillReceiptAvailable(message.result)
if (!receipt.contractAddress) {
console.log('receipt available but contract address not present', receipt)
return
this.askUserPermission('sendAsync', `Calling ${payload.method} with parameters ${JSON.stringify(payload.params, null, '\t')}`).then(
async (result) => {
if (result) {
const provider = this.blockchain.web3().currentProvider
// see https://github.com/ethereum/web3.js/pull/1018/files#diff-d25786686c1053b786cc2626dc6e048675050593c0ebaafbf0814e1996f22022R129
provider[provider.sendAsync ? 'sendAsync' : 'send'](payload, async (error, message) => {
if (error) return reject(error)
if (payload.method === 'eth_sendTransaction') {
if (payload.params.length && !payload.params[0].to && message.result) {
setTimeout(async () => {
const receipt = await this.tryTillReceiptAvailable(message.result)
if (!receipt.contractAddress) {
console.log('receipt available but contract address not present', receipt)
return
}
const contractData = await this.call('compilerArtefacts', 'getContractDataFromAddress', receipt.contractAddress)
if (contractData) this.call('udapp', 'addInstance', receipt.contractAddress, contractData.contract.abi, contractData.name)
}, 50)
}
}
const contractData = await this.call('compilerArtefacts', 'getContractDataFromAddress', receipt.contractAddress)
if (contractData) this.call('udapp', 'addInstance', receipt.contractAddress, contractData.contract.abi, contractData.name)
}, 50)
resolve(message)
})
} else {
reject(new Error('User denied permission'))
}
}
resolve(message)
})
}).catch((e) => {
reject(e)
})
})
}
async tryTillReceiptAvailable (txhash) {
async tryTillReceiptAvailable(txhash) {
try {
const receipt = await this.call('blockchain', 'getTransactionReceipt', txhash)
if (receipt) return receipt
@ -55,9 +65,9 @@ export class Web3ProviderModule extends Plugin {
return await this.tryTillReceiptAvailable(txhash)
}
async pause () {
return new Promise((resolve, reject) => {
setTimeout(resolve, 500)
})
async pause() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 500)
})
}
}

@ -12,7 +12,13 @@ const requiredModules = [ // services + layout views + system views
const dependentModules = ['git', 'hardhat', 'truffle', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd)
export function isNative (name) {
const sensitiveCalls = {
'fileManager': ['writeFile', 'copyFile', 'rename', 'copyDir'],
'contentImport': ['resolveAndSave'],
'web3Provider': ['sendAsync'],
}
export function isNative(name) {
const nativePlugins = ['vyper', 'workshops', 'debugger', 'remixd', 'menuicons', 'solidity', 'hardhat-provider', 'solidityStaticAnalysis', 'solidityUnitTesting', 'layout', 'notification', 'hardhat-provider', 'ganache-provider']
return nativePlugins.includes(name) || requiredModules.includes(name)
}
@ -27,34 +33,34 @@ export function isNative (name) {
* @param {any, any}
* @returns {boolean}
*/
export function canActivate (from, to) {
export function canActivate(from, to) {
return ['ethdoc'].includes(from.name) ||
isNative(from.name) ||
(to && from && from.canActivate && from.canActivate.includes(to.name))
isNative(from.name) ||
(to && from && from.canActivate && from.canActivate.includes(to.name))
}
export class RemixAppManager extends PluginManager {
constructor () {
constructor() {
super()
this.event = new EventEmitter()
this.pluginsDirectory = 'https://raw.githubusercontent.com/ethereum/remix-plugins-directory/master/build/metadata.json'
this.pluginLoader = new PluginLoader()
}
async canActivatePlugin (from, to) {
async canActivatePlugin(from, to) {
return canActivate(from, to)
}
async canDeactivatePlugin (from, to) {
async canDeactivatePlugin(from, to) {
if (requiredModules.includes(to.name)) return false
return isNative(from.name)
}
async canDeactivate(from,to) {
return this.canDeactivatePlugin(from, to)
async canDeactivate(from, to) {
return this.canDeactivatePlugin(from, to)
}
async deactivatePlugin (name) {
async deactivatePlugin(name) {
const [to, from] = [
await this.getProfile(name),
await this.getProfile(this.requestFrom)
@ -64,7 +70,8 @@ export class RemixAppManager extends PluginManager {
}
}
async canCall (from, to, method, message) {
async canCall(from, to, method, message) {
const isSensitiveCall = sensitiveCalls[to] && sensitiveCalls[to].includes(method)
// Make sure the caller of this methods is the target plugin
if (to !== this.currentRequest.from) {
return false
@ -73,43 +80,44 @@ export class RemixAppManager extends PluginManager {
if (isNative(from)) {
return true
}
// ask the user for permission
return await this.call('permissionhandler', 'askPermission', this.profiles[from], this.profiles[to], method, message)
return await this.call('permissionhandler', 'askPermission', this.profiles[from], this.profiles[to], method, message, isSensitiveCall)
}
onPluginActivated (plugin) {
onPluginActivated(plugin) {
this.pluginLoader.set(plugin, this.actives)
this.event.emit('activate', plugin)
this.emit('activate', plugin)
if (!requiredModules.includes(plugin.name)) _paq.push(['trackEvent', 'pluginManager', 'activate', plugin.name])
}
getAll () {
getAll() {
return Object.keys(this.profiles).map((p) => {
return this.profiles[p]
})
}
getIds () {
getIds() {
return Object.keys(this.profiles)
}
onPluginDeactivated (plugin) {
onPluginDeactivated(plugin) {
this.pluginLoader.set(plugin, this.actives)
this.event.emit('deactivate', plugin)
_paq.push(['trackEvent', 'pluginManager', 'deactivate', plugin.name])
}
isDependent (name) {
isDependent(name) {
return dependentModules.includes(name)
}
isRequired (name) {
isRequired(name) {
// excluding internal use plugins
return requiredModules.includes(name)
}
async registeredPlugins () {
async registeredPlugins() {
let plugins
try {
const res = await fetch(this.pluginsDirectory)
@ -138,7 +146,7 @@ export class RemixAppManager extends PluginManager {
})
}
async registerContextMenuItems () {
async registerContextMenuItems() {
await this.call('filePanel', 'registerContextMenuItem', {
id: 'flattener',
name: 'flattenFileCustomAction',
@ -167,11 +175,11 @@ export class RemixAppManager extends PluginManager {
* (localStorage, queryParams)
**/
class PluginLoader {
get currentLoader () {
get currentLoader() {
return this.loaders[this.current]
}
constructor () {
constructor() {
const queryParams = new QueryParams()
this.donotAutoReload = ['remixd', 'git'] // that would be a bad practice to force loading some plugins at page load.
this.loaders = {}
@ -195,11 +203,11 @@ class PluginLoader {
this.current = queryParams.get().activate ? 'queryParams' : 'localStorage'
}
set (plugin, actives) {
set(plugin, actives) {
this.currentLoader.set(plugin, actives)
}
get () {
get() {
return this.currentLoader.get()
}
}

@ -124,6 +124,10 @@ export class CompilerImports extends Plugin {
*/
async resolveAndSave (url, targetPath) {
try {
if (targetPath && this.currentRequest) {
const canCall = await this.askUserPermission('resolveAndSave', 'This action will update the path ' + targetPath)
if (!canCall) throw new Error('No permission to update ' + targetPath)
}
const provider = await this.call('fileManager', 'getProviderOf', url)
if (provider) {
if (provider.type === 'localhost' && !provider.isConnected()) {
@ -177,7 +181,7 @@ export class CompilerImports extends Plugin {
}
}
} catch (e) {
throw new Error(`not found ${url}`)
throw new Error(e)
}
}
}

@ -5,7 +5,8 @@ export interface PermissionHandlerValue {
to: Profile,
remember: boolean,
method: string,
message: string
message: string,
sensitiveCall: boolean
}
export interface PermissionHandlerProps {

@ -3,7 +3,7 @@ import { PermissionHandlerProps } from '../interface'
import './permission-dialog.css'
const PermissionHandlerDialog = (props: PermissionHandlerProps) => {
const { from, to, remember, method, message } = props.value
const { from, to, remember, method, message, sensitiveCall } = props.value
const [feedback, setFeedback] = useState<string>('')
const theme = props.theme
@ -52,12 +52,14 @@ const PermissionHandlerDialog = (props: PermissionHandlerProps) => {
<h6>{to.displayName} :</h6>
<p> {to.description || <i>No description Provided</i>}</p>
{pluginMessage()}
{ sensitiveCall ? <p className='text-warning'><i className="fas fa-exclamation-triangle mr-2" aria-hidden="true"></i>You are going to process a sensitive call. Please make sure you trust this plugin.</p> : '' }
</article>
<article className='remember'>
<div className='form-check'>
{ !sensitiveCall && <div className='form-check'>
{rememberSwitch()}
<label htmlFor='remember' className="form-check-label" data-id="permissionHandlerRememberChoice">Remember this choice</label>
</div>
</div>
}
<button className="btn btn-sm" onClick={reset}>Reset all Permissions</button>
</article>
<div>{feedback}</div>

Loading…
Cancel
Save