<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/atom10full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">

  <title><![CDATA[andrew makes things]]></title>
  
  <link href="http://blog.andrewcantino.com/" />
  <updated>2013-03-12T23:32:09-07:00</updated>
  <id>http://blog.andrewcantino.com/</id>
  <author>
    <name><![CDATA[Andrew Cantino]]></name>
    
  </author>
  <generator uri="http://octopress.org/">Octopress</generator>

  
  <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/atom+xml" href="http://feeds.feedburner.com/andrew-makes-things" /><feedburner:info uri="andrew-makes-things" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><entry>
    <title type="html"><![CDATA[Archive a PDF of your Posterous blog]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/w27qr69TKPs/" />
    <updated>2013-03-12T23:23:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2013/03/12/archive-a-pdf-of-your-posterous-blog</id>
    <content type="html">&lt;p&gt;My wife and I had a private travel blog on Posterous.  Unfortunately, Posterous got aquihired by Twitter and is shutting down, so I spent a few minutes figuring out how to save a PDF of the blog.  Chrome and Firefox did pretty poorly at saving a decent looking PDF for my long blog so I installed wkhtmltopdf and made one myself.  Here&amp;#8217;s how.&lt;/p&gt;

&lt;p&gt;Install &lt;a href="http://code.google.com/p/wkhtmltopdf/"&gt;wkhtmltopdf&lt;/a&gt;, then run:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;wkhtmltopdf --enable-plugins --margin-bottom 0 --margin-top 0 --margin-left 0 --margin-right 0 "http://yourblog.com" archive.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If your blog has a password, log in with a browser and copy the cookie.  Firefox makes this easy in the Developer Toolbar by entering &lt;code&gt;cookie list&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;wkhtmltopdf --cookie cookiename cookievalue ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, if you&amp;#8217;re on a Mac and used the pre-built wkhtmltopdf disk image, you can still use the command line.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/path/to/wkhtmltopdf.app/Contents/MacOS/wkhtmltopdf ...
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/w27qr69TKPs" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2013/03/12/archive-a-pdf-of-your-posterous-blog/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Command Line Accounting with Ledger and Reckon, an update]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/Dq9fdTD5e9w/" />
    <updated>2013-02-16T14:08:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2013/02/16/command-line-accounting-with-ledger-and-reckon</id>
    <content type="html">&lt;p&gt;I&amp;#8217;ve been using &lt;a href="http://ledger-cli.org/"&gt;ledger&lt;/a&gt;, combined with a custom Ruby gem called &lt;a href="https://github.com/cantino/reckon"&gt;reckon&lt;/a&gt;, to balance my small business&amp;#8217;s accounts for the &lt;a href="http://blog.andrewcantino.com/blog/2010/11/06/command-line-accounting-with-ledger-and-reckon/"&gt;last few years&lt;/a&gt;.  The command line, Bayesian statistics, and Double Entry Accounting!  What could be better?  Here&amp;#8217;s how I do it.&lt;/p&gt;

&lt;p&gt;First, I export the year&amp;#8217;s transaction history from Chase (in my case) and save it as a CSV file called &lt;code&gt;chase-2012.csv&lt;/code&gt;.  It looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Type,Post Date,Description,Amount
DEBIT,12/31/2012,"ODESK***BAL-27DEC12 650-12345 CA           12/28",-123.45
DEBIT,12/24/2012,"ODESK***BAL-20DEC12 650-12345 CA           12/21",-123.45
DEBIT,12/24/2012,"GH *GITHUB.COM     FP 12345 CA        12/23",-12.00
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then, I make a new ledger file called &lt;code&gt;2012.dat&lt;/code&gt; and start it with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;2012/01/01 * Checking
    Assets:Bank:Checking            $10,000.00
    Equity:Opening Balances
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Where the &lt;code&gt;$10,000.00&lt;/code&gt; is the hypothetical starting balance of my bank account on the first day of 2012.  Since I&amp;#8217;ve been using ledger, this is just the balance of the account from the summary that I generated at the end of 2011.&lt;/p&gt;

&lt;p&gt;Now, I run &lt;code&gt;reckon&lt;/code&gt;, initially with the &lt;code&gt;-p&lt;/code&gt; option to see its analysis of the CSV file:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; reckon -f chase-2012.csv -v -p --contains-header

What is the account name of this bank account in Ledger? |Assets:Bank:Checking| 
I didn't find a high-likelyhood money column, but I'm taking my best guess with column 4.
+------------+------------+----------------------------------------------------------+
| Date       | Amount     | Description                                              |
+------------+------------+----------------------------------------------------------+
| ...        | ...        | ...                                                      |
| 2012/12/24 | -$12.00    | DEBIT; GH *GITHUB.COM FP 12345 CA 12/23                  |
| 2012/12/24 | -$123.45   | DEBIT; ODESK***BAL-20DEC12 650-12345 CA 12/21            |
| 2012/12/31 | -$123.45   | DEBIT; ODESK***BAL-27DEC12 650-12345 CA 12/28            |
+------------+------------+----------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It looks like &lt;code&gt;reckon&lt;/code&gt; has guessed the correct columns from the CSV, so now I run it in &amp;#8220;learning&amp;#8221; mode.  It loads in my data from 2011 and uses it to guess at labels for my 2012 data, using a simple Naive Bayes classifier.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; reckon -f chase-2012.csv -v -o 2012.dat -l 2011.dat --contains-header

...
+------------+---------+------------------------------------------------+
| 2012/12/24 | -$12.00 | DEBIT; GH *GITHUB.COM FP 12345 CA 12/23        |
+------------+---------+------------------------------------------------+
To which account did this money go? ([account]/[q]uit/[s]kip) |Expenses:Web Hosting:Github| 
+------------+----------+-------------------------------------------------+
| 2012/12/24 | -$123.45 | DEBIT; ODESK***BAL-20DEC12 650-12345 CA 12/21   |
+------------+----------+-------------------------------------------------+
To which account did this money go? ([account]/[q]uit/[s]kip) |Expenses:Programming| 
+------------+----------+-------------------------------------------------+
| 2012/12/31 | -$123.45 | DEBIT; ODESK***BAL-27DEC12 650-12345 CA 12/28   |
+------------+----------+-------------------------------------------------+
To which account did this money go? ([account]/[q]uit/[s]kip) |Expenses:Programming|
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In each of these cases, the Bayesian classifier correctly guessed the appropriate label for these expenses based on last year&amp;#8217;s data.&lt;/p&gt;

&lt;p&gt;Now, with a fully-updated 2012.dat file, I run it through ledger and get the following hypothetical results:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; ledger -f 2012.dat -s bal

    $20,000   Assets:Bank:Checking
   $-10,000   Equity:Opening Balances
    $258.90   Expenses
    $246.90     Programming
     $12.00     Web Hosting
     $12.00       Github
   $-10,258.90  Income
   $-10,258.90    Some source of income that makes this math work
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I like to do all of this work inside of my Dropbox folder in case I delete or overwrite a file by mistake.&lt;/p&gt;

&lt;p&gt;Want to do all of this yourself?  Start by visiting &lt;a href="http://ledger-cli.org/"&gt;ledger-cli.org&lt;/a&gt;, or by installing ledger with homebrew:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; brew install ledger
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then install reckon:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;gt; (sudo) gem install reckon
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Have fun!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cantino/reckon"&gt;Reckon&lt;/a&gt; is available on GitHub, and, for updates, you should &lt;a href="https://twitter.com/tectonic"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/Dq9fdTD5e9w" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2013/02/16/command-line-accounting-with-ledger-and-reckon/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Running Ruby inside of Ruby (in the best way ever)]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/Wg-OGnIVdrQ/" />
    <updated>2013-01-01T13:39:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2013/01/01/running-ruby-inside-of-ruby-in-the-best-way-ever</id>
    <content type="html">&lt;p&gt;There are no good Ruby sandboxing options right now.  You can sort of use &lt;code&gt;$SAFE&lt;/code&gt; levels and &lt;a href="http://www.ruby-doc.org/docs/ProgrammingRuby/html/taint.html"&gt;taint checking&lt;/a&gt;, you can sort of use &lt;a href="https://github.com/tario/shikashi"&gt;Shikashi&lt;/a&gt;, you can use the &lt;a href="http://rubydoc.info/gems/secure"&gt;secure gem&lt;/a&gt; to run in a separate process, and you can, with much care, use chrooted and jailed virtual machines or &lt;a href="http://lxc.sourceforge.net/"&gt;Linux containers&lt;/a&gt;.  None of these options met my exacting standards, meaning they&amp;#8217;re not ridiculous.  Therefore, I&amp;#8217;m introducing…&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RubyOnRuby&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;An unholy amalgam of therubyracer&amp;#8217;s V8 engine and emscripted-ruby to allow a truly sandboxed Ruby-on-Ruby environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cantino/ruby_on_ruby"&gt;Check it out on GitHub!&lt;/a&gt;&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/Wg-OGnIVdrQ" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2013/01/01/running-ruby-inside-of-ruby-in-the-best-way-ever/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Make an expanding text UI with jQuery Expando]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/zFIyuTi4TQk/" />
    <updated>2012-09-21T20:34:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2012/09/21/make-an-expanding-text-ui-with-jquery-expando</id>
    <content type="html">&lt;script&gt;
  jQuery(function() {
    jQuery("#expando-example").expando();
  });
&lt;/script&gt;


&lt;p&gt;&lt;span id="expando-example"&gt;&lt;expando&gt;&lt;initial&gt;Recently&lt;/initial&gt;&lt;expanded&gt;Recently, after receiving a couple of &lt;expando&gt;&lt;initial&gt;requests&lt;/initial&gt;&lt;expanded&gt;friendly requests&lt;/expanded&gt;&lt;/expando&gt;&lt;/expanded&gt;&lt;/expando&gt;, I extracted the &lt;expando&gt;&lt;initial&gt;code&lt;/initial&gt;&lt;expanded&gt;jQuery code&lt;/expanded&gt;&lt;/expando&gt; powering the &lt;expando&gt;&lt;initial&gt;UI&lt;/initial&gt;&lt;expanded&gt;expanding text UI&lt;/expanded&gt;&lt;/expando&gt; of &lt;a href="http://andrewcantino.com"&gt;andrewcantino.com&lt;/a&gt;.  You can &lt;expando&gt;&lt;initial&gt;get it&lt;/initial&gt;&lt;expanded&gt;download or fork the source&lt;/expanded&gt;&lt;/expando&gt; on GitHub: &lt;a href="https://github.com/cantino/expando"&gt;https://github.com/cantino/expando&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/zFIyuTi4TQk" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2012/09/21/make-an-expanding-text-ui-with-jquery-expando/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Compressing Code]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/-waIiOi1Z80/" />
    <updated>2012-06-15T21:15:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2012/06/15/compressing-code</id>
    <content type="html">&lt;div class='empty'&gt;
  &lt;script type="text/javascript" src="http://www.google.com/jsapi"&gt;&lt;/script&gt;
  &lt;script type="text/javascript"&gt;
    google.load('visualization', '1', {packages: ['corechart']});
  &lt;/script&gt;
  &lt;script type="text/javascript"&gt;
    function drawVisualization() {
      var languageData = {
        Ruby: [
          ["octopress",3.109],
          ["homebrew",3.310],
          ["will_paginate",3.724],
          ["jekyll",3.909],
          ["cucumber",3.976],
          ["compass",4.076],
          ["bundler",4.087],
          ["sinatra",4.096],
          ["gollum",4.126],
          ["refinerycms",4.215],
          ["resque",4.217],
          ["ruby",4.217],
          ["diaspora",4.285],
          ["active_admin",4.452],
          ["spree",4.634],
          ["devise",4.961],
          ["paperclip",5.087],
          ["rails_admin",5.276],
          ["rails",5.327],
          ["capybara",5.335],
          ["cancan",5.434],
          ["authlogic",5.666],
          ["fog",5.787],
          ["mongoid",6.714]
        ],

        CPP: [
          ["v8",3.031],
          ["node",3.049],
          ["bitcoin",4.037],
          ["wkhtmltopdf",4.081],
          ["mongo",4.264],
          ["scribe",4.504],
          ["mysql",4.977],
          ["doom3.gpl",5.024],
          ["phantomjs",5.501]
        ],

        Java: [
          ["hudson",4.558],
          ["hadoop",4.952],
          ["grails-core",5.466],
          ["clojure",5.930],
          ["cassandra",6.601],
          ["netty",6.692],
          ["voldemort",6.986],
          ["spring-framework",7.043],
          ["storm",9.340]
        ],

        JS: [
          ["Skeleton",1.648],
          ["mustache.js",2.838],
          ["impress.js",3.102],
          ["pdf.js",3.245],
          ["three.js",3.371],
          ["underscore",3.410],
          ["zepto",3.505],
          ["raphael",3.614],
          ["backbone",3.630],
          ["history.js",3.752],
          ["UglifyJS",3.788],
          ["headjs",3.866],
          ["scriptaculous",4.044],
          ["less.js",4.105],
          ["handlebars.js",4.199],
          ["jquery",4.202],
          ["ace",4.207],
          ["bootstrap",4.219],
          ["d3",4.413],
          ["prototype",4.511],
          ["ember.js",4.549],
          ["jasmine",4.872],
          ["async",4.978],
          ["express",5.145],
          ["mongoose",5.735],
          ["socket.io",6.719]
        ],

        PHP: [
          ["WordPress",4.373],
          ["twitteroauth",4.408],
          ["pyrocms",4.559],
          ["foundation",5.067],
          ["Slim",5.552],
          ["symfony",6.337],
          ["zf2",6.580],
          ["cakephp",6.839]
        ],

        C: [
          ["git",3.758],
          ["redis",4.194],
          ["memcached",4.539],
          ["ccv",4.659],
          ["linux",4.847],
          ["yajl",4.983],
          ["nginx",5.352],
          ["php-src",8.752]
        ],

        Python: [
          ["webpy",2.943],
          ["fabric",3.573],
          ["reddit",3.587],
          ["blueprint",3.962],
          ["tornado",4.279],
          ["flask",4.415],
          ["django",4.901]
        ]
      };

      var summary = [
        ["Python",3.952],
        ["JS",4.064],
        ["CPP",4.274],
        ["Ruby",4.584],
        ["C",5.136],
        ["PHP",5.464],
        ["Java",6.396]
      ];

      var crossLanguageSummary = google.visualization.arrayToDataTable([["", ""]].concat(summary.reverse()));
      new google.visualization.BarChart(jQuery("#cross-language").get(0)).draw(crossLanguageSummary,
            {title:"Average Compressability by Language",
              width: 400, height: 300,
              legend: { position: "none" },
              chartArea: { width: "80%", height: "80%" }
            });


      jQuery.each(languageData, function(name, value) {
        var data = google.visualization.arrayToDataTable([["", ""]].concat(languageData[name].reverse()));
        var $elem = jQuery("&lt;div&gt;&lt;/div&gt;").addClass("visualization").addClass(name).css({ width: "600px", height: "400px", display: "none" });
        jQuery("#visualizations").append($elem);
        jQuery("#viz-links").append(jQuery('&lt;a href="#"&gt;&lt;/a&gt;').text(name).click(function(e) {
          e.preventDefault();
          jQuery("#visualizations .visualization").hide();
          $elem.show();
          return false;
        }));

        new google.visualization.BarChart($elem.get(0)).draw(data,
               {title:"Compressability of " + name + " Libraries",
                width: 600, height: 500,
                legend: { position: "none" },
                hAxis: { maxValue: 7, minValue: 2, viewWindowMode: 'explicit', viewWindow: { max: 7, min: 2 } },
                chartArea: { width: "70%", height: "75%" }
               });
      });

      jQuery("#visualizations .visualization.Ruby").show();
    }

    google.setOnLoadCallback(drawVisualization);
  &lt;/script&gt;

  &lt;style&gt;
    .empty {
      width: 0;
      height: 0;
      padding: 0;
      margin: 0;
    }

    #viz-links a {
      padding: 10px;
    }

    #visualizations {
      width: 600px;
      height: 500px;
    }

    #cross-language {
      width: 400px;
      height: 300px;
    }

    .cross-language-wrapper {
      margin: 8px 20px 10px 20px;
      float: right;
      width: 385px;
      height: 355px;
      padding-left: 5px;
      font-size: 13px;
      font-style: italic;
      text-align: center;
      overflow: hidden;
      background-color: white;
    }

    .box {
      box-shadow: 2px 2px 9px #999;
      border: 1px solid #999;
    }

    .results {
      clear: both;
      margin: 10px;
      padding: 10px;
      width: 600px;
      overflow: hidden;
      height: 500px;
    }
  &lt;/style&gt;
