12

I have JSON data that I am searching through using filter:

myJsonData.filter(function (entry) { return (entry.type === 'model' || entry.type === 'photographer' ); });

Now instead of specifying those conditions after return, I've created a similar string (because I want to have a list of pre-created search conditions) then using eval() so:

myJsonData.filter(function () { return eval(stringToSearch) ; });

This appears to work. However, I just want to confirm, is this its correct usage? Are there any risks/issues in doing this?

I want to have the flexibility to do, any kind of search e.g.:

myJsonData.filter(function (entry) { 
   return (entry.type === 'model' || entry.type === 'photographer') 
          && entry.level.indexOf('advanced') > -1 ; 
});

That's why I made a separate class to create that string.

morten.c
  • 3,414
  • 5
  • 40
  • 45
userMod2
  • 8,312
  • 13
  • 63
  • 115
  • 3
    Inelegant and possibly unsafe. Best to find another way. – CertainPerformance Feb 17 '19 at 07:14
  • 1
    You should be able to set up an object `filters` with the functions you need, and then do `myJsonData.filter(filters[selectedFilterName])`. That way there can be no code injection. – Thilo Feb 17 '19 at 07:15
  • 1
    @CertainPerformance @Thilio - Thanks. So to add more detail, I have a bunch of filter buttons, so each time a new filter button is pressed I may add another condition to the filter in which case I'd need to search the JSON again - e.g. `return (entry.type === 'model' || entry.type === 'photographer' ) && entry.location ==='ny' ;` Any other suggestions to look into then? – userMod2 Feb 17 '19 at 07:18
  • Is there anything stopping you from just dynamically creating the callback with whatever logic you need? – CertainPerformance Feb 17 '19 at 07:22
  • I think "dynamically creating the callback" is roughly the same effort and logic than what you now need to "dynamically create the string to eval", but much safer. – Thilo Feb 17 '19 at 07:24
  • @Thilo - thanks, could you elaborate on that a little more, I can't picture how it would look. – userMod2 Feb 17 '19 at 07:29
  • @ userMod2 See my example, i do it dymaicly without using `eval`. look at example two – Alen.Toma Feb 17 '19 at 09:17
  • Where does `stringToSearch` come from, who would supply it? Please post the code of the "*separate class [made] to create that string*". And what do you mean by "*I want to have a list of pre-created search conditions*"? – Bergi Feb 17 '19 at 14:02

3 Answers3

8

To avoid eval you could translate user input (through buttons, or whatever) to filters. Those filters would have one filter per data property (i.e. per location, type, level, ...). One of those filters could either be a list of values, or a free-text single value.

Here is an example implementation with a sample data set, without any sexy input/output widgets,... just the bare minimum to demo the algorithm of filtering:

// The sample data to work with:
var data = [
    { location: "ny", type: "model", level: "advanced", name: "Jack" },
    { location: "ny", type: "model", level: "beginner", name: "Fred" },
    { location: "sf", type: "model", level: "experienced", name: "Helen" },
    { location: "sf", type: "photographer", level: "is advanced", name: "Stacy" },
    { location: "sf", type: "photographer", level: "advanced experience", name: "Joy" },
    { location: "ny", type: "photographer", level: "beginner++", name: "John" },
    { location: "sf", type: "model", level: "no experience", name: "Jim" },
    { location: "ny", type: "photographer", level: "professional", name: "Kay" },
];

// A global variable to maintain the currently applied filters
var filters = { type: [], location: [], level: "" };

// Capture user selections and translate them to filters
// Type 1: multiple selections from a closed list of values:
document.querySelector("#seltypes").addEventListener("change", function() {
    filters.type = [...this.options].filter(option => option.selected).map(option => option.value);
    refresh();
});

document.querySelector("#sellocations").addEventListener("change", function() {
    filters.location = [...this.options].filter(option => option.selected).map(option => option.value);
    refresh();
});

// Type 2: free text filter:
document.querySelector("#inplevel").addEventListener("input", function() {
    filters.level = this.value;
    refresh();
});

function refresh() {
    // This is the actual filtering mechanism, making use of the filters variable
    let result = data;
    for (let prop in filters) {
        let value = filters[prop];
        if (!value.length) continue; // If this filter is empty: don't filter
        result = Array.isArray(value)
            ? result.filter(entry => value.some(type => entry[prop] === type))
            : result.filter(entry => entry[prop].includes(value));
    }
    // No effort done here on the output format: just JSON :-)
    document.querySelector("#output").textContent = JSON.stringify(result, null, 2);
}

// Start 
refresh();
td { vertical-align: top }
<b>Filters (Ctrl to multi select):</b>
<table>
<tr><th>Types</th><th>Locations</th><th>Level</th></tr>
<tr><td>
  <select multiple id="seltypes" size="2">
    <option value="model">Model</option>
    <option value="photographer">Photographer</option>
  </select>
</td><td>
  <select multiple id="sellocations" size="2">
    <option value="ny">New York</option>
    <option value="sf">San Francisco</option>
  </select>
</td><td>
  <input id="inplevel">
</td></tr></table>

