what's-this?

Content Syndication with Case-Hardened Web Badges

Here's my presentation from the 2008 edition of Web 2.0, originally given at the crack of dawn, 8:30am, Wednesday, 23 April 2008.

Quite a bit more on the subject is available here:

Some Examples:

Examples found in the wild:

Thank You:

willkommen-bienvenu-welcome-stranger

Good Morning!

Before We Begin:

Do Me A Favor

If anyone comes in late, please point them at the URL at the bottom of the screen. Thanks!

content-syndication-with-case-hardened-javascript

Content Syndication with Case-Hardened JavaScript, or

Badges! Badges! Badges!

Kent Brewster

http://kentbrewster.com

In just a moment, we'll be off and running into the wonderful world of single-line-include JavaScript badges. But first, it's time to play America's favorite game show....

danger-danger-will-robinson

Disclaimerama!

Yes, I work for Yahoo!, but:

  1. Anything I say is my opinion, not theirs.
  2. If anything breaks, it's my fault, not theirs.
  3. Sorry, I have absolutely nothing to say about the you-know-what. :)

the-state-of-the-art

The State Of The Art:

whats-on-for-today

Today's Plan

Build a script that:

Break out your editors, because it's time to copy and paste!

getting-started

Structure

Your basic struture:

<html>
 <head>
  <title>Badge Me!</title>
 </head>
 <body>
  <script src="behavior.js"></script>
 </body>
</html>
hello-world

Default Behavior

Save this as behavior.js and run it, by dragging structure.html into your browser:

var MYGLOBAL = window.MYGLOBAL || {};
MYGLOBAL.myProgram = function() {
 return {
  init : function() {
   alert('Hello, world!');
  }
 };
}();
window.onload = function() {
 MYGLOBAL.myProgram.init();
};

Don't close behavior.js or structure.html. We have work to do.

checkpoint

What Just Happened?

Where We're Vulnerable:

hide-the-ball

Hide the Ball

We can start to fix this, by obfuscating our root global variable. Here's one way, with a random function name:

var trueName = '';
for (var i = 0; i < 16; i++) {
 trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}
window[trueName] = {};
window[trueName].myProgram = function() {
 return {
  init : function() {
   alert('Hello, ' + trueName + '!');
  }
 };
}();
window.onload = function() {
 window[trueName].myProgram.init();
};

Save this and run it, to learn your function's true name.

checkpoint

What Just Happened?

build-a-wall

Build a Wall

Here we put the whole thing into an anonymous function, and add an internal shortcut. Copy, paste, and run this:

( function() {
 var trueName = '';
 for (var i = 0; i < 16; i++) {
  trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
 }
 window[trueName] = {};
 var $ = window[trueName];
  $.f = function() {
   return {
    init : function() {
     alert('Hello again, ' + trueName + '!');
    }
   };
  }();
 window.onload = function() {
  $.f.init();
 };
})();
checkpoint

What Just Happened?

creating-structure

Creating Structure

To make a badge work, we'll need a container element. We'll also want to clean up after the badge loads, just to be sure.

( function() {
 var trueName = '';
 for (var i = 0; i < 16; i++) {
  trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
 }
 window[trueName] = {};
 var $ = window[trueName];
 $.f = function() {
  return {
   init : function(target) {
    var theScripts = document.getElementsByTagName('SCRIPT');
    for (var i = 0; i < theScripts.length; i++) {
     if (theScripts[i].src.match(target)) {
      $.w = document.createElement('DIV');
      $.w.innerHTML = 'Hello, world.  My name is ' + trueName + '.';
      theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
      theScripts[i].parentNode.removeChild(theScripts[i]);
      break;
     }
    }
   }
  };
 }();
 var thisScript = /behavior.js/;
 window.onload = function() {
  $.f.init(thisScript);
 };
})();

Here's where you really do want to be copying and pasting. :)

checkpoint

What Just Happened?

wait-for-it

Wait For It!

It's good manners (and critical for low percieved load times) for your badge to wait for the rest of the page to load before firing off. Unfortunately, we can't depend on document.onload.

( function() {
 var trueName = '';
 for (var i = 0; i < 16; i++) {
  trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
 }
 window[trueName] = {};
 var $ = window[trueName];
 $.f = function() {
  return {
   init : function(target) {
    var theScripts = document.getElementsByTagName('SCRIPT');
    for (var i = 0; i < theScripts.length; i++) {
     if (theScripts[i].src.match(target)) {
      $.w = document.createElement('DIV');
      $.w.innerHTML = 'Hello, world.  My name is ' + trueName + '.';
      theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
      theScripts[i].parentNode.removeChild(theScripts[i]);
      break;
     }
    }
   }
  };
 }();
 var thisScript = /behavior.js/;
 if (typeof window.addEventListener !== 'undefined') {
  window.addEventListener('load', function() { $.f.init(thisScript); }, false);
 } else if (typeof window.attachEvent !== 'undefined') {
  window.attachEvent('onload', function() { $.f.init(thisScript); });
 }
})();
checkpoint