&lt;/div&gt;


&lt;p&gt;What can we learn about a code base or a language based on its compressibility?  My pet theory is that less compressible code will be, on average, better code, because less compressible code implies more factoring, more reuse, and fewer repetitions.&lt;/p&gt;

&lt;div class='cross-language-wrapper box'&gt;
  &lt;div id='cross-language'&gt;&lt;/div&gt;
  &lt;div&gt;
    Larger numbers (longer lines) indicate more compressibility.
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Below are the compressibility results for some popular libraries and languages.  To generate this data, I downloaded each package or library, extracted the source files, removed comments, and gzipped the files with maximum compression.  The numbers represent the ratio of uncompressed source file size to compressed source file size.  Smaller ratios imply less compressible code.&lt;/p&gt;

&lt;p&gt;Unsurprisingly, Java was the most compressible language- all that boilerplate!  Python was the least compressible language, with Java, on average, being about twice as compressible.  I was somewhat surprised that JavaScript was the second best language in terms of incompressibility.&lt;/p&gt;

&lt;p&gt;It&amp;#8217;s probably disingenuous to draw strong conclusions from these results, but I still find them intriguing.  What, if anything, do you think they mean?&lt;/p&gt;

&lt;div class='results box'&gt;
  &lt;div id="viz-links"&gt;
    &lt;span&gt;View data for:&lt;/span&gt;
  &lt;/div&gt;

  &lt;div id="visualizations"&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;br /&gt;


