@processmaker/screen-builder 3.8.11 → 3.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@processmaker/screen-builder",
3
- "version": "3.8.11",
3
+ "version": "3.8.13",
4
4
  "scripts": {
5
5
  "dev": "VITE_COVERAGE=true vite",
6
6
  "build": "vite build",
package/src/bootstrap.js CHANGED
@@ -50,6 +50,9 @@ const cacheEnabled = document.head.querySelector(
50
50
  const cacheTimeout = document.head.querySelector(
51
51
  "meta[name='screen-cache-timeout']"
52
52
  );
53
+ const secureHandlerToggleVisibleMeta = document.head.querySelector(
54
+ "meta[name='screen-secure-handler-toggle-visible']"
55
+ );
53
56
 
54
57
  // Get the current protocol, hostname, and port
55
58
  const { protocol, hostname, port } = window.location;
@@ -73,7 +76,10 @@ window.ProcessMaker = {
73
76
  alert(message, variant) {},
74
77
  screen: {
75
78
  cacheEnabled: cacheEnabled ? cacheEnabled.content === "true" : false,
76
- cacheTimeout: cacheTimeout ? Number(cacheTimeout.content) : 0
79
+ cacheTimeout: cacheTimeout ? Number(cacheTimeout.content) : 0,
80
+ secureHandlerToggleVisible: !!Number(
81
+ secureHandlerToggleVisibleMeta?.content
82
+ )
77
83
  }
78
84
  };
79
85
  window.Echo = {
@@ -1,9 +1,21 @@
1
1
  export const handlerEventProperty = {
2
- type: 'CodeEditor',
3
- field: 'handler',
2
+ type: "CodeEditor",
3
+ field: "handler",
4
4
  config: {
5
- label: 'Click Handler',
6
- helper: 'The handler is a JavaScript function that will be executed when the button is clicked.',
7
- dataFeature: 'i1177',
8
- },
5
+ label: "Click Handler",
6
+ helper:
7
+ "The handler is a JavaScript function that will be executed when the button is clicked.",
8
+ dataFeature: "i1177"
9
+ }
10
+ };
11
+
12
+ export const handlerSecurityProperty = {
13
+ type: "FormCheckbox",
14
+ field: "handlerSecurityEnabled",
15
+ config: {
16
+ label: "Secure Handler Execution",
17
+ toggle: true,
18
+ helper:
19
+ "When enabled, the handler runs inside a sandboxed worker. Disable to allow full JavaScript access."
20
+ }
9
21
  };
@@ -2,12 +2,12 @@
2
2
  <div class="form-group" style="overflow-x: hidden">
3
3
  <button
4
4
  v-b-tooltip="options"
5
- @click="click"
6
5
  :class="classList"
7
6
  :name="name"
8
7
  :aria-label="$attrs['aria-label']"
9
8
  :tabindex="$attrs['tabindex']"
10
9
  :disabled="showSpinner"
10
+ @click="click"
11
11
  >
12
12
  <b-spinner v-if="showSpinner" small></b-spinner>
13
13
  {{ showSpinner ? (!loadingLabel ? "Loading..." : loadingLabel) : label }}
@@ -15,13 +15,16 @@
15
15
  </div>
16
16
  </template>
17
17
 
18
+ <!-- eslint-disable import/no-extraneous-dependencies -->
19
+ <!-- eslint-disable import/no-unresolved -->
20
+ <!-- eslint-disable import/extensions -->
18
21
  <script>
19
- import Mustache from 'mustache';
20
- import { mapActions, mapState } from "vuex";
21
- import { getValidPath } from '@/mixins';
22
+ import Mustache from "mustache";
23
+ import { mapState } from "vuex";
24
+ import { stringify } from "flatted";
25
+ import { getValidPath } from "@/mixins";
22
26
  import Worker from "@/workers/worker.js?worker&inline";
23
27
  import { findRootScreen } from "@/mixins/DataReference";
24
- import { stringify } from 'flatted';
25
28
 
26
29
  export default {
27
30
  mixins: [getValidPath],
@@ -37,7 +40,8 @@ export default {
37
40
  "transientData",
38
41
  "loading",
39
42
  "loadingLabel",
40
- "handler"
43
+ "handler",
44
+ "handlerSecurityEnabled"
41
45
  ],
42
46
  data() {
43
47
  return {
@@ -47,30 +51,35 @@ export default {
47
51
  computed: {
48
52
  ...mapState("globalErrorsModule", ["valid"]),
49
53
  classList() {
50
- let variant = this.variant || 'primary';
54
+ const variant = this.variant || "primary";
51
55
  return {
52
56
  btn: true,
53
- ['btn-' + variant]: true,
54
- disabled: this.event === 'submit' && !this.valid
57
+ [`btn-${variant}`]: true,
58
+ disabled: this.event === "submit" && !this.valid
55
59
  };
56
60
  },
57
61
  options() {
58
- if (!this.tooltip || this.event === 'submit') {
62
+ if (!this.tooltip || this.event === "submit") {
59
63
  return {};
60
64
  }
61
65
 
62
- let content = '';
66
+ let content = "";
63
67
  try {
64
- content = Mustache.render(this.tooltip.content || '', this.transientData);
65
- } catch (error) { error; }
68
+ content = Mustache.render(
69
+ this.tooltip.content || "",
70
+ this.transientData
71
+ );
72
+ } catch (error) {
73
+ console.error(error);
74
+ }
66
75
 
67
76
  return {
68
77
  title: content,
69
78
  html: true,
70
- placement: this.tooltip.position || '',
71
- trigger: 'hover',
72
- variant: this.tooltip.variant || '',
73
- boundary: 'window',
79
+ placement: this.tooltip.position || "",
80
+ trigger: "hover",
81
+ variant: this.tooltip.variant || "",
82
+ boundary: "window"
74
83
  };
75
84
  },
76
85
  buttonInfo() {
@@ -92,74 +101,150 @@ export default {
92
101
  }
93
102
  },
94
103
  async click() {
95
- if (this.event === 'script') {
96
- const trueValue = this.fieldValue || '1';
97
- const value = (this.value == trueValue) ? null : trueValue;
98
- this.$emit('input', value);
104
+ if (this.event === "script") {
105
+ const trueValue = this.fieldValue || "1";
106
+ // eslint-disable-next-line eqeqeq
107
+ const value = this.value == trueValue ? null : trueValue;
108
+ this.$emit("input", value);
99
109
  // Run handler after setting the value
100
110
  await this.runHandler();
101
111
  }
102
- if (this.event !== 'pageNavigate' && this.name) {
112
+ if (this.event !== "pageNavigate" && this.name) {
103
113
  this.setValue(this.$parent, this.name, this.fieldValue);
104
114
  }
105
- if (this.event === 'submit') {
115
+ if (this.event === "submit") {
106
116
  if (this.loading && this.valid) {
107
117
  this.showSpinner = true;
108
118
  }
109
- this.$emit('input', this.fieldValue);
119
+ this.$emit("input", this.fieldValue);
110
120
  // Run handler after setting the value
111
121
  await this.runHandler();
112
122
  this.$nextTick(() => {
113
- this.$emit('submit', this.eventData, this.loading, this.buttonInfo);
123
+ this.$emit("submit", this.eventData, this.loading, this.buttonInfo);
114
124
  });
115
125
  return;
116
126
  }
117
- if (this.event === 'pageNavigate') {
127
+ if (this.event === "pageNavigate") {
118
128
  // Run handler for page navigate
119
129
  await this.runHandler();
120
130
  }
121
131
  this.$emit(this.event, this.eventData);
122
- if (this.event === 'pageNavigate') {
123
- this.$emit('page-navigate', this.eventData);
132
+ if (this.event === "pageNavigate") {
133
+ this.$emit("page-navigate", this.eventData);
124
134
  }
125
135
  },
126
136
  runHandler() {
127
- if (this.handler) {
128
- return new Promise((resolve, reject) => {
129
- try {
130
- const rootScreen = findRootScreen(this);
131
- const data = rootScreen.vdata;
132
- const scope = this.transientData;
137
+ if (!this.handler) {
138
+ return Promise.resolve();
139
+ }
133
140
 
134
- const worker = new Worker();
135
- // Send the handler code to the worker
136
- worker.postMessage({
137
- fn: this.handler,
138
- dataRefs: stringify({data, scope}),
139
- });
141
+ const rootScreen = findRootScreen(this);
142
+ const data = rootScreen.vdata;
143
+ const scope = this.transientData;
140
144
 
141
- // Listen for the result from the worker
142
- worker.onmessage = (e) => {
143
- if (e.data.error) {
144
- reject(e.data.error);
145
- } else if (e.data.result) {
146
- // Update the data with the result
147
- Object.keys(e.data.result).forEach(key => {
148
- if (key === '_root') {
149
- Object.assign(data, e.data.result[key]);
150
- } else {
151
- scope[key] = e.data.result[key];
152
- }
153
- });
154
- resolve();
155
- }
156
- };
157
- } catch (error) {
158
- console.error("❌ There is an error in the button handler", error);
159
- }
160
- });
145
+ if (this.handlerSecurityEnabled === false) {
146
+ return this.executeHandlerWithoutWorker(data, scope);
161
147
  }
148
+
149
+ return this.executeHandlerWithWorker(data, scope);
150
+ },
151
+ executeHandlerWithWorker(data, scope) {
152
+ return new Promise((resolve, reject) => {
153
+ try {
154
+ const worker = new Worker();
155
+ worker.postMessage({
156
+ fn: this.handler,
157
+ dataRefs: stringify({ data, scope })
158
+ });
159
+
160
+ worker.onmessage = (e) => {
161
+ worker.terminate();
162
+ if (e.data.error) {
163
+ console.error(
164
+ "There is an error in the button handler",
165
+ e.data.error
166
+ );
167
+ reject(e.data.error);
168
+ return;
169
+ }
170
+ this.applyHandlerResult(e.data.result, data, scope);
171
+ resolve();
172
+ };
173
+
174
+ worker.onerror = (errorEvent) => {
175
+ worker.terminate();
176
+ console.error(
177
+ "There is an error in the button handler",
178
+ errorEvent
179
+ );
180
+ reject(errorEvent);
181
+ };
182
+ } catch (error) {
183
+ console.error("There is an error in the button handler", error);
184
+ reject(error);
185
+ }
186
+ });
187
+ },
188
+ executeHandlerWithoutWorker(data, scope) {
189
+ const hasDataReferenceHelper =
190
+ typeof this.getScreenDataReference === "function";
191
+ const dataReference = hasDataReferenceHelper
192
+ ? this.getScreenDataReference(null, (screen, name, value) => {
193
+ screen.$set(screen.vdata, name, value);
194
+ })
195
+ : data;
196
+ const parentReference =
197
+ hasDataReferenceHelper && dataReference
198
+ ? dataReference._parent
199
+ : undefined;
200
+ const context = scope || dataReference;
201
+ const toRaw = (item) => (item && item[Symbol.for("__v_raw")]) || item;
202
+ const functionBody = `return (async () => { ${this.handler} })();`;
203
+
204
+ try {
205
+ // eslint-disable-next-line no-new-func, max-len
206
+ 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.
207
+ const result = userFunc.apply(context, [
208
+ dataReference,
209
+ parentReference,
210
+ toRaw
211
+ ]);
212
+ return this.resolveHandlerResult(result, data, scope);
213
+ } catch (error) {
214
+ console.error("There is an error in the button handler", error);
215
+ return Promise.reject(error);
216
+ }
217
+ },
218
+ resolveHandlerResult(result, data, scope) {
219
+ if (result && typeof result.then === "function") {
220
+ return result
221
+ .then((resolved) => {
222
+ this.applyHandlerResult(resolved, data, scope);
223
+ })
224
+ .catch((error) => {
225
+ console.error("There is an error in the button handler", error);
226
+ throw error;
227
+ });
228
+ }
229
+
230
+ this.applyHandlerResult(result, data, scope);
231
+ return Promise.resolve();
232
+ },
233
+ applyHandlerResult(result, data, scope) {
234
+ if (!result || typeof result !== "object") {
235
+ return;
236
+ }
237
+
238
+ const targetScope = scope || this.transientData || {};
239
+
240
+ Object.keys(result).forEach((key) => {
241
+ if (key === "_root") {
242
+ Object.assign(data, result[key]);
243
+ } else {
244
+ this.$set(targetScope, key, result[key]);
245
+ }
246
+ });
162
247
  }
163
- },
248
+ }
164
249
  };
165
250
  </script>
@@ -531,6 +531,7 @@ export default {
531
531
  const url = `?user_id=${this.userId}&status=ACTIVE&process_request_id=${requestId}&include_sub_tasks=1${timestamp}`;
532
532
  return this.$dataProvider
533
533
  .getTasks(url).then((response) => {
534
+ this.$emit("load-data-task", response);
534
535
  if (response.data.data.length > 0) {
535
536
  let task = response.data.data[0];
536
537
  if (task.process_request_id !== this.requestId) {
@@ -810,6 +810,13 @@ export default {
810
810
  },
811
811
  showToolbar() {
812
812
  return this.screenType === formTypes.form;
813
+ },
814
+ secureHandlerToggleVisible() {
815
+ return _.get(
816
+ globalObject,
817
+ "ProcessMaker.screen.secureHandlerToggleVisible",
818
+ false
819
+ );
813
820
  }
814
821
  },
815
822
  watch: {
@@ -1220,6 +1227,13 @@ export default {
1220
1227
  (control) => control.component === this.inspection.component
1221
1228
  ) || { inspector: [] };
1222
1229
  return control.inspector.filter((input) => {
1230
+ if (
1231
+ !this.secureHandlerToggleVisible &&
1232
+ typeof input === "object" &&
1233
+ input.field === "handlerSecurityEnabled"
1234
+ ) {
1235
+ return false;
1236
+ }
1223
1237
  if (accordionFields.includes(input.field)) {
1224
1238
  return true;
1225
1239
  }
@@ -14,7 +14,7 @@ import FormListTable from './components/renderer/form-list-table';
14
14
  import FormAnalyticsChart from "./components/renderer/form-analytics-chart";
15
15
  import FormCollectionRecordControl from './components/renderer/form-collection-record-control.vue';
16
16
  import FormCollectionViewControl from './components/renderer/form-collection-view-control.vue';
17
- import { handlerEventProperty } from './components/inspector/button/handler-event-property';
17
+ import { handlerEventProperty, handlerSecurityProperty } from './components/inspector/button/handler-event-property';
18
18
  import {DataTypeProperty, DataFormatProperty, DataTypeDateTimeProperty} from './VariableDataTypeProperties';
19
19
  import {
20
20
  FormInput,
@@ -763,6 +763,7 @@ export default [
763
763
  fieldValue: null,
764
764
  tooltip: {},
765
765
  handler: '',
766
+ handlerSecurityEnabled: true,
766
767
  },
767
768
  inspector: [
768
769
  {
@@ -786,6 +787,7 @@ export default [
786
787
  },
787
788
  buttonTypeEvent,
788
789
  handlerEventProperty,
790
+ handlerSecurityProperty,
789
791
  LoadingSubmitButtonProperty,
790
792
  LabelSubmitButtonProperty,
791
793
  tooltipProperty,
package/src/main.js CHANGED
@@ -152,6 +152,10 @@ const cacheEnabled = document.head.querySelector(
152
152
  const cacheTimeout = document.head.querySelector(
153
153
  "meta[name='screen-cache-timeout']"
154
154
  );
155
+ const secureHandlerToggleVisibleMeta = document.head.querySelector(
156
+ "meta[name='screen-secure-handler-toggle-visible']"
157
+ );
158
+
155
159
  // Get the current protocol, hostname, and port
156
160
  const { protocol, hostname, port } = window.location;
157
161
  window.ProcessMaker = {
@@ -302,7 +306,10 @@ window.ProcessMaker = {
302
306
  },
303
307
  screen: {
304
308
  cacheEnabled: cacheEnabled ? cacheEnabled.content === "true" : false,
305
- cacheTimeout: cacheTimeout ? Number(cacheTimeout.content) : 0
309
+ cacheTimeout: cacheTimeout ? Number(cacheTimeout.content) : 0,
310
+ secureHandlerToggleVisible: !!Number(
311
+ secureHandlerToggleVisibleMeta?.content
312
+ )
306
313
  }
307
314
  };
308
315
  window.Echo = {
@@ -55,11 +55,19 @@ export default {
55
55
  componentName === "FormTextArea" ||
56
56
  componentName === "FormInput"
57
57
  ) {
58
- properties["@input"] = `updateScreenData('${safeDotName}', '${element.config.name}')`;
59
- properties["@change"] = `updateScreenDataNow('${safeDotName}', '${element.config.name}')`;
58
+ properties[
59
+ "@input"
60
+ ] = `updateScreenData('${safeDotName}', '${element.config.name}')`;
61
+ properties[
62
+ "@change"
63
+ ] = `updateScreenDataNow('${safeDotName}', '${element.config.name}')`;
60
64
  } else {
61
- properties["@input"] = `updateScreenDataNow('${safeDotName}', '${element.config.name}')`;
62
- properties["@change"] = `updateScreenDataNow('${safeDotName}', '${element.config.name}')`;
65
+ properties[
66
+ "@input"
67
+ ] = `updateScreenDataNow('${safeDotName}', '${element.config.name}')`;
68
+ properties[
69
+ "@change"
70
+ ] = `updateScreenDataNow('${safeDotName}', '${element.config.name}')`;
63
71
  }
64
72
  // Process the FormSelectList@reset event
65
73
  properties[
@@ -100,12 +108,17 @@ export default {
100
108
  properties[":readonly"] = isCalcProp || element.config.readonly;
101
109
  properties[":disabled"] = isCalcProp || element.config.disabled;
102
110
  // Events
103
- properties['@submit'] = 'submitForm';
111
+ properties["@submit"] = "submitForm";
104
112
  // Add handler event if Button
105
- if(componentName === 'FormButton') {
106
- properties[':handler'] = this.byRef(element.config.handler);
113
+ if (componentName === "FormButton") {
114
+ properties[":handler"] = this.byRef(element.config.handler);
115
+ const handlerSecurity =
116
+ element.config.handlerSecurityEnabled === undefined
117
+ ? true
118
+ : element.config.handlerSecurityEnabled;
119
+ properties[":handler-security-enabled"] = this.byRef(handlerSecurity);
107
120
  }
108
- },
121
+ }
109
122
  },
110
123
  mounted() {
111
124
  this.extensions.push({