diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index e430b0278..32d385fb8 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -34,6 +34,7 @@ module.exports = { singleDatabase: connections.singleDatabase, hideAppEditor: !!process.env.HIDE_APP_EDITOR, allowShellConnection: platformInfo.allowShellConnection, + allowShellScripting: platformInfo.allowShellConnection, permissions, ...currentVersion, }; diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index 4a537decf..a83c6e5a4 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -73,7 +73,7 @@ module.exports = { const res = []; for (const packageName of _.union(files1, files2)) { if (packageName == 'dist') continue; - // if (!/^dbgate-plugin-.*$/.test(packageName)) continue; + if (!/^dbgate-plugin-.*$/.test(packageName)) continue; try { if (packagedContent && packagedContent[packageName]) { const manifest = { diff --git a/packages/api/src/controllers/runners.js b/packages/api/src/controllers/runners.js index e7da9d8fb..be8445c61 100644 --- a/packages/api/src/controllers/runners.js +++ b/packages/api/src/controllers/runners.js @@ -6,9 +6,10 @@ const byline = require('byline'); const socket = require('../utility/socket'); const { fork } = require('child_process'); const { rundir, uploadsdir, pluginsdir, getPluginBackendPath, packagedPluginList } = require('../utility/directories'); -const { extractShellApiPlugins, extractShellApiFunctionName } = require('dbgate-tools'); +const { extractShellApiPlugins, extractShellApiFunctionName, jsonScriptToJavascript } = require('dbgate-tools'); const { handleProcessCommunication } = require('../utility/processComm'); const processArgs = require('../utility/processArgs'); +const platformInfo = require('../utility/platformInfo'); function extractPlugins(script) { const requireRegex = /\s*\/\/\s*@require\s+([^\s]+)\s*\n/g; @@ -148,12 +149,18 @@ module.exports = { }, start_meta: true, - async start({ script, isGeneratedScript }) { - if (!isGeneratedScript && process.env.DISABLE_SHELL) { - return { errorMessage: 'Shell is disabled' }; + async start({ script }) { + const runid = uuidv1(); + + if (script.type == 'json') { + const js = jsonScriptToJavascript(script); + return this.startCore(runid, scriptTemplate(js, false)); + } + + if (!platformInfo.allowShellScripting) { + return { errorMessage: 'Shell scripting is not allowed' }; } - const runid = uuidv1(); return this.startCore(runid, scriptTemplate(script, false)); }, diff --git a/packages/api/src/utility/platformInfo.js b/packages/api/src/utility/platformInfo.js index ce29d3028..b5d590520 100644 --- a/packages/api/src/utility/platformInfo.js +++ b/packages/api/src/utility/platformInfo.js @@ -40,6 +40,7 @@ const platformInfo = { platform, runningInWebpack: !!process.env.WEBPACK_DEV_SERVER_URL, allowShellConnection: !!process.env.SHELL_CONNECTION || !!isElectron(), + allowShellScripting: !!process.env.SHELL_SCRIPTING || !!isElectron(), defaultKeyfile: path.join(os.homedir(), '.ssh/id_rsa'), }; diff --git a/packages/tools/src/ScriptWriter.ts b/packages/tools/src/ScriptWriter.ts new file mode 100644 index 000000000..23dae1166 --- /dev/null +++ b/packages/tools/src/ScriptWriter.ts @@ -0,0 +1,165 @@ +import _uniq from 'lodash/uniq'; +import { extractShellApiFunctionName, extractShellApiPlugins } from './packageTools'; + +export class ScriptWriter { + s = ''; + packageNames: string[] = []; + varCount = 0; + + constructor(varCount = '0') { + this.varCount = parseInt(varCount) || 0; + } + + allocVariable(prefix = 'var') { + this.varCount += 1; + return `${prefix}${this.varCount}`; + } + + _put(s = '') { + this.s += s; + this.s += '\n'; + } + + endLine() { + this._put(); + } + + assignCore(variableName, functionName, props) { + this._put(`const ${variableName} = await ${functionName}(${JSON.stringify(props)});`); + } + + assign(variableName, functionName, props) { + this.assignCore(variableName, extractShellApiFunctionName(functionName), props); + this.packageNames.push(...extractShellApiPlugins(functionName, props)); + } + + assignValue(variableName, jsonValue) { + this._put(`const ${variableName} = ${JSON.stringify(jsonValue)};`); + } + + requirePackage(packageName) { + this.packageNames.push(packageName); + } + + copyStream(sourceVar, targetVar, colmapVar = null) { + if (colmapVar) { + this._put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar}, {columns: ${colmapVar}});`); + } else { + this._put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`); + } + } + + comment(s) { + this._put(`// ${s}`); + } + + getScript(schedule = null) { + const packageNames = this.packageNames; + let prefix = _uniq(packageNames) + .map(packageName => `// @require ${packageName}\n`) + .join(''); + if (schedule) prefix += `// @schedule ${schedule}`; + if (prefix) prefix += '\n'; + + return prefix + this.s; + } +} + +export class ScriptWriterJson { + s = ''; + packageNames: string[] = []; + varCount = 0; + commands = []; + + constructor(varCount = '0') { + this.varCount = parseInt(varCount) || 0; + } + + allocVariable(prefix = 'var') { + this.varCount += 1; + return `${prefix}${this.varCount}`; + } + + endLine() { + this.commands.push({ + type: 'endline', + }); + } + + assign(variableName, functionName, props) { + this.commands.push({ + type: 'assign', + variableName, + functionName: extractShellApiFunctionName(functionName), + props, + }); + + this.packageNames.push(...extractShellApiPlugins(functionName, props)); + } + + assignValue(variableName, jsonValue) { + this.commands.push({ + type: 'assignValue', + variableName, + jsonValue, + }); + } + + copyStream(sourceVar, targetVar, colmapVar = null) { + this.commands.push({ + type: 'copyStream', + sourceVar, + targetVar, + colmapVar, + }); + } + + comment(text) { + this.commands.push({ + type: 'comment', + text, + }); + } + + getScript(schedule = null) { + return { + type: 'json', + schedule, + commands: this.commands, + packageNames: this.packageNames, + }; + } +} + +export function jsonScriptToJavascript(json) { + const { schedule, commands, packageNames } = json; + const script = new ScriptWriter(); + for (const packageName of packageNames) { + if (!/^dbgate-plugin-.*$/.test(packageName)) { + throw new Error('Unallowed package name:' + packageName); + } + script.packageNames.push(packageName); + } + + for (const cmd of commands) { + switch (cmd.type) { + case 'assign': + script.assignCore(cmd.variableName, cmd.functionName, cmd.props); + break; + case 'assignValue': + script.assignValue(cmd.variableName, cmd.jsonValue); + break; + case 'copyStream': + script.copyStream(cmd.sourceVar, cmd.targetVar, cmd.colmapVar); + break; + case 'endLine': + script.endLine(); + break; + case 'comment': + script.comment(cmd.text); + break; + } + } + + return script.getScript(schedule); +} diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 2ce79b5c1..71f0d0833 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -17,3 +17,4 @@ export * from './yamlModelConv'; export * from './stringTools'; export * from './computeDiffRows'; export * from './preloadedRowsTools'; +export * from './ScriptWriter'; diff --git a/packages/web/src/impexp/ScriptWriter.ts b/packages/web/src/impexp/ScriptWriter.ts deleted file mode 100644 index 53dd3b6f5..000000000 --- a/packages/web/src/impexp/ScriptWriter.ts +++ /dev/null @@ -1,58 +0,0 @@ -import _ from 'lodash'; -import { extractShellApiFunctionName, extractShellApiPlugins } from 'dbgate-tools'; - -export default class ScriptWriter { - s = ''; - packageNames: string[] = []; - varCount = 0; - - constructor(varCount = '0') { - this.varCount = parseInt(varCount) || 0; - } - - allocVariable(prefix = 'var') { - this.varCount += 1; - return `${prefix}${this.varCount}`; - } - - put(s = '') { - this.s += s; - this.s += '\n'; - } - - assign(variableName, functionName, props) { - this.put(`const ${variableName} = await ${extractShellApiFunctionName(functionName)}(${JSON.stringify(props)});`); - this.packageNames.push(...extractShellApiPlugins(functionName, props)); - } - - assignValue(variableName, jsonValue) { - this.put(`const ${variableName} = ${JSON.stringify(jsonValue)};`); - } - - requirePackage(packageName) { - this.packageNames.push(packageName); - } - - copyStream(sourceVar, targetVar, colmapVar = null) { - if (colmapVar) { - this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar}, {columns: ${colmapVar}});`); - } else { - this.put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`); - } - } - - comment(s) { - this.put(`// ${s}`); - } - - getScript(schedule = null) { - const packageNames = this.packageNames; - let prefix = _.uniq(packageNames) - .map(packageName => `// @require ${packageName}\n`) - .join(''); - if (schedule) prefix += `// @schedule ${schedule}`; - if (prefix) prefix += '\n'; - - return prefix + this.s; - } -} diff --git a/packages/web/src/impexp/createImpExpScript.ts b/packages/web/src/impexp/createImpExpScript.ts index 8677b235c..5e8f51940 100644 --- a/packages/web/src/impexp/createImpExpScript.ts +++ b/packages/web/src/impexp/createImpExpScript.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import ScriptWriter from './ScriptWriter'; +import { ScriptWriter, ScriptWriterJson } from 'dbgate-tools'; import getAsArray from '../utility/getAsArray'; import { getConnectionInfo } from '../utility/metadataLoaders'; import { findEngineDriver, findObjectLike } from 'dbgate-tools'; @@ -187,8 +187,12 @@ export function normalizeExportColumnMap(colmap) { return null; } -export default async function createImpExpScript(extensions, values, addEditorInfo = true) { - const script = new ScriptWriter(values.startVariableIndex || 0); +export default async function createImpExpScript(extensions, values, addEditorInfo = true, forceScript = false) { + const config = getCurrentConfig(); + const script = + config.allowShellScripting || forceScript + ? new ScriptWriter(values.startVariableIndex || 0) + : new ScriptWriterJson(values.startVariableIndex || 0); const [sourceConnection, sourceDriver] = await getConnection( extensions, @@ -222,7 +226,7 @@ export default async function createImpExpScript(extensions, values, addEditorIn } script.copyStream(sourceVar, targetVar, colmapVar); - script.put(); + script.endLine(); } if (addEditorInfo) { script.comment('@ImportExportConfigurator'); diff --git a/packages/web/src/modals/ImportExportModal.svelte b/packages/web/src/modals/ImportExportModal.svelte index fa8203401..37777c822 100644 --- a/packages/web/src/modals/ImportExportModal.svelte +++ b/packages/web/src/modals/ImportExportModal.svelte @@ -16,7 +16,7 @@ import { getDefaultFileFormat } from '../plugins/fileformats'; import RunnerOutputFiles from '../query/RunnerOutputFiles.svelte'; import SocketMessageView from '../query/SocketMessageView.svelte'; - import { currentArchive, currentDatabase, extensions, selectedWidget } from '../stores'; + import { currentArchive, currentDatabase, extensions, getCurrentConfig, selectedWidget } from '../stores'; import { apiCall, apiOff, apiOn } from '../utility/api'; import createRef from '../utility/createRef'; import openNewTab from '../utility/openNewTab'; @@ -90,7 +90,7 @@ const handleGenerateScript = async e => { closeCurrentModal(); - const code = await createImpExpScript($extensions, e.detail); + const code = await createImpExpScript($extensions, e.detail, undefined, true); openNewTab( { title: 'Shell #', @@ -108,7 +108,7 @@ const script = await createImpExpScript($extensions, values); executeNumber += 1; let runid = runnerId; - const resp = await apiCall('runners/start', { script, isGeneratedScript: true }); + const resp = await apiCall('runners/start', { script }); runid = resp.runid; runnerId = runid; diff --git a/packages/web/src/tabs/ShellTab.svelte b/packages/web/src/tabs/ShellTab.svelte index fe4089a4c..d97ccaf3f 100644 --- a/packages/web/src/tabs/ShellTab.svelte +++ b/packages/web/src/tabs/ShellTab.svelte @@ -12,6 +12,7 @@ execute: true, toggleComment: true, findReplace: true, + executeAdditionalCondition: () => getCurrentConfig().allowShellScripting, }); registerCommand({ @@ -51,6 +52,7 @@ import AceEditor from '../query/AceEditor.svelte'; import RunnerOutputPane from '../query/RunnerOutputPane.svelte'; import useEditorData from '../query/useEditorData'; + import { getCurrentConfig } from '../stores'; import { apiCall, apiOff, apiOn } from '../utility/api'; import { copyTextToClipboard } from '../utility/clipboard'; import { changeTab } from '../utility/common'; @@ -177,6 +179,11 @@ const resp = await apiCall('runners/start', { script: getActiveScript(), }); + if (resp.errorMessage) { + showSnackbarError(resp.errorMessage); + return; + } + runid = resp.runid; runnerId = runid; busy = true; diff --git a/packages/web/src/utility/exportFileTools.ts b/packages/web/src/utility/exportFileTools.ts index 6386f24f5..93e753df0 100644 --- a/packages/web/src/utility/exportFileTools.ts +++ b/packages/web/src/utility/exportFileTools.ts @@ -1,9 +1,10 @@ -import ScriptWriter from '../impexp/ScriptWriter'; +import { ScriptWriter, ScriptWriterJson } from 'dbgate-tools'; import getElectron from './getElectron'; import { showSnackbar, showSnackbarInfo, showSnackbarError, closeSnackbar } from '../utility/snackbar'; import resolveApi from './resolveApi'; import { apiCall, apiOff, apiOn } from './api'; import { normalizeExportColumnMap } from '../impexp/createImpExpScript'; +import { getCurrentConfig } from '../stores'; export async function exportQuickExportFile(dataName, reader, format, columnMap = null) { const electron = getElectron(); @@ -25,7 +26,7 @@ export async function exportQuickExportFile(dataName, reader, format, columnMap if (!filePath) return; - const script = new ScriptWriter(); + const script = getCurrentConfig().allowShellScripting ? new ScriptWriter() : new ScriptWriterJson(); const sourceVar = script.allocVariable(); script.assign(sourceVar, reader.functionName, reader.props); @@ -42,9 +43,9 @@ export async function exportQuickExportFile(dataName, reader, format, columnMap } script.copyStream(sourceVar, targetVar, colmapVar); - script.put(); + script.endLine(); - const resp = await apiCall('runners/start', { script: script.getScript(), isGeneratedScript: true }); + const resp = await apiCall('runners/start', { script: script.getScript() }); const runid = resp.runid; let isCanceled = false;