GlideEvaluator Context

tltoulson
Kilo Sage

So according to best practices we are supposed to avoid the use of eval and instead use the GlideEvaluator.evaluateString (formerly Packages.com.glide.script.Evaluator.evaluateString). But how can you pass context to the GlideEvaluator? For eval, the context is always the current context but for GlideEvaluator (as demonstrated in the wiki) the context is always global. This makes it problematic to use GlideEvaluator instead of eval from within script includes. For example:



var NewInclude = Class.create();
NewInclude.prototype = {
initialize: function() {
},

runScript: function(script) {
var msg = 'Hello';
return eval(script);
},

type: "NewInclude"
}

var example = new NewInclude();
gs.print(example.runScript("msg = msg + ' World'"));


In the above example, gs.print will print out 'Hello World'. This is because the context for eval is the same as the context being executed within the Script Include function, thus it has access to the 'msg' variable set up before the eval line.

If you replace eval with the proper GlideEvaluator.evaluateString (or the Package call as appropriate), an error is thrown stating that 'msg' is not defined. Digging into this further, you will discover that the 'this' context executed within the GlideEvaluator is in fact the global context, not the context within the script include function.

Is there a way to pass the correct context into the GlideEvaluator or is it best to just to use eval? Or is there another option?

1 ACCEPTED SOLUTION

Starting with Calgary, you won't be able to make Packages calls. What about something like below - it allows you to send in a script that references a local variable in the runScript method and that variable is not global:



var NewInclude = Class.create();



NewInclude.prototype = {



      initialize: function () {


              // do nothing


      },



      runScript: function (script) {



              // Local vars


              var hasOwn = Object.prototype.hasOwnProperty,


                      name,


                      tmpScript;



              // Define variables needed on 'this'


              this.msg = "Hello";


              this.obj = {


                      name: "ServiceNow"


              };



              // Start tmpScript


              tmpScript = "(function() {\n";



              // Get all non-functions from 'this'


              for (name in this) {


                      if (hasOwn.call(this, name)) {


                              if (typeof this[name] !== "function") {


                                      tmpScript += " var " + name + " = " + new JSON().encode(this[name]) + ";\n";


                              }


                      }


              }



              // Finish tmpScript


              tmpScript += script;


              tmpScript += "\n})();";



              // This is just to show what it's doing


              gs.print("Using script:\n" + tmpScript + "\n");



              return GlideEvaluator.evaluateString(tmpScript);


      },



      type: "NewInclude"



};



// Instantiate NewInclude and call runScript method


var example = new NewInclude();


example.runScript(" msg = msg + ' World';\n gs.print(msg + ' from ' + obj.name + '\\n');");



// Proof of not polluting global context


// NOTE: If 'msg' existed globally, it would retain its existing value


gs.print("typeof msg: " + typeof msg);




Output:




*** Script: Using script:
(function() {
  var obj = {"name":"ServiceNow"};
  var msg = "Hello";
  msg = msg + ' World';
  gs.print(msg + ' from ' + obj.name + '\n');
})();

*** Script: Hello World from ServiceNow

*** Script: typeof msg: undefined

View solution in original post

9 REPLIES 9

Update to this: GlideEvaluator does not work in scoped apps. Instead you need to use GlideScopedEvaluator. Unfortunately, it only has the evaluateScript() method (which requires a GlideRecord object and field name) - no evaluateString() method is available as of this writing.



What I did as a workaround is to simply instantiate a GlideRecord (no table reading/writing needed) and populate the script field, then use that.


Example:



var dummy = new GlideRecord('sysauto_script');


dummy.initialize();


dummy.script = "gs.debug('Hello World');"


new GlideScopedEvaluator().evaluateScript(dummy, 'script', '');


Hi Gregor,



I think you're right.



What has been deprecated is the heaps of the glideapp, glide and snc sub packages within Packages. The Packages.java apparently is still usable. It is actually called in 41 out of the box script includes in my personal instance:


Build name: Helsinki


Build date: 03-22-2017_2340


Build tag: glide-helsinki-03-16-2016__patch9-hotfix0a-03-17-2017



Thanks,
Michel


Thanks to andrew.venables for find this for me... you can just instantiate an object with a script field and run it. It seems that in later releases (J and perhaps I or earlier) you need to actually use a real record. This from Andrew...




My best guess is that the developers inserted a gr.isValidRecord() check before the evaluation, check out the following use cases:



var gse = new GlideScopedEvaluator();



var dummy = new GlideRecord('sysauto_script');


dummy.initialize();


gs.info(dummy.isValidRecord()); // will return false


dummy.script = "Math.PI;"


var res = gse.evaluateScript(dummy, 'script', '');


gs.info(res); // result is null L



var dummy = new GlideRecord('sysauto_script');


dummy.get('3e0c3a350fd483009410fa6ce1050e18');


gs.info(dummy.isValidRecord()); // will return true


dummy.script = "Math.PI;"


var res = gse.evaluateScript(dummy, 'script', '');


gs.info(res); // result is 3.14…



var dummy = new GlideRecord('sysauto_script');


dummy.query();


dummy.next();


gs.info(dummy.isValidRecord()); // will return true


dummy.script = "Math.PI;"


var res = gse.evaluateScript(dummy, 'script', '');


gs.info(res); // result is 3.14…



So the technique just needs a slight modification to first get a real/valid record then it can be modified.


Hi andrew,

Is there any way out, as I want lists refresh automatically? eg. after 30 mins whole bunch of new tickets reflects in list view.

Daniel A-C
Tera Expert

Here is an updated version that should work now that later versions enforce the requirement of it not being an unsaved record. Credit my colleague: David Sturman



Issue


The issue is that the GlideScopedEvaluator will only execute script which is specified in a GlideRecord object. Previously it has been sufficient to just initialize one in memory and pass it to the .evaluateScript() method without saving the record first. It appears that sometime after Istanbul_Patch2 ServiceNow has implemented a change which prevents this usage. (Whether on purpose or not, I have no idea. I cannot find any documentation for this change.)



Workaround


Retrieve any record with a script field, dynamically set the script value but don't actually update and it will still execute the updated script. For extra protection against any inadvertent updates, I suggest doing this against a record with a 'Read-only' protection policy.


var evaluator = new GlideScopedEvaluator();  


var gr = new GlideRecord('sys_script');  


gr.addQuery('sys_policy','read'); //additional protection against accidental update


gr.setLimit(1);  


gr.query();  


 


if (gr.next()) {  


        gr.setValue('script','gs.print("This script has executed and my user id is " + gs.getUserID());');  


        try {  


                  evaluator.evaluateScript(gr, 'script');  


        }  


        catch (e) {  


                  gs.error(e);  


        }  


}  


else {  


        throw new Error('No rows were found to dynamically use GlideScopedEvaluator against');  


}  



I've also thrown some error handling and protection in here. Hopefully that will assist anyone else finding this article in future.


Daniel