What Just Happened?

parsing-user-input

Pasing User Input

Here we check for the script's innerHTML attribute, and attempt to parse it as JSON if it's there.

( function() {
 var trueName = '';
 for (var i = 0; i < 16; i++) {
  trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
 }
 window[trueName] = {};
 var $ = window[trueName];
 $.f = function() {
  return {
   init : function(target) {
    var theScripts = document.getElementsByTagName('SCRIPT');
    for (var i = 0; i < theScripts.length; i++) {
     if (theScripts[i].src.match(target)) {
      $.w = document.createElement('DIV');
      $.w.innerHTML = 'Hello, world.  My name is ' + trueName + '.';
      $.a = {};
      if (theScripts[i].innerHTML) {
       $.a = $.f.parseJson(theScripts[i].innerHTML);
      }
      if ($.a.err) {
       alert('bad json!');
      }
      if ($.a.color) {
       $.w.style.color = $.a.color;
      }
      theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
      theScripts[i].parentNode.removeChild(theScripts[i]);
      break;
     }
    }
   },
   parseJson : function(json) {
    this.parseJson.data = json;
    if ( typeof json !== 'string') {
     return {"err":"trying to parse a non-string JSON object"};
    }
    try {
     var f = Function(['var document,top,self,window,parent,Number,Date,Object,Function,',
      'Array,String,Math,RegExp,Image,ActiveXObject;',
      'return (' , json.replace(/<\!--.+-->/gim,'').replace(/\bfunction\b/g,'function­') , ');'].join(''));
       return f();
    } catch (e) {
     return {"err":"trouble parsing JSON object"};
    }
   }
  };
 }();
 var thisScript = /behavior.js/;
 if (typeof window.addEventListener !== 'undefined') {
  window.addEventListener('load', function() { $.f.init(thisScript); }, false);
 } else if (typeof window.attachEvent !== 'undefined') {
  window.attachEvent('onload', function() { $.f.init(thisScript); });
 }
})();

Don't run this yet; we need to change structure.html first.

how-to-send-input

Structure, With Input

<html>
 <head>
  <title>Badge Me!</title>
 </head>
 <body>
  <script src="behavior.js">{"color":"red"}</script>
  <script src="behavior.js">{"color":"green"}</script>
  <script src="behavior.js">{"color":"blue"}</script>
 </body>
</html>

Run this, and you ought to see three separate instances of the script on the same page.

checkpoint

What Just Happened?

Up next: getting data from an outside Web service.

getting-outside-data

Getting Outside Data

This next little bit is explained in much greater detail in Build a Wikipedia Search Widget in an Hour.

