Skip to content

Commit 0710efb

Browse files
committed
fix: sanitize and calulate expressionbs
1 parent 4d0c496 commit 0710efb

File tree

2 files changed

+170
-155
lines changed

2 files changed

+170
-155
lines changed

demo/index.html

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
<!DOCTYPE html>
22
<html lang="en">
3-
<head>
4-
<title>Calculate | CoCreateJS</title>
5-
<!-- CoCreate Favicon -->
6-
<link
7-
rel="icon"
8-
type="image/png"
9-
sizes="32x32"
10-
href="../assets/favicon.ico" />
11-
<link rel="manifest" href="/manifest.webmanifest" />
12-
</head>
13-
<body>
14-
<input class="we" key="total" id="id1" value="12" />
15-
<input class="we" key="total" id="id2" value="13" />
16-
<!--<input id="id3" value="14"><br><br><br>-->
3+
<head>
4+
<title>Calculate | CoCreateJS</title>
5+
<!-- CoCreate Favicon -->
6+
<link
7+
rel="icon"
8+
type="image/png"
9+
sizes="32x32"
10+
href="../assets/favicon.ico" />
11+
<link rel="manifest" href="/manifest.webmanifest" />
12+
</head>
13+
<body>
14+
<input value="12" calculate="* 100" />
15+
<input class="we" key="total" id="id1" value="12" />
16+
<input class="we" key="total" id="id2" value="13" />
17+
<!--<input id="id3" value="14"><br><br><br>-->
1718

18-
<input id="te" calculate="{(#id1)} + {(#id2)}" />
19-
<!--<input calculate="1 + 3 * 5">-->
20-
<input calculate="{[key='total']} + 1" />
21-
<h1 calculate="{[key='total']} + {(#te)}">sum</h1>
19+
<input id="te" calculate="{(#id1)} + {(#id2)}" />
20+
<!--<input calculate="1 + 3 * 5">-->
21+
<input calculate="{[key='total']} + 1" />
22+
<h1 calculate="{[key='total']} + {(#te)}">sum</h1>
2223

23-
<!--<h1 calculate="sum[.we]">sum</h1>-->
24-
<!--<h1 calculate="1 + {(#id1)} * 5 + {(#id3)}/{(#id2)} + {(#id2)}"></h1>-->
25-
<!--<h1 calculate="(({#id1} + {#id2})) * {#id3}"></h1>-->
24+
<!--<h1 calculate="sum[.we]">sum</h1>-->
25+
<!--<h1 calculate="1 + {(#id1)} * 5 + {(#id3)}/{(#id2)} + {(#id2)}"></h1>-->
26+
<!--<h1 calculate="(({#id1} + {#id2})) * {#id3}"></h1>-->
2627

27-
<!--<script src="../dist/CoCreate-calculate.js"></script>-->
28-
<script src="https://CoCreate.app/dist/CoCreate.js"></script>
29-
</body>
28+
<!--<script src="../dist/CoCreate-calculate.js"></script>-->
29+
<script src="https://CoCreate.app/dist/CoCreate.js"></script>
30+
</body>
3031
</html>

src/index.js

Lines changed: 145 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,180 @@
1-
import observer from '@cocreate/observer';
2-
import { getAttributes } from '@cocreate/utils';
1+
import observer from "@cocreate/observer";
2+
import { getAttributes } from "@cocreate/utils";
33
// import { renderValue } from '@cocreate/render';
4-
import '@cocreate/element-prototype';
5-
4+
import "@cocreate/element-prototype";
65

76
function init() {
8-
let calculateElements = document.querySelectorAll('[calculate]');
9-
initElements(calculateElements);
7+
let calculateElements = document.querySelectorAll("[calculate]");
8+
initElements(calculateElements);
109
}
1110

1211
function initElements(elements) {
13-
for (let el of elements)
14-
initElement(el);
12+
for (let el of elements) initElement(el);
1513
}
1614