&lt;p&gt;&lt;strong&gt;Aside:&lt;/strong&gt; Compression itself is a fascinating subject.  Being able to compress something is fundamental to being able to understanding it.  If you can rewrite a 2 page document into 2 paragraphs, while still expressing its core ideas, you&amp;#8217;ve deeply understood the material.  Hence the existence of the &lt;a href="http://en.wikipedia.org/wiki/Hutter_Prize"&gt;Hutter Prize&lt;/a&gt;, a standing challenge in Artificial Intelligence to further compress a corpus of English text.  Hence, also, the existence of &lt;a href="http://www.cs.technion.ac.il/~elad/publications/journals/2007/FaceCompress_KSVD_JVCIR.pdf"&gt;specialized image compression algorithms&lt;/a&gt; that compress human faces better than anything else because they understand, in software, what a human face generally looks like.&lt;/p&gt;

&lt;p&gt;If I can compress an image of your face, I can probably also recognize it.  Imagine that I have thousands of photos of faces.  Using some linear algebra and creative encodings, I can figure out the commonalities and differences among these faces.  Basically, I can derive a set of common noses, a set of common eyes, a set of common brows&amp;#8230; and, given a new face photo, I can compute a mixture of these common attributes for the new face.  Perhaps it, roughly speaking, has 60% of Common Nose 6 and 40% of Common Nose 12.  Well, then I can represent the picture of this new nose as roughly two numbers, the &amp;#8220;amounts&amp;#8221; of Nose 6 and of Nose 12, and suddenly I&amp;#8217;ve compressed a collection of hundreds or thousands of pixels- a photo of a nose- into just two numbers.  We could also go in reverse, taking a photo, calculating the percentages of different common features, and then looking up in a database to see who we know whose face expresses those same feature percentages.  We can compress, and, thus, we can recognize.  (Interested in this?  See &lt;a href="http://en.wikipedia.org/wiki/Eigenface"&gt;Eigenfaces&lt;/a&gt; as well as &lt;a href="http://www.face-rec.org/algorithms/Comparisons/draper_cviu.pdf"&gt;PCA and ICA for face analysis&lt;/a&gt;.)&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/-waIiOi1Z80" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2012/06/15/compressing-code/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[My experiences with personal outsourcing]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/7PH16Fxnoi4/" />
    <updated>2012-06-10T16:42:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2012/06/10/on-lifesourcing</id>
    <content type="html">&lt;p&gt;Over the last few years I&amp;#8217;ve been experimenting with outsourcing.  I&amp;#8217;ve done this both for personal and business projects.  In the personal domain, some people call this &amp;#8220;lifesourcing&amp;#8221;: the practice of modularizing and outsourcing parts of your life that you don&amp;#8217;t enjoy so that you can maximize the parts that you do.  It&amp;#8217;s outsourcing (with many of the same pros and cons), but for your personal life.&lt;/p&gt;

&lt;p&gt;A growing number of sites have popped up recently to facilitate lifesourcing, and while these sites aren&amp;#8217;t strictly needed- you can still find skilled people to help you on Craigslist, for example- they make this sort of outsourcing even easier.&lt;/p&gt;

&lt;p&gt;I&amp;#8217;d like to talk about some tips and tricks, but first, let me list a few of the things that I have outsourced over the last couple of years.&lt;/p&gt;