<pre id="output"></pre>
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Thanks for this - one bit I don't fully understand, in the line: `filters.type = [...this.options].filter(option => option.selected).map(option => option.value);` - what does `[...this.options]` mean? – userMod2 Feb 17 '19 at 09:11
  • `[...this.options]` turns a [`HTMLCollection`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection) into a standard array, so that I can use Array methods on it, like `filter` in this case. I could also have written `Array.from(this.options)` which may be more readable. – trincot Feb 17 '19 at 09:12
  • Ah ok, I think I understand what happening. Well after a run through. Thanks. – userMod2 Feb 17 '19 at 09:20
  • I don't think that part is essential, as it is specific to the input elements I have used. You may not be using `select` elements at all. Maybe you will use checkboxes or radiobuttons, ... The important thing is that you find a way to collect the values from the user-input. That's all this line of code is doing. – trincot Feb 17 '19 at 09:23
1

You can create an object with the values you want in the output and then filter.

In an if condition I check whether the advanced filter is applied or not. If applied with check for the && condition too. If not, then I will just check the normal condition.

let data  = [{type: 'model', level:'advanced'}, {type:'photographer',level:'advanced'},{type:'random', level:'random'}, {type:'model', value:'without level'}]
let checks = {'model':true, 'photographer':true, advanced:['advanced']}

let output = data.filter(( {type,level} ) => {
  if(checks.advanced && checks.advanced ){
     return checks[type] && checks.advanced.includes(level)
  } else {
    return checks[type]
  }
} )

console.log(output)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
  • 3
    This does not cover the `&& entry.location ==='ny'` part. – Thilo Feb 17 '19 at 07:21
  • @Thilo where is this thing in `&& entry.location ==='ny'` question ? – Code Maniac Feb 17 '19 at 07:22
  • @userMod2 you want output to be true only if all the filter matches right ? can you add more info about that. than will update code accordingly – Code Maniac Feb 17 '19 at 07:26
  • 1
    Well I want to be able to search multiple things in the data, so even something like: `myJsonData.filter(function (entry) { return (entry.type === 'model' || entry.type === 'photographer') && entry.level.indexOf('advanced') > -1 ; });` – userMod2 Feb 17 '19 at 07:32
-1

There is nothing wrong in using eval. Here are three ways you could have done it.

There is of course other ways to do it, but this is a much more dynamic approach.

    // The sample data to work with:
    var data = [
        { location: "ny", type: "model", level: "advanced", name: "Jack" },
        { location: "ny", type: "model", level: "beginner", name: "Fred" },
        { location: "sf", type: "model", level: "experienced", name: "Helen" },
        { location: "sf", type: "photographer", level: "is advanced", name: "Stacy" },
        { location: "sf", type: "photographer", level: "advanced experience", name: "Joy" },
        { location: "ny", type: "photographer", level: "beginner++", name: "John" },
        { location: "sf", type: "model", level: "no experience", name: "Jim" },
        { location: "ny", type: "photographer", level: "professional", name: "Kay" },
    ];

    // Example 1
    var searchOne = function(a ){
        return a.location == "ny";
    }

    // Example two: an attribute
    var searchTwo = new Function("a", test.getAttribute("condition"));

    // Example three: filter list, need much work.... to handle operator      // And, OR
    var searchThree = [
        { field: "location", key: "=", value:"ny"  }]

    console.log("example 1")
    console.log(data.filter(searchOne))

    console.log("example 2")
    console.log(data.filter(searchTwo))

    console.log("example 3")
    console.log(data.filter((a)=> {
        var result = true;
        searchThree.forEach((x)=> {
            var v = a[x.field];
            if (x.key == "=")
                result = (v == x.value);
            else if (x.key == "!=")
                result = (v != x.value);
                //.....
        });

        return result;
    }))
    <p id="test" condition="return a.location=='sf';"</p>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alen.Toma
  • 4,684
  • 2
  • 14
  • 31
  • 1
    why do you think eval is not a problem here? –  Feb 17 '19 at 08:54
  • It all depend on what you are using it for, if its a calculation thing, then its very dangerous. but a simple table filter should not be very dangerous. this so the user want to search a table. so hacking it wont couse a problem. Remember im only using eval on a function that will be included in `.filter`. This what i think anyway. – Alen.Toma Feb 17 '19 at 08:58
  • 1
    i removed eval and added Function instead have a look. – Alen.Toma Feb 17 '19 at 09:17
  • 1
    -1 for the first sentence. [There is a lot wrong with `eval`](https://stackoverflow.com/q/86513/1048572). – Bergi Feb 17 '19 at 14:04
  • The problem with Function/eval is not necessary in the way the programmer intends to use it but by using it you open the door to unintended consequences. – Dan Feb 17 '19 at 14:12
  • He want to be able to create a `where stats` eg filter function dynamicly. converting a string to code. now using Eval hes able to do so, i simple showing him that there is anothr way of doing it without using eval. He wanted to know if it was there an alternative to eval. and also if there is issue using eval, in his case it was not an issue. – Alen.Toma Feb 17 '19 at 14:26
  • `new Function()` and `eval` are essentially equivalent in terms of the root issue - Code generation at runtime is fundamentally a *bad idea* – Dan Feb 18 '19 at 13:59
  • Well i still think that it all depend at the context. if you look at `anguler` they to include code at html attributes. how do you think they would convert it to code ? – Alen.Toma Feb 18 '19 at 14:02
  • Angular has its own security controls in place. The average joe should not implement code generation. – Dan Feb 18 '19 at 15:44