Developing Feather-Weight Webservices with JavaScript

Args()

A class for remote script arguments

Getting the arguments out of a query string in the URI of a <script>’s src attribute turned out to be surprisingly easy in Passing JavaScript arguments via the src attribute. It’s only about 20 lines of code but we really don’t want it floating around in a dozen different service script and we’d rather never have to see it directly again. Just have a simple interface to use the functionality and get it out of the way. Enter the object class.

Our work is already mostly done. We’re just going to wrap what we’ve already written into an object like we talked about in the last chapter: Object orientating and prototypes.

First, a constructor

function Args () {
  var caller = this.findCaller();
  var qString = caller.src.replace(/^[^\?]+\??/,'');
  if ( qString ) {
     this.queryString = qString;
     this.params = this.parseArgs();
  }
}

We’ve leapt ahead a bit here. The argument object always needs to know the query string from its <script>’s src attribute. That’s the whole point of the object. So we want to initialize with it. We get at it with the method findCaller(), which we haven’t written yet.

We could dispense with findCaller() and do the src lookup inside the constructor, Args(). It is preferable to break code up into the smallest holistic units. The constructor is about making an object, not about finding its src, so we break it out. When writing methods, functions, and subroutines, it’s a good rule of thumb to never write more than be taken in at a glance. It’s easier to work with and it’s especially easier to debug. You’ll thank yourself later.

We’re not quite ready to call new Args(). We need the methods it uses first.

Args.findCaller()

Args.prototype.findCaller = function () {
  var scripts = document.getElementsByTagName('script');
  return scripts[ scripts.length - 1 ];
}

We’ve also taken the liberty of installing the parsed parameters into our object’s params attribute. We use another method that the object, this, calls upon itself to load the returned data into itself. Aren’t objects swell.

Args.parseArgs()

Args.prototype.parseArgs = function () {
   var Params = new Object ();
   var query = this.queryString;
   if ( ! query ) return Params;
   var Pairs = query.split(/;/)
   for ( var i = 0; i < Pairs.length; i++ ) {
      var KeyVal = Pairs[i].split('=');
      if ( ! KeyVal.length == 2 ) continue;
      if ( ! ( KeyVal[0] || KeyVal[1] ) ) continue;
      var key = unescape( KeyVal[0] );
      var val = unescape( KeyVal[1] );
      val = val.replace(/\+/g, ' ');
      val = val.replace(/&/g, '&amp;');
      val = val.replace(/>/g, '&gt;');
      val = val.replace(/</g, '&lt;');
      Params[key] = val;
   }
   return Params;
}

This is our friend from Passing JavaScript arguments via the src attribute. NB: we’ve chosen the version that does not support multiple values per key. This isn’t the standard but it’s easier to deal with and it’s an in house library so we don’t have to support any standards at all if we don’t feel so inclined.

We’ve also made an important addition to avoid cross-site scripting attacks. After unescaping the argument value we convert <, >, and & into their HTML entities. We might consider doing this for the keys as well but we’re never going to echo keys back to the document so it’s not in this implementation.

Sample usage

// ALL the Args() code goes here; omitted to save space

// Now we can construct objects
var args = new Args();

var params = args.params;

for ( var key in params ) {
  document.write( '<div style="margin:1ex">' );
  document.write( "key: " + b(key) );
  document.write( "<br />" );
  document.write( "value: " + b(params[key]) );
  document.write( "</div>" );
}

// little extra something to get bold tags easily
function b (str) { return '<b>' + str + '</b>' }

Now a call like so:

Sample client call

<script type="text/javascript"
 src="http://elektrum.org/js/args.js?holiday=Valentine's;flower=Roses">
</script>

The output

That’s good but...

We have a nice arguments object1 now but one part of it is irksome. This step:

var params = args.params;

An arguments object should be just that: arguments. It shouldn’t have a level of indirection to get at them. It’s inelegant. This is fixable if we’re careful.

Args(), take 2

The new concerns

Since we’ll be putting the query string keys at the top level of the Args() object we need a way to keep them separate from other attributes in the object. We track things like the queryString through the object’s properties. We can’t just take them out because they have nowhere to go but global. That’s a bad solution.

There is a hacker convention for marking a variable or attribute name private. Prefix it with an underscore. varName becomes _varName and secretMethod() becomes _secretMethod().

It is works because other hackers will respect its privacy and the general user would never think to start a variable name with an underscore. It doesn’t mean the variables and methods are protected, it just means they’re a bit out of reach of casual users and marked “Don’t Touch” for the savvy.