&lt;h3&gt;Personal Things&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;My wife and I outsourced hand written, cursive wedding invitations on &lt;a href="http://taskrabbit.com/PAL/29828"&gt;TaskRabbit&lt;/a&gt;.  (My mother-in-law preferred them to be hand written, my hand writing stinks, and my wife didn&amp;#8217;t have the time.)&lt;/li&gt;
&lt;li&gt;Also for our wedding, someone on &lt;a href="http://fiverr.com"&gt;Fiverr&lt;/a&gt; polished our save-the-date photo.&lt;/li&gt;
&lt;li&gt;After the wedding, we hired a wonderful, well-traveled woman on TaskRabbit to help plan our honeymoon in South America.&lt;/li&gt;
&lt;li&gt;We paid a TaskRabbit to scan all of our wedding cards for posterity.&lt;/li&gt;
&lt;li&gt;Currently, we have a virtual assistant from &lt;a href="http://odesk.com"&gt;oDesk&lt;/a&gt; who helps us with the ocasional life task.  She has proofread documents and called gyms around San Francisco, looking for ones with good pools.&lt;/li&gt;
&lt;li&gt;We&amp;#8217;ve given friends custom, hand painted watercolor birthday cards from &lt;a href="http://fiverr.com"&gt;Fiverr&lt;/a&gt; and custom wedding presents from &lt;a href="http://etsy.com"&gt;Etsy&lt;/a&gt;.  (Which, one couple swears, is their favorite wedding present!)  We designed the art (roughly) and then it was made with skill by the artists.&lt;/li&gt;
&lt;li&gt;When I have outstanding questions, I turn to Aardvark, Yahoo! Answers, and other outsourced question answering services.  I would gladly pay for a better one.&lt;/li&gt;
&lt;li&gt;A carpenter on Craigslist designed and built a custom, adjustable standing desk for me.  (Arguably outsourcing, arguably not.)&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;(Micro-)business Things&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I&amp;#8217;ve hired artists and logo designers on Fiverr.  (I actually bought some excellent artwork and had an ongoing business relationship with an artist who I found for $5 on Fiverr.)&lt;/li&gt;
&lt;li&gt;Workers on oDesk and TaskRabbit have helped me brainstorm domain names.&lt;/li&gt;
&lt;li&gt;I&amp;#8217;ve paid users on Mechanical Turk and later on oDesk to label data for me for some Machine Learning research.&lt;/li&gt;
&lt;li&gt;I&amp;#8217;ve brainstormed with a worker from &lt;a href="http://www.coffeeandpower.com"&gt;Coffee &amp;amp; Power&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;oDeskers have also written software, done graphics, wrote blogs, sent emails, maintained website communities, and researched ideas for me.&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;Okay, so clearly I&amp;#8217;ve experimented with this a fair bit.  Here are some of the things that I&amp;#8217;ve learned:&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Outsourcers take maintenance.  It only makes sense to outsource something that would take you more than some threshold amount of time to do yourself.  Repetitive tasks are great candidates.&lt;/li&gt;
&lt;li&gt;Be creative- what could you do, if only you had the time?&lt;/li&gt;
&lt;li&gt;Outsource things you&amp;#8217;re bad at, or simply hate doing.&lt;/li&gt;
&lt;li&gt;Art is a great thing to outsource.  Finding someone&amp;#8217;s work that you like on Etsy is fun and addictive and custom gifts make a lasting impression, often costing the same as something far more mundane.&lt;/li&gt;
&lt;li&gt;If you&amp;#8217;re trying to get art off of Fiverr, I recommend contacting 5-10 different providers, having them all do the work for $5 each, and then continuing to work with your favorite.  This same strategy, of redundant hiring and then consolidating, works well across many lifesourcing and outsourcing domains.&lt;/li&gt;
&lt;li&gt;You should think about hiring people on oDesk in the same way as you would any other interview process.  Ask to see work, look at portfolios, and, ideally, provide interview challenges that directly map to the work they will be doing for you.  In my case, when I hired someone to maintain one of my websites, my interview questions revolved around writing example emails and deciding which links were worth posting.  When I hired people to classify a dataset, I gave them access to the real classification application and had them do a sample set.  If they did well, I hired them.&lt;/li&gt;
&lt;li&gt;If you&amp;#8217;re going to go through the trouble of interviewing and hiring on oDesk, I strongly recommend codifying your interview and training instructions as reusable documents.  When your current worker(s) inevitably leave or flake out, you can hire and train the next set more quickly.  You can also hire more than one at a time for added redundancy.&lt;/li&gt;
&lt;li&gt;There are a shockingly large number of people on this planet who speak (nearly) perfect English, have sharp wits, and are looking for work.  If you have tasks that you can pay them a fair wage to solve, you&amp;#8217;re helping everyone.  And remember, a fair wage in the Philippines (where many people speak English perfectly), is significantly less than in the US.  Do your cost of living research and pay fairly and generously!&lt;/li&gt;
&lt;li&gt;Accountability and incentives are important.  I left Mechanical Turk and instead interviewed and hired individuals off oDesk for data labeling tasks because I received better quality and had more consistency over who I was working with.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Are you a lifesourcer or a micro-outsourcer?  What have you learned?&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/7PH16Fxnoi4" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2012/06/10/on-lifesourcing/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Machine Learning Project Ideas]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/jJJyYeSRO3A/" />
    <updated>2012-04-22T15:56:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2012/04/22/machine-learning-project-ideas</id>
    <content type="html">&lt;p&gt;&lt;a href="http://twitter.com/#!/ryanstout"&gt;Ryan Stout&lt;/a&gt; and I are giving a &lt;a href="http://railsconf2012.com/sessions/14"&gt;talk&lt;/a&gt; at RailsConf about Machine Learning tomorrow.  To go along with the talk, here is a list of project ideas to get your creative juices flowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A robust email and mailing address typo corrector for web forms.&lt;/li&gt;
&lt;li&gt;A Rickroll detecting browser plugin- it warns you before you follow a link that will likely result in Rickrolling.  (Rickroll Protection As A Service?)&lt;/li&gt;
&lt;li&gt;A per-user clicktrail analyzer that predicts which links a user is most likely to follow, given their history.  Use this to highlight or promote high-likelihood links.&lt;/li&gt;
&lt;li&gt;A user info and usage pattern analyzer that classifies users by likelihood of upgrading to a premium plan.&lt;/li&gt;
&lt;li&gt;A RubyGem for classifying user generated content into appropriate, inappropriate, spam, NSFW, etc.&lt;/li&gt;
&lt;li&gt;Along the same lines: a nudity detector for uploaded images.&lt;/li&gt;
&lt;li&gt;A RubyGem for code optimization based on the current backtrace, possibly using reinforcement learning.  For example:&lt;/li&gt;
&lt;/ul&gt;


&lt;figure class='code'&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;div class="highlight"&gt;&lt;table&gt;&lt;tr&gt;&lt;td class="gutter"&gt;&lt;pre class="line-numbers"&gt;&lt;span class='line-number'&gt;1&lt;/span&gt;
&lt;span class='line-number'&gt;2&lt;/span&gt;
&lt;span class='line-number'&gt;3&lt;/span&gt;
&lt;span class='line-number'&gt;4&lt;/span&gt;
&lt;span class='line-number'&gt;5&lt;/span&gt;
&lt;span class='line-number'&gt;6&lt;/span&gt;
&lt;span class='line-number'&gt;7&lt;/span&gt;
&lt;span class='line-number'&gt;8&lt;/span&gt;
&lt;span class='line-number'&gt;9&lt;/span&gt;
&lt;span class='line-number'&gt;10&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;td class='code'&gt;&lt;pre&gt;&lt;code class='ruby'&gt;&lt;span class='line'&gt;  &lt;span class="c1"&gt;# This probabilistically selects a choice based on the&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;  &lt;span class="c1"&gt;# current backtrace and the history of reinforcement signals seen.&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;  &lt;span class="n"&gt;optimize&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;      &lt;span class="c1"&gt;# some code path that ultimately triggers a &amp;quot;reward&amp;quot; or &amp;quot;punishment&amp;quot; signal&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;      &lt;span class="c1"&gt;# some other code path&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;


&lt;ul&gt;
&lt;li&gt;A story karma predictor that estimates the final score on Hacker News of any article, based on textual content and the poster&amp;#8217;s info.&lt;/li&gt;
&lt;li&gt;A system that classifies support requests by their estimated severity.&lt;/li&gt;
&lt;li&gt;Make things easier for your users:

&lt;ul&gt;
&lt;li&gt;given them default settings selected by users similar to themselves&lt;/li&gt;
&lt;li&gt;default to pages they use often; expand modules they interact with frequently&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Once you know what&amp;#8217;s possible, it&amp;#8217;s hard to find a project that &lt;em&gt;wouldn&amp;#8217;t&lt;/em&gt; benefit from some machine learning.&lt;/p&gt;

&lt;p&gt;Have other ideas?  Want to discuss these?  Post them in the comments and follow &lt;a href="http://twitter.com/tectonic"&gt;@tectonic&lt;/a&gt; for updates.&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/jJJyYeSRO3A" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2012/04/22/machine-learning-project-ideas/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Fixing the Chrome background refresh bug]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/b_zwtObTOuY/" />
    <updated>2012-02-15T11:55:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2012/02/15/fixing-the-chrome-background-refresh-bug</id>
    <content type="html">&lt;p&gt;There is a &lt;a href="http://code.google.com/p/chromium/issues/detail?id=111218#makechanges"&gt;bug in the current version of Chromium&lt;/a&gt; (hence Google Chrome) that sometimes fails to redraw CSS background images when they&amp;#8217;re hidden and then re-shown.  This issue appeared on &lt;a href="http://mavenlink.com/tour"&gt;Mavenlink&amp;#8217;s Tour&lt;/a&gt; page.  Thomas Fuchs &lt;a href="http://mir.aculo.us/2011/12/07/the-case-of-the-disappearing-element/"&gt;discusses some possible solutions&lt;/a&gt;, but none of those worked for us.  Here is our ugly solution:&lt;/p&gt;

&lt;div&gt;&lt;script src='https://gist.github.com/1838523.js?file='&gt;&lt;/script&gt;
&lt;noscript&gt;&lt;pre&gt;&lt;code&gt;function refreshBackgrounds(selector) {
  // Chrome shim to fix http://groups.google.com/a/chromium.org/group/chromium-bugs/browse_thread/thread/1b6a86d6d4cb8b04/739e937fa945a921
  // Remove this once Chrome fixes its bug.
  if (/chrome/.test(navigator.userAgent.toLowerCase())) {
    $(selector).each(function() {
      var $this = $(this);
      if ($this.css(&amp;quot;background-image&amp;quot;)) {
        var oldBackgroundImage = $this.css(&amp;quot;background-image&amp;quot;);
        setTimeout(function() {
          $this.css(&amp;quot;background-image&amp;quot;, oldBackgroundImage);
        }, 1);
      }
    });
  }
}

// You'll need to call this every time the event occurs that exposes the bug, such as changing tab divs.
refreshBackgrounds(&amp;quot;.something-with-a-background-image&amp;quot;);
refreshBackgrounds(&amp;quot;*&amp;quot;); // but it'll be slow!&lt;/code&gt;&lt;/pre&gt;&lt;/noscript&gt;&lt;/div&gt;


&lt;p&gt;Please let me know if you find something better!&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/b_zwtObTOuY" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2012/02/15/fixing-the-chrome-background-refresh-bug/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Hacking Google for fun and profit]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/ZPYevRQdu-I/" />
    <updated>2011-12-14T20:37:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2011/12/14/hacking-google-for-fun-and-profit</id>
    <content type="html">&lt;p&gt;At the end of last year, Google announced their &lt;a href="http://googleonlinesecurity.blogspot.com/2010/11/rewarding-web-application-security.html"&gt;Vulnerability Reward Program&lt;/a&gt; which rewards security researchers for reported security and privacy holes in Google properties.  This sounded like an interesting challenge, and I set out to find security holes.  I found three, got paid, and am now in the &lt;a href="http://www.google.com/about/corporate/company/halloffame.html"&gt;Google Security Hall of Fame&lt;/a&gt;. All in all, a rewarding experience.&lt;/p&gt;

&lt;p&gt;Below I describe the three security holes that I found.&lt;/p&gt;

&lt;h2&gt;Determining if a user has emailed another user&lt;/h2&gt;

&lt;p&gt;In my opinion, this is the most subtle, but also the most disturbing, of the three bugs.  As with the other bugs that I found, this was an example of &lt;a href="http://en.wikipedia.org/wiki/Cross-site_request_forgery"&gt;Cross Site Request Forgery&lt;/a&gt;- the practice of convincing a user&amp;#8217;s browser to make a request on their behalf to a remote server.  This type of attack generally only works when the user is logged in to the remote service.  In this case, if a user is already logged into Gmail (and they usually are), a malicious website could make a series of requests for Gmail profile images and, based on the return codes, determine whether or not the visitor had communicated with another Gmail user.  This worked because Gmail, as a well-intentioned privacy measure, would only show profile images to a viewer if they had had mutual contact.  Here is some example code that worked at the time:&lt;/p&gt;

&lt;figure class='code'&gt;&lt;figcaption&gt;&lt;span&gt;checkUsername&lt;/span&gt;&lt;/figcaption&gt;&lt;div class="highlight"&gt;&lt;table&gt;&lt;tr&gt;&lt;td class="gutter"&gt;&lt;pre class="line-numbers"&gt;&lt;span class='line-number'&gt;1&lt;/span&gt;
&lt;span class='line-number'&gt;2&lt;/span&gt;
&lt;span class='line-number'&gt;3&lt;/span&gt;
&lt;span class='line-number'&gt;4&lt;/span&gt;
&lt;span class='line-number'&gt;5&lt;/span&gt;
&lt;span class='line-number'&gt;6&lt;/span&gt;
&lt;span class='line-number'&gt;7&lt;/span&gt;
&lt;span class='line-number'&gt;8&lt;/span&gt;
&lt;span class='line-number'&gt;9&lt;/span&gt;
&lt;span class='line-number'&gt;10&lt;/span&gt;
&lt;span class='line-number'&gt;11&lt;/span&gt;
&lt;span class='line-number'&gt;12&lt;/span&gt;
&lt;span class='line-number'&gt;13&lt;/span&gt;
&lt;span class='line-number'&gt;14&lt;/span&gt;
&lt;span class='line-number'&gt;15&lt;/span&gt;
&lt;span class='line-number'&gt;16&lt;/span&gt;
&lt;span class='line-number'&gt;17&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;td class='code'&gt;&lt;pre&gt;&lt;code class='javascript'&gt;&lt;span class='line'&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;checkUsername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;          &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;      &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;https://mail.google.com/mail/photos/&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;%40gmail.com?1&amp;amp;rp=1&amp;amp;pld=1&amp;amp;r=&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;&lt;span class="nx"&gt;checkUsername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fbi-reports&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasEmailed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;  &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The current visitor &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasEmailed&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;has&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;has not&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; emailed the FBI.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;&lt;span class="nx"&gt;checkUsername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;wikileaks&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasEmailed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;  &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The current visitor &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasEmailed&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;has&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;has not&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; emailed WikiLeaks.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;


&lt;p&gt;&lt;/p&gt;

&lt;p&gt;It should be clear why this is a serious privacy concern.  If you suspected someone of being a whistleblower, for example, you could make a page that probed a bunch of revealing email addresses and checked to see if any had been contacted.  Luckily, Google reports that they have now fixed this bug.  Cross Site Request Forgery attacks can usually be prevented by adding a &lt;a href="http://en.wikipedia.org/wiki/Cross-site_request_forgery#Other_approaches_to_CSRF"&gt;CSRF&lt;/a&gt; token (a unique and user-specific token) to every request.&lt;/p&gt;

&lt;h2&gt;Identification of a user&amp;#8217;s Gmail address&lt;/h2&gt;

&lt;p&gt;This bug would have allowed a malicious website to determine your Google username if you were simultaneously logged into your Google account and typed anything into a seemingly innocuous web form.  One of the fields in the form would actually be an iframe pointing to a public Google Document.  When the user typed into the field, they would really be entering text into the Google Document, and what appeared to be their cursor in the field would actually be the Google Document insertion point.  When a user typed into the field, the attacker could determine their username (and hence email address) by observing the publicly-displayed list of current document editors.&lt;/p&gt;

&lt;p&gt;Again, this is a type of Cross Site Request Forgery, specifically known as &lt;a href="http://en.wikipedia.org/wiki/Clickjacking"&gt;Clickjacking&lt;/a&gt;, which can be especially hard to prevent.  There are many types of Clickjacking, almost all of which use iframes.  One approach, which I used here, is to artfully display content from a target site in such a way as to look like it&amp;#8217;s part of the current page. Another approach is to hide the iframe invisibly under the user&amp;#8217;s cursor, moving it as the cursor moves, and causing the user to click on the other site without realizing it.&lt;/p&gt;

&lt;p&gt;Google correctly used the &lt;a href="http://msdn.microsoft.com/en-us/library/dd565647.aspx"&gt;X-XSS-Protection&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header"&gt;X-Frame-Options&lt;/a&gt; headers, but some browsers do not honor these.  The solution to this one is tricky, but it is generally to use &lt;a href="http://en.wikipedia.org/wiki/Framekiller"&gt;frame busting&lt;/a&gt;, to provide appropriate headers, to use CSRF tokens, and to not expose any user information without a direct user interaction.&lt;/p&gt;

&lt;h2&gt;Deletion of all future email&lt;/h2&gt;

&lt;p&gt;The third bug that I found was a fairly severe security hole that affected a portion of Gmail users.  Due to a missing CSRF token during the first step of the filter creation flow in the HTML-only version of Gmail, a malicious site could trick visitors into creating a Gmail filter that would delete all future received email.  This worked in the current (at the time) version of Firefox, but not in Chrome or Safari due to their correct handling of the x-frame-options header.  I didn&amp;#8217;t test it in IE.&lt;/p&gt;

&lt;p&gt;This security hole was exploitable via a combination of a classic Cross Site Request Forgery with a Clickjacking attack.  First, I discovered that it was possible to submit the first part of the filter creation flow in an iframe using JavaScript because Google had forgotten to include a unique CSRF token in the form.&lt;/p&gt;

&lt;figure class='code'&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;div class="highlight"&gt;&lt;table&gt;&lt;tr&gt;&lt;td class="gutter"&gt;&lt;pre class="line-numbers"&gt;&lt;span class='line-number'&gt;1&lt;/span&gt;
&lt;span class='line-number'&gt;2&lt;/span&gt;
&lt;span class='line-number'&gt;3&lt;/span&gt;
&lt;span class='line-number'&gt;4&lt;/span&gt;
&lt;span class='line-number'&gt;5&lt;/span&gt;
&lt;span class='line-number'&gt;6&lt;/span&gt;
&lt;span class='line-number'&gt;7&lt;/span&gt;
&lt;span class='line-number'&gt;8&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;td class='code'&gt;&lt;pre&gt;&lt;code class='html'&gt;&lt;span class='line'&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;form&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;POST&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;iframe&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;https://mail.google.com/mail/h/ignored/?v=prf&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;enctype=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;multipart/form-data&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;cf1_hasnot&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;adfkjhsdf&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;s&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;z&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;cf2_tr&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;true&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;cf1_attach&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;false&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;nvp_bu_nxsb&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;Next Step&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;display: none&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;


&lt;p&gt;I then positioned the iframe such that the &amp;#8220;Create Filter&amp;#8221; button on the subsequent page would fill the frame without showing the button border; only the word &amp;#8220;Create&amp;#8221; was visible.  A fake button was then shown around the iframe with a style that matched the gmail style such that when the user believed they were submitting a form with a submit button entitled &amp;#8220;Create,&amp;#8221; they were really creating a malicious and destructive filter in Gmail.&lt;/p&gt;

&lt;p&gt;Google says this has now been fixed.&lt;/p&gt;

&lt;h2&gt;Google&amp;#8217;s Response&lt;/h2&gt;

&lt;p&gt;In all three cases, Google responded promptly to my security report and fixed the bug within a reasonable amount of time.  I was given two $500 awards for the three bugs.  Google generously doubled these amounts when I chose to donate them to charity, so the &lt;a href="http://www.athensconservancy.org/"&gt;Athens Conservency&lt;/a&gt; and the &lt;a href="http://www.buckeyeforestcouncil.org/"&gt;Buckeye Forest Council&lt;/a&gt;, two of my favorite local charities in Athens, OH, received one thousand dollars each, care of Google.&lt;/p&gt;

&lt;p&gt;These were subtle bugs.  They took trial and error to find.  However, in total, I only spent a few spare evenings of my time.  If Google&amp;#8217;s products- some of the most secure in the world- are susceptible to these sorts of attacks, you can bet many others are as well.  Every programer makes these mistakes sometimes.  Security is too complicated for anyone to get right all of the time.  Check your code!&lt;/p&gt;

&lt;h2&gt;Take your security into your own hands&amp;#8230; or, why you should hack Google too!&lt;/h2&gt;

&lt;p&gt;Many companies try to silence security bug reporters through legal threats and sometimes even action, driving discoverers of bugs underground and onto the black market where such knowledge can do real harm.  Google has set an admirable example by creating a program that is enlightened, responsive, and well-run, and I hope other companies move in the same direction.&lt;/p&gt;

&lt;p&gt;I had a great time using &lt;a href="http://jsFiddle.net"&gt;jsFiddle&lt;/a&gt; to explore and demonstrate bugs.  You can do the same&amp;#8211; check out their &lt;a href="http://googleonlinesecurity.blogspot.com/2010/11/rewarding-web-application-security.html"&gt;guidelines&lt;/a&gt; and do your part to improve the security of products that you love.&lt;/p&gt;

&lt;p&gt;Enjoyed this post?  You should &lt;a href="https://twitter.com/intent/follow?original_referer=http%3A%2F%2Fblog.andrewcantino.com%2Fblog%2F2011%2F12%2F14%2Fhacking-google-for-fun-and-profit%2F&amp;region=follow_link&amp;screen_name=tectonic&amp;source=followbutton&amp;variant=2.0"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/ZPYevRQdu-I" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2011/12/14/hacking-google-for-fun-and-profit/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[How to make your Rails app tweet the Twitter]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/EsokS-bU_wQ/" />
    <updated>2011-05-12T17:35:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2011/05/12/how-to-make-your-rails-app-tweet-the-twitter</id>
    <content type="html">&lt;p&gt;Suppose you want to build a Rails application for tracking popular links, and you want it to post the most popular links to Twitter automatically.  This quick tutorial will show you how to do that using the newest version of the Ruby &lt;a href="https://github.com/jnunemaker/twitter"&gt;twitter gem&lt;/a&gt;.  A little while ago I added the ability for &lt;a href="http://absurdlycool.com"&gt;Freebies Finder&lt;/a&gt; to tweet popular freebies.  I recently had to do this for another site and decided that a tutorial was in order.&lt;/p&gt;

&lt;h2&gt;Setup the accounts&lt;/h2&gt;

&lt;p&gt;We&amp;#8217;ll pretend that our website is called AwesomeLinks.com.  &lt;a href="https://twitter.com/signup" target="_blank"&gt;Signup for two Twitter accounts&lt;/a&gt;, AwesomeLinks and AwesomeLinksDev.  We need to create a Twitter application through which our website can post to these accounts.  Do this by logging into AwesomeLinks and visiting &lt;a href="https://dev.twitter.com/apps/new"&gt;https://dev.twitter.com/apps/new&lt;/a&gt;.  Select &amp;#8216;Client&amp;#8217; as the Application Type, skip the Callback URL, and select Read &amp;amp; Write access.  Twitter will give you a OAuth Consumer key and secret, which you will soon need.&lt;/p&gt;

&lt;!--more--&gt;


&lt;h2&gt;Getting your OAuth Token and Secret&lt;/h2&gt;

&lt;p&gt;Now you need to authorize your new Twitter Application to post on both of your Twitter accounts.  For this, we use a script:&lt;/p&gt;

&lt;script src="https://gist.github.com/969776.js?file=get_token.rb"&gt;&lt;/script&gt;


&lt;p style="font-size: 0.8em"&gt;(If you used the Twitter gem in the past, you may have used &lt;code&gt;authorize_from_access&lt;/code&gt; for this, but that no longer works.  We now have to require and use oauth separately.)&lt;/p&gt;


&lt;p&gt;Fill in your Twitter Application&amp;#8217;s Consumer key and secret and run the script.  You will be prompted to visit a URL and then to enter the PIN that Twitter provides.  Do this for both of your new Twitter accounts and record the results in &lt;code&gt;config/twitter.yml&lt;/code&gt;, like so:&lt;/p&gt;

&lt;script src="https://gist.github.com/969776.js?file=twitter.yml"&gt;&lt;/script&gt;


&lt;h2&gt;Initializing the Twitter gem&lt;/h2&gt;

&lt;p&gt;Now, create an initializer in &lt;code&gt;config/initializers/twitter.rb&lt;/code&gt; and again include your Twitter App&amp;#8217;s key and secret:&lt;/p&gt;

&lt;script src="https://gist.github.com/969776.js?file=twitter_initializer.rb"&gt;&lt;/script&gt;




&lt;p style="font-size: 0.8em"&gt;(If you want to Tweet to multiple accounts, you can do this differently and instead make separate &lt;code&gt;Twitter::Client&lt;/code&gt; objects, each with their own OAuth tokens.)&lt;/p&gt;


&lt;h2&gt;Tweeting the Twitter&lt;/h2&gt;

&lt;p&gt;Finally, it&amp;#8217;s time to augment our Link model so that it can send tweets.  I decided to have it tweet the link&amp;#8217;s description and a shortened version of its URL using the following code:&lt;/p&gt;

&lt;script src="https://gist.github.com/969776.js?file=link.rb"&gt;&lt;/script&gt;


&lt;p&gt;The rest is up to you.  You could write a cronjob to automatically call &lt;code&gt;tweet!&lt;/code&gt; on every link, or only on those with enough popularity.  Have fun!&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/EsokS-bU_wQ" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2011/05/12/how-to-make-your-rails-app-tweet-the-twitter/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Why plug computers are a security nightmare]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/0tfgsQMvsvk/" />
    <updated>2011-02-28T18:03:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2011/02/28/why-plug-computers-are-a-security-nightmare</id>
    <content type="html">&lt;p&gt;The increasing availability of low profile &amp;#8220;wall-wart&amp;#8221; plug computers like the &lt;a href="http://www.plugcomputer.org/"&gt;SheevaPlug&lt;/a&gt; can be viewed as an emerging threat to physical network security.  For $99, a budding industrial espionagist could buy the SheevaPlug developer kit or the consumer &lt;a href="http://www.tonidoplug.com/"&gt;TonidoPlug&lt;/a&gt;, install some easily-available network intrusion testing software, and illicitly &amp;#8220;test&amp;#8221; the security of a competitor&amp;#8217;s network.&lt;/p&gt;

&lt;p&gt;While many of these techniques have been known for a while, the low form factor of plug computers and consumer netbooks, coupled with their rapidly decreasing price, could enable disposable intrusion tools and open new avenues for attack.  The current $99 SheevaPlug has no wireless capability and limited storage, but the manufacture has &lt;a href="http://www.prnewswire.com/news-releases/marvell-unveils-plug-computer-30-with-integrated-wireless-and-built-in-hard-drive-80693037.html"&gt;just announced&lt;/a&gt; an expanded model with wifi, bluetooth, and an internal hard drive.  Even without these advances, current generation plug computers can easily be expanded with a USB memory stick or external USB hard drive, USB wireless interface, and more.  For little more than $100 one could make a practically undetectable wireless bug that can be deployed in seconds.&lt;/p&gt;

&lt;p&gt;In fact, soon you may be able to just buy an all-in-one penetration plug computer, the &lt;a href="http://theplugbot.com/"&gt;PlugBot&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;edit&lt;/strong&gt;: as was pointed out to me after posting this article, the described device already exists and can tunnel out over &lt;strong&gt;3G&lt;/strong&gt;.  &lt;a href="http://pwnieexpress.com/pwnplug3g.html"&gt;http://pwnieexpress.com/pwnplug3g.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With two wireless adapters and some simple software, a plug computer becomes a wireless bridge capable of automatically cracking wireless networks in range (both WEP and WPA are vulnerable these days; see &lt;a href="http://www.aircrack-ng.org/"&gt;aircrack&lt;/a&gt;).  Most locations have multiple 3rd party networks overlapping their physical space, which, if cracked, could be used as back channels for the plug computer to phone home.  The attacker could then tunnel into the company network undetected and completely bypass the company&amp;#8217;s external defenses by routing through an available 3rd party wireless network.  From the perspective of the attacked network, even if the intrusion is noticed, it appears to come from within their own physical space.&lt;/p&gt;

&lt;p&gt;A number of other uses come to mind for such devices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passive sniffing of internal network traffic using dsniff and sending it back to an attacker.  Many networks aren&amp;#8217;t sufficiently secured once you&amp;#8217;re past the perimeter firewalls.&lt;/li&gt;
&lt;li&gt;Physically connect two ethernet interfaces and use the plug computer as a man-in-the-middle proxy to sniff all traffic entering and leaving a workstation.&lt;/li&gt;
&lt;li&gt;Attach a camera or other sensor payload and use as an over-the-internet video bug.&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;Again, much of this has been possible for a while, but form factor is everything.  Also, I haven&amp;#8217;t seen people talking about the possibility of bridging multiple available wireless networks together for attack obfuscation and to avoid connecting through a company&amp;#8217;s edge network.  I don&amp;#8217;t think companies pay enough attention to passive physical monitoring and intrusion threats like this, especially given the insecurity of wireless encryption standards.  What do you think?&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/0tfgsQMvsvk" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2011/02/28/why-plug-computers-are-a-security-nightmare/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[How I do command-line accounting: Ledger and Reckon]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/fDIC-MndRNU/" />
    <updated>2010-11-06T18:03:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2010/11/06/command-line-accounting-with-ledger-and-reckon</id>
    <content type="html">&lt;p&gt;Ledger is a powerful yet simple double-entry accounting system with a command line interface, making it perfect for those of us who prefer our text editor and flat files over any confusing and inflexible accounting program.&lt;/p&gt;

&lt;p&gt;I&amp;#8217;ve been using Ledger for about a year now, including for balancing last year&amp;#8217;s budget.  In the process, I&amp;#8217;ve needed both to import CSV files of financial data from various sources and to label that data with the most appropriate account.  To do this, I&amp;#8217;ve written a Ruby gem called &lt;a href="https://github.com/iterationlabs/reckon"&gt;Reckon&lt;/a&gt; that can usually guess bank data CSV headings, and can also use &lt;a href="http://en.wikipedia.org/wiki/Bayesian_inference"&gt;Bayesian learning&lt;/a&gt; to automatically categorize each entry.  I think of this as Mint for the command line, but where you don&amp;#8217;t have to trust a 3rd party with your bank account passwords.&lt;/p&gt;

&lt;p&gt;Getting started with Ledger is pretty easy.  I recommend skimming the &lt;a href="http://cloud.github.com/downloads/jwiegley/ledger/ledger.pdf"&gt;official documentation (pdf)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I usually start out the year by making a new year.dat file and entering my business bank account&amp;#8217;s starting balance:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;2010/01/01 * Checking
    Assets:Bank:Checking            $5.000.00
    Equity:Opening Balances
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next, I run Reckon on a CSV dump file from my bank: &lt;code&gt;reckon -f bank.csv -p&lt;/code&gt;&lt;/p&gt;

&lt;!--more--&gt;


&lt;p&gt;If the output looks reasonable, I convert to ledger format and label everything:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ reckon -f bank.csv -o output.dat

What is the account name of this bank account in Ledger? |Assets:Bank:Checking|

(Here I accept the default because I want to have all money flowing in and out of my bank account.)

+------------+--------+--------------------------+
| 2010/03/13 |  $100.00 | GOOGLE ADSENSE REVENUE |
+------------+--------+--------------------------+

Which account provided this income? ([account]/[q]uit/[s]kip) Income:Adsense

(I decided to call this Income:Adsense.)

+------------+---------+-----------------------------+
| 2010/03/14 | -$10.00 | FEE: INCOMING DOMESTIC WIRE |
+------------+---------+-----------------------------+

To which account did this money go? ([account]/[q]uit/[s]kip) |Income:Adsense| Expenses:Bank Fees

+------------+-----------+-----------------------+
| 2010/03/14 |  $1000.00 | WIRE TRANSFER DEPOSIT |
+------------+-----------+-----------------------+

Which account provided this income? ([account]/[q]uit/[s]kip) |Expenses:Bank Fees| Income:Various Sales

(Reckon guessed Expenses:Bank Fees, but I decided to call this Income:Blogging.)

+------------+-----------+------------------------+
| 2010/03/28 |  $300.00  | GOOGLE ADSENSE REVENUE |
+------------+-----------+------------------------+

Which account provided this income? ([account]/[q]uit/[s]kip) |Income:Adsense|

(Notice how this time it guessed Income:Adsense correctly.)

...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, my output.dat file looks something like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;2010/01/01 * Checking
    Assets:Bank:Checking               $5.000.00
    Equity:Opening Balances

2010/03/13    GOOGLE ADSENSE REVENUE
    Assets:Bank:Checking               $100.00
    Income:Adsense                    -$100.00

2010/03/14    FEE: INCOMING DOMESTIC WIRE
    Expenses:Bank Fees                 $10.00
    Assets:Bank:Checking              -$10.00

2010/03/14    WIRE TRANSFER DEPOSIT
    Assets:Bank:Checking               $1000.00
    Income:Blogging                   -$1000.00

2010/03/28    GOOGLE ADSENSE REVENUE
    Assets:Bank:Checking               $300.00
    Income:Adsense                    -$300.00
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And if I run Ledger on it, I can get a breakdown:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ ledger -f output.dat balance

        $6390.00  Assets
       $-5000.00  Equity
          $10.00  Expenses
       $-1400.00  Income

$ ledger -f output.dat equity

    2010/11/06 Opening Balances
    Assets:Bank:Checking                    $6390.00
    Equity:Opening Balances                $-5000.00
    Expenses:Bank Fees                        $10.00
    Income:Adsense                          $-400.00
    Income:Blogging                        $-1000.00
                                               0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice that the income is negative because the money flowed from the income source (Google and Blogging) to the destination (my bank account).  This is double entry accounting, so the final sum is always zero, as seen in the last row.&lt;/p&gt;

&lt;p&gt;Finally, later in the year when there is new financial data, I want Reckon to learn from my existing .dat file, so I do:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;reckon -f bank.csv -l 2010.dat -o output.dat
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To install Ledger and Reckon, see their Github pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/iterationlabs/reckon"&gt;https://github.com/iterationlabs/reckon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jwiegley/ledger"&gt;https://github.com/jwiegley/ledger&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/fDIC-MndRNU" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2010/11/06/command-line-accounting-with-ledger-and-reckon/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Rails RSpec tests are CPU bound]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/Dnt0JGMFRlY/" />
    <updated>2010-10-26T18:03:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2010/10/26/rspec-is-cpu-bound</id>
    <content type="html">&lt;p&gt;Today I experimented with running a large Rails RSpec test suite on a RAM disk.  My hope was that by hosting either the MySQL server or the Rails project directory on the RAM disk, the test execution would be significantly increased.  If this were the case, I would feel (more) compelled to buy a &lt;a href="http://www.apple.com/macbookair/"&gt;certain new device with a solid-state disk drive&lt;/a&gt;.  Unfortunately, while I now have some slick scripts to bring up a RAM disk with either my Rails project or MySQL running on it, the improvements were on the order of 10 seconds over a 10 minute test run (the load time of Rails).  Thus, it is clear that these tests are CPU bound, not disk IO bound, and a SSD wouldn&amp;#8217;t help.&lt;/p&gt;




&lt;p&gt;If you have an SSD, can you corroborate this?&lt;/p&gt;




&lt;p&gt;&lt;span style="color: #bbb"&gt;(Tests were performed on a brand new Quad-Core Intel Core i7 iMac.)&lt;/span&gt;&lt;/p&gt;

&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/Dnt0JGMFRlY" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2010/10/26/rspec-is-cpu-bound/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Revolutionary]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/jwkjr7m5vug/" />
    <updated>2010-08-22T18:03:00-07:00</updated>
    <id>http://blog.andrewcantino.com/blog/2010/08/22/revolutionary</id>
    <content type="html">&lt;p&gt;Sometime in the late &amp;#8217;80s I convinced my parents to get an external hard drive for our Mac Plus to augment our extravagant two floppy disk drives.  We got a 70MB drive for about $400, &lt;a href="http://www.wolframalpha.com/input/?i=%24400+(1998+US+dollars)"&gt;which is about $500 in today&amp;#8217;s dollars&lt;/a&gt;.  Now you can get &lt;a href="http://slickdeals.net/forums/showthread.php?t=2190461"&gt;2TB for $100&lt;/a&gt;, or 10TB for $500.  This is an increase of about 14 million percent in about 20 years!  Talk about technological change!  I have no doubt that this growth - that storage is now basically free - can enable incredible new technologies.  What can we do with this?&lt;/p&gt;

&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/jwkjr7m5vug" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2010/08/22/revolutionary/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Multiple profiles in Chrome on Ubuntu]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/PaOnorEAvNM/" />
    <updated>2010-03-02T18:03:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2010/03/02/multiple-profiles-in-chrome</id>
    <content type="html">&lt;p&gt;If you&amp;#8217;re running Chrome on Ubuntu (likely elsewhere too), you can run multiple copies of Chrome with fully different profiles by launching it with the &lt;code&gt;--user-data-dir&lt;/code&gt; option.  For example, I run two copies of Chrome like so:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/opt/google/chrome/google-chrome --user-data-dir=~/.config/google-chrome-work &amp;amp;amp;
/opt/google/chrome/google-chrome --user-data-dir=~/.config/google-chrome-personal &amp;amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/PaOnorEAvNM" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2010/03/02/multiple-profiles-in-chrome/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Easy web scraping: Get the title of any URL with YQL]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/P4Ml_oXay3Y/" />
    <updated>2010-03-02T18:03:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2010/03/02/get-the-title-of-any-url-with-yql</id>
    <content type="html">&lt;p&gt;This snippet demonstrates how to get the title from any webpage using a simple YQL query and jQuery.  The title is fetched from &lt;code&gt;url&lt;/code&gt; and is placed in &lt;code&gt;#page_title&lt;/code&gt;.

&lt;div&gt;&lt;script src='https://gist.github.com/320406.js?file='&gt;&lt;/script&gt;
&lt;noscript&gt;&lt;pre&gt;&lt;code&gt;// query: select * from html where url=&amp;quot;http://some.url.com&amp;quot; and xpath='//title'
var yql_url = &amp;quot;http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20html%20where%20url%3D%22&amp;quot; + encodeURIComponent(url) + &amp;quot;%22%20and%0A%20%20%20%20%20%20xpath%3D'%2F%2Ftitle'&amp;amp;format=json&amp;amp;callback=?&amp;quot;;

$.getJSON(yql_url, function(json) {
  if (json &amp;amp;&amp;amp; json.query &amp;amp;&amp;amp; json.query.results &amp;amp;&amp;amp; json.query.results.title) {
    $('#page_title').html(json.query.results.title);
  }
});
&lt;/code&gt;&lt;/pre&gt;&lt;/noscript&gt;&lt;/div&gt;


This was very handy for a project I&amp;#8217;m working on.&lt;/p&gt;

&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/P4Ml_oXay3Y" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2010/03/02/get-the-title-of-any-url-with-yql/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Using TSort in Ruby for Topological Sorting of ActiveRecord Models]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/y4kISIsmsc8/" />
    <updated>2010-01-29T18:03:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2010/01/29/using-tsort-in-ruby-for-topological-sorting-of-activerec</id>
    <content type="html">&lt;p&gt;Recently, I was building a project list application in Rails and I needed to be sure that sub-projects showed up in the list somewhere below their parents.&lt;/p&gt;




&lt;p&gt;A &lt;a title="topolocal sorting" href="http://en.wikipedia.org/wiki/Topological_sorting"&gt;topological sort&lt;/a&gt; does just what I needed, and I was pleased to discover that Ruby ships with a &lt;a title="TSort" href="http://ruby-doc.org/stdlib/libdoc/tsort/rdoc/classes/TSort.html"&gt;TSort&lt;/a&gt; module.  Here is how I extended Array to allow sorting of Projects:&lt;/p&gt;




&lt;div&gt;&lt;script src='https://gist.github.com/290045.js?file='&gt;&lt;/script&gt;
&lt;noscript&gt;&lt;pre&gt;&lt;code&gt;# Ruby's TSort requires that you implement 
# tsort_each_node and tsort_each_child. I 
# extend Array so that it knows how to 
# TSort instances of Project, which has a 
# child_project_id pointing to another 
# Project.

require 'tsort'

class Array
  include TSort

  alias tsort_each_node each

  def tsort_each_child(node, &amp;amp;block)
    if node.is_a?(Project)
      [node.child_project].each(&amp;amp;block) if node.child_project?
    else
      node.each(&amp;amp;block) if node
    end
  end
end

# Then I can topologically sort a user's projects like so:
sorted_projects = current_user.projects.tsort&lt;/code&gt;&lt;/pre&gt;&lt;/noscript&gt;&lt;/div&gt;


&lt;p&gt;&lt;span style="font-size: 0.8em"&gt;(Aside: I later post-processed the list to indent sub-projects and position them just below their parents.  This post-processing was made easier by the fact that all sub-projects were already somewhere below their parents in the list.)&lt;/span&gt;&lt;/p&gt;
&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/y4kISIsmsc8" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2010/01/29/using-tsort-in-ruby-for-topological-sorting-of-activerec/</feedburner:origLink></entry>
  
  <entry>
    <title type="html"><![CDATA[Replacement for script onload in IE]]></title>
    <link href="http://feedproxy.google.com/~r/andrew-makes-things/~3/Tt9aZ7f39rI/" />
    <updated>2008-11-23T18:03:00-08:00</updated>
    <id>http://blog.andrewcantino.com/blog/2008/11/23/replacement-for-script-onload-in-ie</id>
    <content type="html">&lt;p&gt;&lt;i&gt;This is an old post from my last blog.&lt;/i&gt;&lt;/p&gt;




&lt;p&gt;Firefox and Safari support an onload event for SCRIPT elements.  That is, you can dynamically add a new script to a page, set its onload event to fire a callback, and know when the script has been successfully loaded.  You would want to do this because when you include a bunch of new SCRIPT tags in a page, there are no guarantees in what order the browser will decide to evaluate them, thus making dependencies among the scripts difficult to resolve.  Using onload to chain the script additions is one solution to this, however, Internet Explorer doesn&amp;#8217;t seem to support onload in SCRIPT tags.&lt;/p&gt;




&lt;p&gt;&lt;i&gt;Update: Owen in the comments says: Thanks for your post, but I have since found a more efficient way to do this and thought I might share with you. As you said you can use an onload event in Firefox, but in IE you can use the onreadystatechange event. Works from at least IE6. Haven’t tested earlier.&lt;/i&gt;&lt;/p&gt;




&lt;p&gt;My solution:&lt;/p&gt;




&lt;figure class='code'&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;div class="highlight"&gt;&lt;table&gt;&lt;tr&gt;&lt;td class="gutter"&gt;&lt;pre class="line-numbers"&gt;&lt;span class='line-number'&gt;1&lt;/span&gt;
&lt;span class='line-number'&gt;2&lt;/span&gt;
&lt;span class='line-number'&gt;3&lt;/span&gt;
&lt;span class='line-number'&gt;4&lt;/span&gt;
&lt;span class='line-number'&gt;5&lt;/span&gt;
&lt;span class='line-number'&gt;6&lt;/span&gt;
&lt;span class='line-number'&gt;7&lt;/span&gt;
&lt;span class='line-number'&gt;8&lt;/span&gt;
&lt;span class='line-number'&gt;9&lt;/span&gt;
&lt;span class='line-number'&gt;10&lt;/span&gt;
&lt;span class='line-number'&gt;11&lt;/span&gt;
&lt;span class='line-number'&gt;12&lt;/span&gt;
&lt;span class='line-number'&gt;13&lt;/span&gt;
&lt;span class='line-number'&gt;14&lt;/span&gt;
&lt;span class='line-number'&gt;15&lt;/span&gt;
&lt;span class='line-number'&gt;16&lt;/span&gt;
&lt;span class='line-number'&gt;17&lt;/span&gt;
&lt;span class='line-number'&gt;18&lt;/span&gt;
&lt;span class='line-number'&gt;19&lt;/span&gt;
&lt;span class='line-number'&gt;20&lt;/span&gt;
&lt;span class='line-number'&gt;21&lt;/span&gt;
&lt;span class='line-number'&gt;22&lt;/span&gt;
&lt;span class='line-number'&gt;23&lt;/span&gt;
&lt;span class='line-number'&gt;24&lt;/span&gt;
&lt;span class='line-number'&gt;25&lt;/span&gt;
&lt;span class='line-number'&gt;26&lt;/span&gt;
&lt;span class='line-number'&gt;27&lt;/span&gt;
&lt;span class='line-number'&gt;28&lt;/span&gt;
&lt;span class='line-number'&gt;29&lt;/span&gt;
&lt;span class='line-number'&gt;30&lt;/span&gt;
&lt;span class='line-number'&gt;31&lt;/span&gt;
&lt;span class='line-number'&gt;32&lt;/span&gt;
&lt;span class='line-number'&gt;33&lt;/span&gt;
&lt;span class='line-number'&gt;34&lt;/span&gt;
&lt;span class='line-number'&gt;35&lt;/span&gt;
&lt;span class='line-number'&gt;36&lt;/span&gt;
&lt;span class='line-number'&gt;37&lt;/span&gt;
&lt;span class='line-number'&gt;38&lt;/span&gt;
&lt;span class='line-number'&gt;39&lt;/span&gt;
&lt;span class='line-number'&gt;40&lt;/span&gt;
&lt;span class='line-number'&gt;41&lt;/span&gt;
&lt;span class='line-number'&gt;42&lt;/span&gt;
&lt;span class='line-number'&gt;43&lt;/span&gt;
&lt;span class='line-number'&gt;44&lt;/span&gt;
&lt;span class='line-number'&gt;45&lt;/span&gt;
&lt;span class='line-number'&gt;46&lt;/span&gt;
&lt;span class='line-number'&gt;47&lt;/span&gt;
&lt;span class='line-number'&gt;48&lt;/span&gt;
&lt;span class='line-number'&gt;49&lt;/span&gt;
&lt;span class='line-number'&gt;50&lt;/span&gt;
&lt;span class='line-number'&gt;51&lt;/span&gt;
&lt;span class='line-number'&gt;52&lt;/span&gt;
&lt;span class='line-number'&gt;53&lt;/span&gt;
&lt;span class='line-number'&gt;54&lt;/span&gt;
&lt;span class='line-number'&gt;55&lt;/span&gt;
&lt;span class='line-number'&gt;56&lt;/span&gt;
&lt;span class='line-number'&gt;57&lt;/span&gt;
&lt;span class='line-number'&gt;58&lt;/span&gt;
&lt;span class='line-number'&gt;59&lt;/span&gt;
&lt;span class='line-number'&gt;60&lt;/span&gt;
&lt;span class='line-number'&gt;61&lt;/span&gt;
&lt;span class='line-number'&gt;62&lt;/span&gt;
&lt;span class='line-number'&gt;63&lt;/span&gt;
&lt;span class='line-number'&gt;64&lt;/span&gt;
&lt;span class='line-number'&gt;65&lt;/span&gt;
&lt;span class='line-number'&gt;66&lt;/span&gt;
&lt;span class='line-number'&gt;67&lt;/span&gt;
&lt;span class='line-number'&gt;68&lt;/span&gt;
&lt;span class='line-number'&gt;69&lt;/span&gt;
&lt;span class='line-number'&gt;70&lt;/span&gt;
&lt;span class='line-number'&gt;71&lt;/span&gt;
&lt;span class='line-number'&gt;72&lt;/span&gt;
&lt;span class='line-number'&gt;73&lt;/span&gt;
&lt;span class='line-number'&gt;74&lt;/span&gt;
&lt;span class='line-number'&gt;75&lt;/span&gt;
&lt;span class='line-number'&gt;76&lt;/span&gt;
&lt;span class='line-number'&gt;77&lt;/span&gt;
&lt;span class='line-number'&gt;78&lt;/span&gt;
&lt;span class='line-number'&gt;79&lt;/span&gt;
&lt;span class='line-number'&gt;80&lt;/span&gt;
&lt;span class='line-number'&gt;81&lt;/span&gt;
&lt;span class='line-number'&gt;82&lt;/span&gt;
&lt;span class='line-number'&gt;83&lt;/span&gt;
&lt;span class='line-number'&gt;84&lt;/span&gt;
&lt;span class='line-number'&gt;85&lt;/span&gt;
&lt;span class='line-number'&gt;86&lt;/span&gt;
&lt;span class='line-number'&gt;87&lt;/span&gt;
&lt;span class='line-number'&gt;88&lt;/span&gt;
&lt;span class='line-number'&gt;89&lt;/span&gt;
&lt;span class='line-number'&gt;90&lt;/span&gt;
&lt;span class='line-number'&gt;91&lt;/span&gt;
&lt;span class='line-number'&gt;92&lt;/span&gt;
&lt;span class='line-number'&gt;93&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;td class='code'&gt;&lt;pre&gt;&lt;code class='javascript'&gt;&lt;span class='line'&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;importJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;look_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;script&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text/javascript&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;src&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;wait_for_script_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;look_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;head&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;importCSS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;look_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;link&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rel&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;stylesheet&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text/css&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;media&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;screen&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;href&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;wait_for_script_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;look_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;head&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;wait_for_script_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;look_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;typeof &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;look_for&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;undefined&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;       &lt;span class="nx"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;       &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;importCSS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;some_stylesheet.css&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="nx"&gt;importJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jquery-1.2.6.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;jQuery&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="nx"&gt;importJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;some_script_that_uses_jquery.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;     &lt;span class="nx"&gt;importJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;another_one.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;SomeClassOrVariableSetByTheScript&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;       &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;         &lt;span class="nx"&gt;importJS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a_script_that_uses_that_class_or_variable.js&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;       &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt;   &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;span class='line'&gt;
&lt;/span&gt;&lt;span class='line'&gt; &lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;&lt;/figure&gt;

&lt;img src="http://feeds.feedburner.com/~r/andrew-makes-things/~4/Tt9aZ7f39rI" height="1" width="1"/&gt;</content>
  <feedburner:origLink>http://blog.andrewcantino.com/blog/2008/11/23/replacement-for-script-onload-in-ie/</feedburner:origLink></entry>
  
</feed>