1715
function initElement(element) {
18-
let calculate = element.getAttribute('calculate');
19-
if (calculate.includes('{{') || calculate.includes('{['))
20-
return;
21-
22-
let selectors = getSelectors(calculate);
23-
24-
for (let i = 0; i < selectors.length; i++) {
25-
// if (selectors[i].includes('{{')) return;
26-
27-
// initEvents(element, selectors[i]);
28-
let inputs = document.querySelectorAll(selectors[i]);
29-
for (let input of inputs) {
30-
initEvent(element, input);
31-
}
32-
33-
observer.init({
34-
name: 'calculateSelectorInit',
35-
observe: ['addedNodes'],
36-
selector: selectors[i],
37-
callback(mutation) {
38-
initEvent(element, mutation.target);
39-
setCalcationResult(element);
40-
}
41-
});
42-
}
43-
setCalcationResult(element);
16+
let calculate = element.getAttribute("calculate");
17+
if (calculate.includes("{{") || calculate.includes("{[")) return;
18+
19+
let selectors = getSelectors(calculate);
20+
21+
for (let i = 0; i < selectors.length; i++) {
22+
// if (selectors[i].includes('{{')) return;
23+
24+
// initEvents(element, selectors[i]);
25+
let inputs = document.querySelectorAll(selectors[i]);
26+
for (let input of inputs) {
27+
initEvent(element, input);
28+
}
29+
30+
observer.init({
31+
name: "calculateSelectorInit",
32+
observe: ["addedNodes"],
33+
selector: selectors[i],
34+
callback(mutation) {
35+
initEvent(element, mutation.target);
36+
setCalcationResult(element);
37+
}
38+
});
39+
}
40+
setCalcationResult(element);
4441
}
4542