We will prefix our private, or non query param, attribute variables with an underscore. Don’t forget that methods are also attributes of an object/class. That means we’ll need to hide the private methods away too.

Finally we’ll need a custom way to iterate through the object. If we do something like for ( var argKey in argObj ) ... we’ll get a nasty surprise because we’ll get more than the query string keys back. We’ll also get the method objects and any internal variables the class uses for housekeeping.

The new object constructor

function Args () {
  var caller = this._findCaller();
  var qString = caller.src.replace(/^[^\?]+\??/,'');
  if ( qString ) {
     this._queryString = qString;
     this._parseArgs();
  }
}

The new concerns are reflected in the new names, like the method _parseArgs() and the attribute _queryString.

The new _findCaller()

Args.prototype._findCaller = function () {
  var scripts = document.getElementsByTagName('script');
  return scripts[ scripts.length - 1 ];
}

Only the method name has changed for _findCaller().

The new _parseArgs()

Args.prototype._parseArgs = function () {
   if ( ! this._queryString ) return false;
   var Pairs = this._queryString.split(/;/);
   for ( var i = 0; i < Pairs.length; i++ ) {
      var KeyVal = Pairs[i].split('=');
      if ( ! KeyVal.length == 2 ) continue;
      if ( ! ( KeyVal[0] || KeyVal[1] ) ) continue;
      if ( KeyVal[0].match(/^_/) ) continue;
      var key = unescape( KeyVal[0] );
      var val = unescape( KeyVal[1] );
      val = val.replace(/\+/g, ' ');
      val = val.replace(/&/g, '&amp;');
      val = val.replace(/>/g, '&gt;');
      val = val.replace(/</g, '&lt;');
      this[key] = val;
   }
}

_parseArgs() works a bit differently. We no longer use the intermediary object named Params() and we return nothing. We’re not giving back a parameters object. We are the parameters object. Which is reflected specifically in the following change from the first version.

      Params[key] = val;
      this[key] = val;

For safety’s sake we also disallow parameter names with leading underscores. If you do allow it, it gives anyone with a browser an open channel to mess with the internals of your services.

      if ( KeyVal[0].match(/^_/) ) continue;

We also need a way to iterate on the arguments. Since we’ve put them at the top level, they clash with methods and private variables. We do a simple check, /^(_|getKeys)/, to build a list of argument names in the object.

Arg.getKeys()

Args.prototype.getKeys = function () {
  var keys = new Object();
  for ( var attr in this ) {
    if ( attr.match(/^(_|getKeys)/) ) continue;
    keys[attr] = undefined;
  }
  return keys;
}

Note we could instead use delete in the constructor to manually remove every property of the object that’s not an argument key. Then for ( key in args ) ... would work just fine. This precludes using the object as anything but an associate array of arguments thereafter. It means no class data allowed either. We have grander plans for the object when we start building libraries.

Sample usage

// ALL the new Args() code goes here; omitted to save space

var args = new Args();

// skip getting the "params" out, iterate with our getKeys() method

for ( var key in args.getKeys() ) {
  document.write( '<div style="margin:1ex">' );
  document.write( "key: " + b(key) );
  document.write( "<br />" );
  document.write( "value: " + b(args[key]) );
  document.write( "</div>" );
}

// little extra something to get bold tags easily
function b (str) { return '<b>' + str + '</b>' }

// We also have direct access to the keys we know/expect

document.write( '<div style="margin:1ex">' );
document.write( 'Direct access via <i>args.art</i>: ' + b( args.art ) );
document.write( "</div>" );

Now a call like so:

Sample client call

<script type="text/javascript"
 src="http://elektrum.org/js/args2.js?art=Tae+Kwon+Do;sport=satire">
</script>

“getKeys” is now, for all purposes, a reserved word. As far as parameter names go it can’t be used. Choose this sort of thing carefully.

The output

Ta! We’ll make a a couple of adjustments to the class along the way to extend where it can be used. The final version is in and the reasons for changes to it are discussed in Using JS code libraries.

Now for a slightly more complex object: Query catching for fun and profit.

1 To clarify, there already is an arguments() object built into JavaScript. You have access to it inside functions and methods and nowhere else. It contains an indexed list of the arguments passed to the function as well as two special attributes: length and callee which contains a reference to the function itself which facilitates recursive logic.
« Object orientating and prototypes · Query catching for fun and profit »
Google
 
Web Developing Featherweight Web Services with JavaScript
This is version 0.57b of this manual. It is a beta version with some gaps. We are grateful for feedback.

The code is the manual has not yet been fully tested against Internet Explorer. Bug reports are welcome.
An Elektrum Press Online