( function() {
 var trueName = '';
 for (var i = 0; i < 16; i++) {
  trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
 }
 window[trueName] = {};
 var $ = window[trueName];
 $.f = function() {
  return {
   init : function(target) {
    var theScripts = document.getElementsByTagName('SCRIPT');
    for (var i = 0; i < theScripts.length; i++) {
     if (theScripts[i].src.match(target)) {
      $.w = document.createElement('DIV');
      $.a = {};
      if (theScripts[i].innerHTML) {
       $.a = $.f.parseJson(theScripts[i].innerHTML);
      }
      if ($.a.err) {
       alert('bad json!');
      }
      $.w.q = document.createElement('INPUT');
      if ($.a.query) {
       $.w.q.value = $.a.query;
      }
      $.w.q.onkeypress = function(e) {
       if ( (e ? e.which : event.keyCode) == 13) {
        $.f.runSearch();
       }
      };
      $.w.appendChild($.w.q);
      $.w.b = document.createElement('BUTTON');
      $.w.b.innerHTML = 'Search';
      if ($.a.site) {
       $.w.b.innerHTML += ' ' + $.a.site;
      }
      $.w.b.onmouseup = function() {
       $.f.runSearch();
      };
      $.w.appendChild($.w.b);
      $.w.r = document.createElement('UL');
      $.w.appendChild($.w.r);
      theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
      theScripts[i].parentNode.removeChild(theScripts[i]);
      break;
     }
    }
   },
   runSearch : function() {
    $.w.r.innerHTML = '';
    if ($.w.q.value) {
     if (!$.f.runFunction) {
      $.f.runFunction = [];
     }
     var n = $.f.runFunction.length;
     var id = trueName + '.f.runFunction[' + n + ']';
     $.f.runFunction[n] = function(r) {
      delete($.f.runFunction[n]);
      $.f.removeScript(id);
      $.f.renderResult(r);
     }
     var url = 'http://search.yahooapis.com/WebSearchService/V1/webSearch?';
     url += '&appid=YahooSearch';
     url += '&results=5';
     url += '&output=json';
     url += '&query=' + $.w.q.value;
     url += '&callback=' + id;
     if ($.a.site) {
      url += '&site=' + $.a.site;
     }
     $.f.runScript(url, id);
    }
   },
   renderResult: function(r) {
    for (var i = 0; i < r.ResultSet.Result.length; i++) {
     var li = document.createElement('LI');
     var a = document.createElement('A');
     a.innerHTML = r.ResultSet.Result[i].Title;
     a.href = r.ResultSet.Result[i].Url;
     a.target = '_blank';
     li.appendChild(a);
     $.w.r.appendChild(li);
    }
   },
   runScript : function(url, id) {
    var s = document.createElement('script');
    s.id = id;
    s.type ='text/javascript';
    s.src = url;
    document.getElementsByTagName('body')[0].appendChild(s);
   },
   removeScript : function(id) {
    var s = '';
    if (s = document.getElementById(id)) {
     s.parentNode.removeChild(s);
    }
   },
   parseJson : function(json) {
    this.parseJson.data = json;
    if ( typeof json !== 'string') {
     return {"err":"trying to parse a non-string JSON object"};
    }
    try {
     var f = Function(['var document,top,self,window,parent,Number,Date,Object,Function,',
      'Array,String,Math,RegExp,Image,ActiveXObject;',
      'return (' , json.replace(/<\!--.+-->/gim,'').replace(/\bfunction\b/g,'function­') , ');'].join(''));
       return f();
    } catch (e) {
     return {"err":"trouble parsing JSON object"};
    }
   }
  };
 }();
 var thisScript = /behavior.js/;
 if (typeof window.addEventListener !== 'undefined') {
  window.addEventListener('load', function() { $.f.init(thisScript); }, false);
 } else if (typeof window.attachEvent !== 'undefined') {
  window.attachEvent('onload', function() { $.f.init(thisScript); });
 }
})();

Don't run this yet; we need to fix up the structure.

how-to-send-input

Structure, With Input

<html>
 <head>
  <title>Badge Me!</title>
 </head>
 <body>
  <script src="behavior.js"></script>
  <script src="behavior.js">{ "site":"en.wikipedia.org" }</script>
  <script src="behavior.js">{ "query":"madonna", "site":"mtv.com" }</script>
 </body>
</html>

Run this, and you ought to see three separate search widgets, with different default sites and queries.

checkpoint

What Just Happened?

styling-it-up

Creating Stylesheets

