Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const cacheEnabled = document.head.querySelector(
const cacheTimeout = document.head.querySelector(
"meta[name='screen-cache-timeout']"
);
const secureHandlerToggleVisibleMeta = document.head.querySelector(
"meta[name='screen-secure-handler-toggle-visible']"
);

// Get the current protocol, hostname, and port
const { protocol, hostname, port } = window.location;
Expand All @@ -73,7 +76,10 @@ window.ProcessMaker = {
alert(message, variant) {},
screen: {
cacheEnabled: cacheEnabled ? cacheEnabled.content === "true" : false,
cacheTimeout: cacheTimeout ? Number(cacheTimeout.content) : 0
cacheTimeout: cacheTimeout ? Number(cacheTimeout.content) : 0,
secureHandlerToggleVisible: !!Number(
secureHandlerToggleVisibleMeta?.content
)
}
};
window.Echo = {
Expand Down
24 changes: 18 additions & 6 deletions src/components/inspector/button/handler-event-property.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
export const handlerEventProperty = {
type: 'CodeEditor',
field: 'handler',
type: "CodeEditor",
field: "handler",
config: {
label: 'Click Handler',
helper: 'The handler is a JavaScript function that will be executed when the button is clicked.',
dataFeature: 'i1177',
},
label: "Click Handler",
helper:
"The handler is a JavaScript function that will be executed when the button is clicked.",
dataFeature: "i1177"
}
};

export const handlerSecurityProperty = {
type: "FormCheckbox",
field: "handlerSecurityEnabled",
config: {
label: "Secure Handler Execution",
toggle: true,
helper:
"When enabled, the handler runs inside a sandboxed worker. Disable to allow full JavaScript access."
}
};
207 changes: 146 additions & 61 deletions src/components/renderer/form-button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@
<div class="form-group" style="overflow-x: hidden">
<button
v-b-tooltip="options"
@click="click"
:class="classList"
:name="name"
:aria-label="$attrs['aria-label']"
:tabindex="$attrs['tabindex']"
:disabled="showSpinner"
@click="click"
>
<b-spinner v-if="showSpinner" small></b-spinner>
{{ showSpinner ? (!loadingLabel ? "Loading..." : loadingLabel) : label }}
</button>
</div>
</template>

<!-- eslint-disable import/no-extraneous-dependencies -->
<!-- eslint-disable import/no-unresolved -->
<!-- eslint-disable import/extensions -->
<script>
import Mustache from 'mustache';
import { mapActions, mapState } from "vuex";
import { getValidPath } from '@/mixins';
import Mustache from "mustache";
import { mapState } from "vuex";
import { stringify } from "flatted";
import { getValidPath } from "@/mixins";
import Worker from "@/workers/worker.js?worker&inline";
import { findRootScreen } from "@/mixins/DataReference";
import { stringify } from 'flatted';

export default {
mixins: [getValidPath],
Expand All @@ -37,7 +40,8 @@ export default {
"transientData",
"loading",
"loadingLabel",
"handler"
"handler",
"handlerSecurityEnabled"
],
data() {
return {
Expand All @@ -47,30 +51,35 @@ export default {
computed: {
...mapState("globalErrorsModule", ["valid"]),
classList() {
let variant = this.variant || 'primary';
const variant = this.variant || "primary";
return {
btn: true,
['btn-' + variant]: true,
disabled: this.event === 'submit' && !this.valid
[`btn-${variant}`]: true,
disabled: this.event === "submit" && !this.valid
};
},
options() {
if (!this.tooltip || this.event === 'submit') {
if (!this.tooltip || this.event === "submit") {
return {};
}

let content = '';
let content = "";
try {
content = Mustache.render(this.tooltip.content || '', this.transientData);
} catch (error) { error; }
content = Mustache.render(
this.tooltip.content || "",
this.transientData
);
} catch (error) {
console.error(error);
}

return {
title: content,
html: true,
placement: this.tooltip.position || '',
trigger: 'hover',
variant: this.tooltip.variant || '',
boundary: 'window',
placement: this.tooltip.position || "",
trigger: "hover",
variant: this.tooltip.variant || "",
boundary: "window"
};
},
buttonInfo() {
Expand All @@ -92,74 +101,150 @@ export default {
}
},
async click() {
if (this.event === 'script') {
const trueValue = this.fieldValue || '1';
const value = (this.value == trueValue) ? null : trueValue;
this.$emit('input', value);
if (this.event === "script") {
const trueValue = this.fieldValue || "1";
// eslint-disable-next-line eqeqeq
const value = this.value == trueValue ? null : trueValue;
this.$emit("input", value);
// Run handler after setting the value
await this.runHandler();
}
if (this.event !== 'pageNavigate' && this.name) {
if (this.event !== "pageNavigate" && this.name) {
this.setValue(this.$parent, this.name, this.fieldValue);
}
if (this.event === 'submit') {
if (this.event === "submit") {
if (this.loading && this.valid) {
this.showSpinner = true;
}
this.$emit('input', this.fieldValue);
this.$emit("input", this.fieldValue);
// Run handler after setting the value
await this.runHandler();
this.$nextTick(() => {
this.$emit('submit', this.eventData, this.loading, this.buttonInfo);
this.$emit("submit", this.eventData, this.loading, this.buttonInfo);
});
return;
}
if (this.event === 'pageNavigate') {
if (this.event === "pageNavigate") {
// Run handler for page navigate
await this.runHandler();
}
this.$emit(this.event, this.eventData);
if (this.event === 'pageNavigate') {
this.$emit('page-navigate', this.eventData);
if (this.event === "pageNavigate") {
this.$emit("page-navigate", this.eventData);
}
},
runHandler() {
if (this.handler) {
return new Promise((resolve, reject) => {
try {
const rootScreen = findRootScreen(this);
const data = rootScreen.vdata;
const scope = this.transientData;
if (!this.handler) {
return Promise.resolve();
}

const worker = new Worker();
// Send the handler code to the worker
worker.postMessage({
fn: this.handler,
dataRefs: stringify({data, scope}),
});
const rootScreen = findRootScreen(this);
const data = rootScreen.vdata;
const scope = this.transientData;

// Listen for the result from the worker
worker.onmessage = (e) => {
if (e.data.error) {
reject(e.data.error);
} else if (e.data.result) {
// Update the data with the result
Object.keys(e.data.result).forEach(key => {
if (key === '_root') {
Object.assign(data, e.data.result[key]);
} else {
scope[key] = e.data.result[key];
}
});
resolve();
}
};
} catch (error) {
console.error("❌ There is an error in the button handler", error);
}
});
if (this.handlerSecurityEnabled === false) {
return this.executeHandlerWithoutWorker(data, scope);
}

return this.executeHandlerWithWorker(data, scope);
},
executeHandlerWithWorker(data, scope) {
return new Promise((resolve, reject) => {
try {
const worker = new Worker();
worker.postMessage({
fn: this.handler,
dataRefs: stringify({ data, scope })
});

worker.onmessage = (e) => {
worker.terminate();
if (e.data.error) {
console.error(
"There is an error in the button handler",
e.data.error
);
reject(e.data.error);
return;
}
this.applyHandlerResult(e.data.result, data, scope);
resolve();
};

worker.onerror = (errorEvent) => {
worker.terminate();
console.error(
"There is an error in the button handler",
errorEvent
);
reject(errorEvent);
};
} catch (error) {
console.error("There is an error in the button handler", error);
reject(error);
}
});
},
executeHandlerWithoutWorker(data, scope) {
const hasDataReferenceHelper =
typeof this.getScreenDataReference === "function";
const dataReference = hasDataReferenceHelper
? this.getScreenDataReference(null, (screen, name, value) => {
screen.$set(screen.vdata, name, value);
})
: data;
const parentReference =
hasDataReferenceHelper && dataReference
? dataReference._parent
: undefined;
const context = scope || dataReference;
const toRaw = (item) => (item && item[Symbol.for("__v_raw")]) || item;
const functionBody = `return (async () => { ${this.handler} })();`;

try {
// eslint-disable-next-line no-new-func, max-len
const userFunc = new Function("data", "parent", "toRaw", functionBody); // NOSONAR. This dynamic code execution is safe because it only occurs when the user has explicitly disabled the security worker.
const result = userFunc.apply(context, [
dataReference,
parentReference,
toRaw
]);
return this.resolveHandlerResult(result, data, scope);
} catch (error) {
console.error("There is an error in the button handler", error);
return Promise.reject(error);
}
},
resolveHandlerResult(result, data, scope) {
if (result && typeof result.then === "function") {
return result
.then((resolved) => {
this.applyHandlerResult(resolved, data, scope);
})
.catch((error) => {
console.error("There is an error in the button handler", error);
throw error;
});
}

this.applyHandlerResult(result, data, scope);
return Promise.resolve();
},
applyHandlerResult(result, data, scope) {
if (!result || typeof result !== "object") {
return;
}

const targetScope = scope || this.transientData || {};

Object.keys(result).forEach((key) => {
if (key === "_root") {
Object.assign(data, result[key]);
} else {
this.$set(targetScope, key, result[key]);
}
});
}
},
}
};
</script>
14 changes: 14 additions & 0 deletions src/components/vue-form-builder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,13 @@ export default {
},
showToolbar() {
return this.screenType === formTypes.form;
},
secureHandlerToggleVisible() {
return _.get(
globalObject,
"ProcessMaker.screen.secureHandlerToggleVisible",
false
);
}
},
watch: {
Expand Down Expand Up @@ -1220,6 +1227,13 @@ export default {
(control) => control.component === this.inspection.component
) || { inspector: [] };
return control.inspector.filter((input) => {
if (
!this.secureHandlerToggleVisible &&
typeof input === "object" &&
input.field === "handlerSecurityEnabled"
) {
return false;
}
if (accordionFields.includes(input.field)) {
return true;
}
Expand Down
4 changes: 3 additions & 1 deletion src/form-builder-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import FormListTable from './components/renderer/form-list-table';
import FormAnalyticsChart from "./components/renderer/form-analytics-chart";
import FormCollectionRecordControl from './components/renderer/form-collection-record-control.vue';
import FormCollectionViewControl from './components/renderer/form-collection-view-control.vue';
import { handlerEventProperty } from './components/inspector/button/handler-event-property';
import { handlerEventProperty, handlerSecurityProperty } from './components/inspector/button/handler-event-property';
import {DataTypeProperty, DataFormatProperty, DataTypeDateTimeProperty} from './VariableDataTypeProperties';
import {
FormInput,
Expand Down Expand Up @@ -763,6 +763,7 @@ export default [
fieldValue: null,
tooltip: {},
handler: '',
handlerSecurityEnabled: true,
},
inspector: [
{
Expand All @@ -786,6 +787,7 @@ export default [
},
buttonTypeEvent,
handlerEventProperty,
handlerSecurityProperty,
LoadingSubmitButtonProperty,
LabelSubmitButtonProperty,
tooltipProperty,
Expand Down
Loading
Loading