4643
function getSelectors(string) {
47-
let regex = /\{\((?:(?!\{\().)*?\)\}/;
48-
let selectors = [];
44+
let regex = /\{\((?:(?!\{\().)*?\)\}/;
45+
let selectors = [];
4946

50-
let match;
51-
while ((match = regex.exec(string)) !== null) {
52-
// Extract the content inside the braces (excluding the leading '{(' and trailing ')}')
53-
let selector = match[0].slice(2, -2);
47+
let match;
48+
while ((match = regex.exec(string)) !== null) {
49+
// Extract the content inside the braces (excluding the leading '{(' and trailing ')}')
50+
let selector = match[0].slice(2, -2);
5451

55-
if (selectors.indexOf(selector) === -1) {
56-
selectors.push(selector);
57-
}
52+
if (selectors.indexOf(selector) === -1) {
53+
selectors.push(selector);
54+
}
5855

59-
// Replace the found match with an empty string to avoid reprocessing in the next iteration
60-
string = string.replace(match[0], '');
61-
}
56+
// Replace the found match with an empty string to avoid reprocessing in the next iteration
57+
string = string.replace(match[0], "");
58+
}
6259

63-
return selectors;
60+
return selectors;
6461
}
6562

6663
async function getValues(calculate) {
67-
let selectors = getSelectors(calculate);
68-
69-
for (let i = 0; i < selectors.length; i++) {
70-
let selector = selectors[i];
71-
72-
let value = null;
73-
let inputs = document.querySelectorAll(selector);
74-
75-
for (let input of inputs) {
76-
let val = null;
77-
if (input.getValue)
78-
val = Number(await input.getValue());
79-
80-
if (!Number.isNaN(value)) {
81-
value += val;
82-
}
83-
}
84-
85-
if (value != null && !Number.isNaN(value)) {
86-
calculate = calculate.replaceAll('{(' + selector + ')}', value);
87-
}
88-
}
89-
90-
return calculate;
64+
let selectors = getSelectors(calculate);
65+
66+
for (let selector of selectors) {
67+
let value = 0; // Default to 0 for missing inputs
68+
let inputs = document.querySelectorAll(selector);
69+
70+
for (let input of inputs) {
71+
let val = null;
72+
if (input.getValue) {
73+
val = Number(await input.getValue());
74+
}
75+
76+
if (!Number.isNaN(val)) {
77+
value += val; // Accumulate valid numeric values
78+
} else {
79+
console.warn(
80+
`Invalid value for selector "${selector}". Defaulting to 0.`
81+
);
82+
}
83+
}
84+
85+
calculate = calculate.replaceAll(`{(${selector})}`, value);
86+
}
87+
88+
return calculate;
9189
}
92-
9390
function initEvent(element, input) {
94-
input.addEventListener('input', function () {
95-
setCalcationResult(element);
96-
});
97-
98-
// if (input.hasAttribute('calculate')) {
99-
// input.addEventListener('changedCalcValue', function(e) {
100-
// setCalcationResult(element);
101-
// });
102-
// }
103-
// setCalcationResult(element);
91+
if (input._calculateInitialized) return; // Avoid duplicate listeners
92+
input._calculateInitialized = true;
93+
94+
input.addEventListener("input", function () {
95+
setCalcationResult(element);
96+
});
10497
}
10598

10699
async function setCalcationResult(element) {
107-
const { object, isRealtime } = getAttributes(element);
108-
109-
let calString = await getValues(element.getAttribute('calculate'));
110-
111-
if (calString) {
112-
let result = calculate(calString);
113-
114-
// TODO: input event below triggers save for all input elements but will not save for regular elements
115-
if (element.setValue) {
116-
element.setValue(result)
117-
if (object && isRealtime && isRealtime !== "false") {
118-
element.save(element);
119-
}
120-
}
121-
else {
122-
// if (element.value){
123-
124-
// }
125-
// else {
126-
element.innerHTML = result;
127-
// }
128-
}
129-
130-
let inputEvent = new CustomEvent('input', { bubbles: true });
131-
Object.defineProperty(inputEvent, 'target', { writable: false, value: element });
132-
element.dispatchEvent(inputEvent);
133-
134-
//. set custom event
135-
// var event = new CustomEvent('changedCalcValue', {
136-
// bubbles: true,
137-
// });
138-
// element.dispatchEvent(event);
139-
}
140-
100+
const { object, isRealtime } = getAttributes(element);
101+
102+
let calString = await getValues(element.getAttribute("calculate"));
103+
104+
if (calString) {
105+
let result = calculate(calString);
106+
107+
if (element.setValue) {
108+
element.setValue(result);
109+
if (object && isRealtime && isRealtime !== "false") {
110+
element.save(element);
111+
}
112+
} else {
113+
element.innerHTML = result;
114+
}
115+
116+
let inputEvent = new CustomEvent("input", { bubbles: true });
117+
Object.defineProperty(inputEvent, "target", {
118+
writable: false,
119+
value: element
120+
});
121+
element.dispatchEvent(inputEvent);
122+
}
141123
}
142124

143-
function calculate(string) {
144-
if (/^[0-9+\-*/()%||?\s:=.]*$/.test(string))
145-
return eval(string);
125+
function calculate(expression) {
126+
try {
127+
// Sanitize the expression to allow only valid characters
128+
const sanitizedExpression = expression.replace(
129+
/[^a-zA-Z0-9+\-*/()%.\s]/g,
130+
""
131+
);
132+
133+
// Extract identifiers (e.g., Math.round, Date.now) using regex
134+
const identifierRegex =
135+
/\b[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?\b/g;
136+
const matches = sanitizedExpression.match(identifierRegex) || [];
137+
138+
// Validate each identifier
139+
for (const match of matches) {
140+
const [object, method] = match.split(".");
141+
142+
// Validate allowed objects and methods
143+
if (
144+
(object === "Math" && (!method || method in Math)) ||
145+
(object === "Number" && (!method || method in Number))
146+
) {
147+
// Valid identifier
148+
continue;
149+
}
150+
151+
throw new Error(`Invalid identifier: ${match}`);
152+
}
153+
154+
// Safely evaluate the sanitized expression
155+
return new Function(`return (${sanitizedExpression})`)();
156+
} catch (error) {
157+
console.error("Error evaluating expression:", error.message);
158+
return null;
159+
}
146160
}
147161

148162
observer.init({
149-
name: 'CoCreateCalculateChangeValue',
150-
observe: ['attributes'],
151-
attributeName: ['calculate'],
152-
callback(mutation) {
153-
setCalcationResult(mutation.target);
154-
}
163+
name: "CoCreateCalculateChangeValue",
164+
observe: ["attributes"],
165+
attributeName: ["calculate"],
166+
callback(mutation) {
167+
setCalcationResult(mutation.target);
168+
}
155169
});
156170

157171
observer.init({
158-
name: 'CoCreateCalculateInit',
159-
observe: ['addedNodes'],
160-
selector: '[calculate]',
161-
callback(mutation) {
162-
initElement(mutation.target);
163-
}
172+
name: "CoCreateCalculateInit",
173+
observe: ["addedNodes"],
174+
selector: "[calculate]",
175+
callback(mutation) {
176+
initElement(mutation.target);
177+
}
164178
});
165179

166180
init();

0 commit comments

Comments
 (0)