( function() {
 var trueName = '';
 for (var i = 0; i < 16; i++) {
  trueName += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
 }
 window[trueName] = {};
 var $ = window[trueName];
 $.f = function() {
  return {
   init : function(target) {
    var theScripts = document.getElementsByTagName('SCRIPT');
    for (var i = 0; i < theScripts.length; i++) {
     if (theScripts[i].src.match(target)) {
      $.w = document.createElement('DIV');
      $.a = {};
      if (theScripts[i].innerHTML) {
       $.a = $.f.parseJson(theScripts[i].innerHTML);
      }
      if ($.a.err) {
       alert('bad json!');
      }
      $.d = {"background":"#fff", "border":"1px solid #000"};
      for (var k in $.d) {
       if ($.a[k] === undefined) {
        $.a[k] = $.d[k];
       }
      }
      var ns = document.createElement('style');
      document.getElementsByTagName('head')[0].appendChild(ns);
      if (!window.createPopup) {
       ns.appendChild(document.createTextNode(''));
       ns.setAttribute("type", "text/css");
      }
      var s = document.styleSheets[document.styleSheets.length - 1];
      var rules = {
       "" : "{zoom:1;padding:5px;margin:5px;background:" + $.a.background + ";border:" + $.a.border + "}",
       "button":"{margin-left:5px;}",
       "ul" : "{margin:0; padding:0;}",
       "ul li" : "{list-style:none;}",
      };
      var ieRules = "";
      for (r in rules) {
       var selector = '.' + trueName + ' ' + r;
       if (!window.createPopup) {
        var theRule = document.createTextNode(selector + rules[r]);
        ns.appendChild(theRule);
       } else {
        ieRules += selector + rules[r];
       }
      }
      if (window.createPopup) {
       s.cssText = ieRules;
      }
      $.w.className = trueName;
      $.w.q = document.createElement('INPUT');
      if ($.a.query) {
       $.w.q.value = $.a.query;
      }
      $.w.q.onkeypress = function(e) {
       if ( (e ? e.which : event.keyCode) == 13) {
        $.f.runSearch();
       }
      };
      $.w.appendChild($.w.q);
      $.w.b = document.createElement('BUTTON');
      $.w.b.innerHTML = 'Search';
      if ($.a.site) {
       $.w.b.innerHTML += ' ' + $.a.site;
      }
      $.w.b.onmouseup = function() {
       $.f.runSearch();
      };
      $.w.appendChild($.w.b);
      $.w.r = document.createElement('UL');
      $.w.appendChild($.w.r);
      theScripts[i].parentNode.insertBefore($.w, theScripts[i]);
      theScripts[i].parentNode.removeChild(theScripts[i]);
      break;
     }
    }
   },
   runSearch : function() {
    $.w.r.innerHTML = '';
    if ($.w.q.value) {
     if (!$.f.runFunction) {
      $.f.runFunction = [];
     }
     var n = $.f.runFunction.length;
     var id = trueName + '.f.runFunction[' + n + ']';
     $.f.runFunction[n] = function(r) {
      delete($.f.runFunction[n]);
      $.f.removeScript(id);
      $.f.renderResult(r);
     }
     var url = 'http://search.yahooapis.com/WebSearchService/V1/webSearch?';
     url += '&appid=YahooSearch';
     url += '&results=5';
     url += '&output=json';
     url += '&query=' + $.w.q.value;
     url += '&callback=' + id;
     if ($.a.site) {
      url += '&site=' + $.a.site;
     }
     $.f.runScript(url, id);
    }
   },
   renderResult: function(r) {
    for (var i = 0; i < r.ResultSet.Result.length; i++) {
     var li = document.createElement('LI');
     var a = document.createElement('A');
     a.innerHTML = r.ResultSet.Result[i].Title;
     a.href = r.ResultSet.Result[i].Url;
     a.target = '_blank';
     li.appendChild(a);
     $.w.r.appendChild(li);
    }
   },
   runScript : function(url, id) {
    var s = document.createElement('script');
    s.id = id;
    s.type ='text/javascript';
    s.src = url;
    document.getElementsByTagName('body')[0].appendChild(s);
   },
   removeScript : function(id) {
    var s = '';
    if (s = document.getElementById(id)) {
     s.parentNode.removeChild(s);
    }
   },
   parseJson : function(json) {
    this.parseJson.data = json;
    if ( typeof json !== 'string') {
     return {"err":"trying to parse a non-string JSON object"};
    }
    try {
     var f = Function(['var document,top,self,window,parent,Number,Date,Object,Function,',
      'Array,String,Math,RegExp,Image,ActiveXObject;',
      'return (' , json.replace(/<\!--.+-->/gim,'').replace(/\bfunction\b/g,'function­') , ');'].join(''));
       return f();
    } catch (e) {
     return {"err":"trouble parsing JSON object"};
    }
   }
  };
 }();
 var thisScript = /behavior.js/;
 if (typeof window.addEventListener !== 'undefined') {
  window.addEventListener('load', function() { $.f.init(thisScript); }, false);
 } else if (typeof window.attachEvent !== 'undefined') {
  window.attachEvent('onload', function() { $.f.init(thisScript); });
 }
})();

Don't run this yet; we're going to pass some different arguments in for the stylesheet creator.

some-sample-arguments

Here we're dropping in a couple of basic CSS selectors, background and border.

<html>
 <head>
  <title>Badge Me!</title>
 </head>
 <body>
  <script src="behavior.js"></script>
  <script src="behavior.js">{"site":"en.wikipedia.org", "background":"#ffa"}</script>
  <script src="behavior.js">{"query":"madonna", "site":"mtv.com", "border":"1px solid red"}</script>
 </body>
</html>

Save this and run it; we're almost done.

checkpoint

What Just Happened?

th-th-that's-all-folks!

The End

See Also:

http://kentbrewster.com/twitterati

http://kentbrewster.com/put-your-digg-in-a-box

Any Yahoo! Pipes Badge

Thank You:

Brady Forrest, Doug Crockford, Hedger Wang, Scott Schille, PPK, Isaac Schleuter, Brothercake, Nate Koechley, Thomas Sha, Matt Sweeney, and Dave Balmer.

Questions?

Meet Me at the Y! Booth This Afternoon