5
\$\begingroup\$

This is my first question on Code Review, so please let me know if I botched anything.

After working for quite a while on my CS50 calculator, it has finally reached the point where I am completely satisfied with how it works. However, since it is hosted on Google Sites, I'm slightly worried about security issues; I controversially chose to use eval since I'm too lazy to evaluate it myself. I'm worried this may lead to Javascript hacking.

The way my code works is it has a bunch of buttons to allow you to put in numbers, symbols, and predefined functions. Then, it adds different symbols to the actual displayed text and the evaluated text. This is so non-programmers can understand it better, e.g. ** vs ^. I also allow you to simply type in symbols, and I restrict it to buttonable ones. Finally, I evaluate what you've typed in using eval. You also have the ability to clear and delete.

There is a link to a working CS50 Calculator in my profile, and I'll also put a link here. You can try it out if my explanation was too vague: https://sites.google.com/view/cs50-calculator/home

And, of course, below is the full code of CS50 calculator (note that I'm not putting it in a snippet since you can access it on Google Sites anyways):

<!DOCTYPE html>
<!--Code help received from thingEvery in Stack Overflow: https://stackoverflow.com/a/63079750/13736952-->

<html lang="en">
    <head>
        <title>Calculator</title>
        <script>
            function sqrt(x)
            {
                return Math.sqrt(x);
            }
            function abs(x)
            {
                return Math.abs(x);
            }
            function sin(x)
            {
                return Math.sin(x);
            }
            function cos(x)
            {
                return Math.cos(x);
            }
            function tan(x)
            {
                return Math.tan(x);
            }
            function arcsin(x)
            {
                return Math.asin(x);
            }
            function arccos(x)
            {
                return Math.acos(x);
            }
            function arctan(x)
            {
                return Math.atan(x);
            }
            function ln(x)
            {
                return Math.log(x);
            }
            function log(x)
            {
                return Math.log10(x)
            }
            function cbrt(x)
            {
                return Math.cbrt(x);
            }
            function exp(x)
            {
                return Math.exp(x);
            }
            function root(index, radicand)
            {
                return radicand**(1/index);
            }
            function logrtm(base, argument)
            {
                return Math.log(argument)/Math.log(base);
            }
            function dec(x)
            {
                return parseFloat(x);
            }
            function int(x)
            {
                return Math.round(x);
            }
            function min(stuff)
            {
                let items = []
                if (arguments.length > 1)
                {
                    for (var i = 0; i < arguments.length; i++) items[i] = arguments[i];
                }
                else if (!isNaN(stuff))
                {
                    items = [stuff]
                }
                else
                {
                    items = stuff;
                }

                return Math.min(...items);
            }
            function max(stuff)
            {
                let items = []
                if (arguments.length > 1)
                {
                    for (var i = 0; i < arguments.length; i++) items[i] = arguments[i];
                }
                else if (!isNaN(stuff))
                {
                    items = [stuff]
                }
                else
                {
                    items = stuff;
                }

                return Math.max(...items);
            }
            function sum(stuff)
            {
                let items = []
                if (arguments.length > 1)
                {
                    for (var i = 0; i < arguments.length; i++) items[i] = arguments[i];
                }
                else if (!isNaN(stuff))
                {
                    items = [stuff]
                }
                else
                {
                    items = stuff;
                }

                let return_value = 0;

                for (let i = 0; i < items.length; i++)
                {
                    return_value += parseFloat(items[i]);
                }

                return return_value;
            }
            function mean(stuff)
            {
                let items = [];
                if (arguments.length > 1)
                {
                    for (var i = 0; i < arguments.length; i++) items[i] = arguments[i];
                }
                else if (!isNaN(stuff))
                {
                    items = [stuff]
                }
                else
                {
                    items = stuff;
                }

                return sum(items) / items.length;
            }
            function median(stuff)
            {
                let items = [];
                if (arguments.length > 1)
                {
                    for (var i = 0; i < arguments.length; i++) items[i] = arguments[i];
                }
                else if (!isNaN(stuff))
                {
                    items = [stuff]
                }
                else
                {
                    items = stuff;
                }

                items.sort(function(a, b){return Number(a) - Number(b);});

                var half = Math.floor(items.length / 2);

                if (half % 2 === 1)
                {
                    return items[half];
                }
                else
                {
                    return mean([items[half], items[half-1]]);
                }
            }
            function mode(stuff)
            {
                let items = [];
                if (arguments.length > 1)
                {
                    for (var i = 0; i < arguments.length; i++) items[i] = arguments[i];
                }
                else if (!isNaN(stuff))
                {
                    items = [stuff]
                }
                else
                {
                    items = stuff;
                }

                var mapping = {};
                for (let i = 0; i < items.length; i++)
                {
                    if (!mapping[items[i]])
                    {
                        mapping[items[i]] = 0;
                    }
                    mapping[items[i]] += 1;
                }

                var max_val = max(Object.values(mapping));
                var return_string = "";

                for (let item in mapping)
                {
                    if (mapping[item] === max_val)
                    {
                        if (return_string.length != 0)
                        {
                            return_string += " ";
                        }
                        return_string += item;
                    }
                }

                return return_string;
            }
            function range(stuff)
            {
                let items = [];
                if (arguments.length > 1)
                {
                    for (var i = 0; i < arguments.length; i++) items[i] = arguments[i];
                }
                else if (!isNaN(stuff))
                {
                    items = [stuff]
                }
                else
                {
                    items = stuff;
                }

                var max_val = max(items);
                var min_val = min(items);
                return max_val - min_val;
            }
            function factorial(x)
            {
                let return_num = 1;
                for (let i = 1; i < x + 1; i++)
                {
                    return_num *= i;
                }
                return return_num;
            }
            function combination(x, y)
            {
                return factorial(x) / (factorial(y) * factorial(x - y));
            }
            function random(low, high)
            {
                let return_number = Math.random() * (high - low) + low;
                return return_number;
            }
            function quad(a, b, c)
            {
                if ((b**2 - 4*a*c) > 0)
                {
                    let var1 = (-b + Math.sqrt(b**2 - 4*a*c)) / (2*a);
                    let var2 = (-b - Math.sqrt(b**2 - 4*a*c)) / (2*a);
                    let return_string = var1.toString() + " " + var2.toString();
                    return return_string;
                }
                else if ((b**2 - 4*a*c) == 0)
                {
                    return (-b / 2*a);
                }
                else if ((b**2 - 4*a*c) < 0)
                {
                    return "Undefined";
                }
            }
            var x = "x";
            function setx(stuff)
            {
                x = stuff;
                return 0;
            }
            var list = ["Empty"];
            function setlist()
            {
                list = [];
                for (var i = 0; i < arguments.length; i++) {
                    if (typeof arguments[i] == "number") {
                        list.push(arguments[i]);
                    }
                }
                if (list.length == 0) {
                    list = ["Empty"];
                }
                return 0;
            }
            function addtolist()
            {
                if (list.length == 1 && "Empty".localeCompare(list[0]) == 0) {
                    list = [];
                }
                for (var i = 0; i < arguments.length; i++) {
                    if (typeof arguments[i] == "number") {
                        list.push(arguments[i]);
                    }
                }
                if (list.length == 0) {
                    list = ["Empty"];
                }
                return 0;
            }
            var func = null;
            function makefunction(stuff)
            {
                func = stuff;
                return 0;
            }
            function f(y)
            {
                if (func !== null)
                    return eval(func);
                else
                    return "Undefined";
            }
            var pi = Math.PI;
            var e = Math.E;
            var percent = 0.01;
        </script>
        <style>
            table
            {
                border: 3px solid black; border-collapse: collapse;
            }

            .center
            {
                 margin: auto;
            }

            .num
            {
                color: black;
            }

            .oper
            {
                color: black;
            }

            .convert
            {
                color: black;
            }

            .operalt
            {
                color: black;
            }

            .numalt
            {
                color: black;
            }

            .convertalt
            {
                color: black;
            }

            .everything
            {
                margin: auto;
                border: 5px ridge midnightblue;
                background: gainsboro;
                height: 360px;
                width: 400px;
            }

            .buttonsdiv
            {
                text-align: center;
                border-width: 3px;
                border-style: ridge double;
                height: 30px;
            }

            button
            {
                background: bisque;
                border: 1px solid coral;
                transition-duration: 0.5s;
                cursor: pointer;
            }

            button:hover
            {
                background: yellow;
                border: 1px solid goldenrod;
            }

            .buttoncenter {
                margin: 4px auto;
                display:block;
            }
        </style>
    </head>
    <body>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <div style="margin: auto; border: 5px ridge midnightblue; background: gainsboro; height: 400px; width: 400px;">
            <div style="text-align: center; font-weight: bold; color: black;" id="expression">0</div>

            <div class="buttonsdiv" style="border-color: blue; width: 300px; margin: 0 12%;">
                <div class="buttoncenter">
                    <button class="num" id="1">1</button>
                    <button class="num" id="2">2</button>
                    <button class="num" id="3">3</button>
                    <button class="num" id="4">4</button>
                    <button class="num" id="5">5</button>
                    <button class="num" id="6">6</button>
                    <button class="num" id="7">7</button>
                    <button class="num" id="8">8</button>
                    <button class="num" id="9">9</button>
                    <button class="num" id="0">0</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: black; width: 180px; margin: 0% 101%; background: gainsboro;">
                <div class="buttoncenter">
                    <button id="enter">Enter</button>
                    <button id="clear">Clear</button>
                    <button id="delete">Delete</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: green; width: 135px; margin: -9% 12%;">
                <div class="buttoncenter">
                    <button class="oper" id="+">+</button>
                    <button class="oper" id="-">–</button>
                    <button class="operalt" id="*" name="×">×</button>
                    <button class="oper" id="/">÷</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: darkblue; width: 159px; margin: 0 47.3%;">
                <div class="buttoncenter">
                    <button class="operalt" id="**2" name="²">x²</button>
                    <button class="numalt" id="sqrt(" name="√(">√</button>
                    <button class="num" id="(">(</button>
                    <button class="num" id=")">)</button>
                    <button class="num" id="[">[</button>
                    <button class="num" id="]">]</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: chartreuse; width: 150px; margin: 0 12%;">
                <div class="buttoncenter">
                    <button class="num" id="sin(">sin</button>
                    <button class="num" id="cos(">cos</button>
                    <button class="num" id="tan(">tan</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: cornflowerblue; width: 144px; margin: -9% 51%;">
                <div class="buttoncenter">
                    <button class="operalt" id="**" name="^">^</button>
                    <button class="numalt" id="pi" name="π">π</button>
                    <button class="num" id="e">e</button>
                    <button class="oper" id=",">,</button>
                    <button class="oper" id=".">.</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: darkgreen; width: 159px; margin: 9% 12%;">
                <div class="buttoncenter">
                    <button class="numalt" id="arcsin(" name="sin⁻¹(">sin⁻¹</button>
                    <button class="numalt" id="arccos(" name="cos⁻¹(">cos⁻¹</button>
                    <button class="numalt" id="arctan(" name="tan⁻¹(">tan⁻¹</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: aqua; width: 135px; margin: -18% 53.2%;">
                <div class="buttoncenter">
                    <button class="num" id="ln(">ln</button>
                    <button class="num" id="exp(">exp</button>
                    <button class="num" id="log(">log</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: mediumseagreen; width: 300px; margin: 18% 12%;">
                <div class="buttoncenter">
                    <button class="num" id="root(">root(index,radicand)</button>
                    <button class="num" id="logrtm(">logrtm(base,argument)</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: olive; width: 151px; margin: -18% 12%;">
                <div class="buttoncenter">
                    <button class="operalt" id="*percent" name="%">%</button>
                    <button class="convert" id="dec(">=>dec</button>
                    <button class="convert" id="int(">=>int</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: royalblue; width: 143px; margin: 9% 51.2%;">
                <div class="buttoncenter">
                    <button class="operalt" id="%" name="mod">mod</button>
                    <button class="num" id="min(">min</button>
                    <button class="num" id="max(">max</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: teal; width: 300px; margin: -9% 12%;">
                <div class="buttoncenter">
                    <button class="num" id="mean(">mean</button>
                    <button class="num" id="median(">median</button>
                    <button class="num" id="mode(">mode</button>
                    <button class="num" id="range(">range</button>
                    <button class="num" id="sum(">sum</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: springgreen; width: 154px; margin: 9% 12%;">
                <div class="buttoncenter">
                    <button class="numalt" id="factorial(" name="!(">!</button>
                    <button class="num" id="combination(">combination(x,y)</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: mediumblue; width: 140px; margin: -18% 52.1%;">
                <div class="buttoncenter">
                    <button class="num" id="random(">random(low,high)</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: forestgreen; width: 140px; margin: 18% 12%;">
                <div class="buttoncenter">
                    <button class="num" id="quad(">quad(a,b,c)</button>
                    <button class="convert" id="setx(">setx</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: dodgerblue; width: 154px; margin: -27% 48.5%;">
                <div class="buttoncenter">
                    <button class="num" id="setlist(">setlist</button>
                    <button class="convert" id="addtolist(">addtolist</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: lawngreen; width: 200px; margin: 27% 12%;">
                <div class="buttoncenter">
                    <button class="num" id='makefunction("'>makefunction("f(y)")</button>
                    <button class="num" id="f(">f(y)</button>
                </div>
            </div>

            <div class="buttonsdiv" style="border-color: darkslateblue; width: 94px; margin: -36% 63.5%;">
                <div class="buttoncenter">
                    <button class="oper" id='"'>"</button>
                    <button class="num" id='func'>func</button>
                </div>
            </div>
        </div>

        <div style="visibility: hidden;"id="math">0</div>
        <script>
            let expression = document.querySelector("#expression");
            let math = document.querySelector("#math");

            let previous_exs = [];
            let previous_maths = [];

            function enter()
            {
                let result = eval(math.textContent.toLowerCase());

                if (String(result).indexOf(" ") > 0)
                {
                    expression.innerHTML = result;
                }

                if ((result % 1 != 0) && (!isNaN(result)))
                {
                    expression.innerHTML = eval(result.toFixed(8));
                }
                else
                {
                    expression.innerHTML = result;
                }

                if (Object.is(result, NaN) || result === null)
                {
                    expression.innerHTML = "Undefined";
                }

                math.innerHTML = expression.textContent;
            }

            function clear()
            {
                expression.innerHTML = 0;
                math.innerHTML = 0;
            }

            function del()
            {
                if ((previous_exs.length != 0) && (previous_maths.length != 0))
                {
                    expression.innerHTML = previous_exs[previous_exs.length - 1];
                    math.innerHTML = previous_maths[previous_maths.length - 1];

                    previous_exs.pop();
                    previous_maths.pop();
                }
                else
                {
                    expression.innerHTML = 0;
                    math.innerHTML = 0;
                }
            }

            function num(item)
            {
                previous_exs.push(expression.textContent);
                previous_maths.push(math.textContent);

                if ((Number(expression.innerHTML) === 0) && (expression.textContent.length === 1)) {
                    expression.innerHTML = item;
                    math.innerHTML = item;
                } else {
                    expression.innerHTML += item;
                    math.innerHTML += item;
                }
            }

            function oper(item)
            {
                previous_exs.push(expression.textContent);
                previous_maths.push(math.textContent);

                expression.innerHTML += item;
                math.innerHTML += item;
            }

            function convert(item)
            {
                previous_exs.push(expression.textContent);
                previous_maths.push(math.textContent);

                expression.innerHTML = item + expression.textContent + ")";
                math.innerHTML = item + math.textContent + ")";
            }

            function operalt(item1, item2)
            {
                previous_exs.push(expression.textContent);
                previous_maths.push(math.textContent);

                expression.innerHTML += item1;
                math.innerHTML += item2;
            }

            function numalt(item1, item2)
            {
                previous_exs.push(expression.textContent);
                previous_maths.push(math.textContent);

                if ((Number(expression.innerHTML) === 0) && (expression.textContent.length === 1)) {
                    expression.innerHTML = item1;
                    math.innerHTML = item2;
                } else {
                    expression.innerHTML += item1;
                    math.innerHTML += item2;
                }
            }

            function convertalt(item1, item2)
            {
                previous_exs.push(expression.textContent);
                previous_maths.push(math.textContent);

                expression.innerHTML = item1 + expression.textContent + ")";
                math.innerHTML = item2 + math.textContent + ")";
            }

            const numbers = document.querySelectorAll(".num");
            for (let i = 0; i < numbers.length; i++) {
                numbers[i].addEventListener("click", function() {
                    num(numbers[i].id);
                });
            }

            const operations = document.querySelectorAll(".oper");
            for (let i = 0; i < operations.length; i++) {
                operations[i].addEventListener("click", function() {
                    oper(operations[i].id);
                });
            }

            const conversion = document.querySelectorAll(".convert");
            for (let i = 0; i < conversion.length; i++) {
                conversion[i].addEventListener("click", function () {
                    convert(conversion[i].id);
                });
            }

            const operalts = document.querySelectorAll(".operalt");
            for (let i = 0; i < operalts.length; i++) {
                operalts[i].addEventListener("click", function () {
                    operalt(operalts[i].name, operalts[i].id);
                });
            }

            const numalts = document.querySelectorAll(".numalt");
            for (let i = 0; i < numalts.length; i++) {
                numalts[i].addEventListener("click", function () {
                    numalt(numalts[i].name, numalts[i].id);
                });
            }

            const convertalts = document.querySelectorAll(".convertalt");
            for (let i = 0; i < convertalts.length; i++) {
                convertalts[i].addEventListener("click", function () {
                    convertalt(convertalts[i].name, convertalts[i].id);
                });
            }

            document.querySelector("#enter").onclick = enter;

            document.querySelector("#clear").onclick = clear;

            document.querySelector("#delete").onclick = del;

            document.addEventListener('keydown', function(){
                var key = event.key;
                if ("0123456789[]()".includes(key) || (key.toUpperCase() != key.toLowerCase() && key.length == 1)) {
                    if (key != "Space")
                    {
                        num(key);
                    }
                }
            });

            document.addEventListener('keydown', function(){
                var key = event.key;
                if ('-,.+/"'.includes(key)) {
                    oper(key);
                }
            });

            document.addEventListener("keydown", function(){
                var key = event.key;
                var symbols = {"*":"×", "^":"**", "%":"*percent", "!":"factorial"};
                if ("^%".includes(key)) {
                    operalt(key, symbols[key]);
                }
                else if ("*".includes(key)) {
                    operalt(symbols[key], key);
                }
                else if ("!".includes(key)) {
                    numalt("!(", "factorial(")
                }
            });

            document.addEventListener("keydown", function() {
                var key = event.key;
                if (key == "Enter" || key == "=")
                {
                    enter();
                }

                if (key == "Backspace")
                {
                    del();
                }

                if (key == "Delete")
                {
                    clear();
                }
            });

        </script>
    </body>
</html>

SIDENOTE: I tried asking a similar question in Stack Overflow, but it got downvoted. Hopefully it goes better here...

\$\endgroup\$
3
  • \$\begingroup\$ Can you explain what you mean with security? What are you trying to protect against? \$\endgroup\$ Mar 7, 2021 at 11:11
  • 1
    \$\begingroup\$ @MaartenBodewes Basically, can someone mess up my calculator by inputting Javascript code that gets evaluated? \$\endgroup\$
    – M-Chen-3
    Mar 7, 2021 at 19:13
  • 1
    \$\begingroup\$ Alright. You might want to edit such info into the question as well. I might not be the most experienced security wizz w.r.t. JavaScript eval, so I'll pass. \$\endgroup\$ Mar 7, 2021 at 20:49

1 Answer 1

2
+50
\$\begingroup\$

As long as the evaluation code is only executed on the client side, you don't have to worry, because the user can just change your code if he wants to. An example where it could be an insecurity is if the attacker can make another user evaluate his formula. If you implement a function to share the result of a calculation in a link (for example cs50.com/calculator?input=hackTheMachine()) and there is a security issue with your evaluation the one that clicked the link has a problem.

If you would do the evaluation on your server, you would need more security checks because then the attacker can do things he might not be able to do otherwise.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.