<?xml version='1.0' encoding='utf-8' ?>
<!--  If you are running a bot please visit this policy page outlining rules you must respect. http://www.livejournal.com/bots/  -->
<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/' xmlns:media='http://search.yahoo.com/mrss/' xmlns:atom10='http://www.w3.org/2005/Atom'>
<channel>
  <title>Cocktail Builder: JavaScript Alcoholic</title>
  <link>http://cocktailbuilder.livejournal.com/</link>
  <description>Cocktail Builder: JavaScript Alcoholic - LiveJournal.com</description>
  <lastBuildDate>Mon, 16 Feb 2009 17:35:56 GMT</lastBuildDate>
  <generator>LiveJournal / LiveJournal.com</generator>
  <lj:journal>cocktailbuilder</lj:journal>
  <lj:journalid>11971444</lj:journalid>
  <lj:journaltype>personal</lj:journaltype>
  <image>
    <url>http://l-userpic.livejournal.com/56810156/11971444</url>
    <title>Cocktail Builder: JavaScript Alcoholic</title>
    <link>http://cocktailbuilder.livejournal.com/</link>
    <width>91</width>
    <height>100</height>
  </image>

<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/8366.html</guid>
  <pubDate>Mon, 16 Feb 2009 17:35:56 GMT</pubDate>
  <title>QCubed: An Excellent PHP Framework</title>
  <link>http://cocktailbuilder.livejournal.com/8366.html</link>
  <description>I haven&apos;t been writing about it on this blog, but I&apos;ve been intimately involved in the development of one of the best PHP5 frameworks out there. The framework is called &lt;a href=&quot;http://qcu.be/&quot; mce_href=&quot;http://qcu.be&quot; rel=&quot;nofollow&quot;&gt;QCubed&lt;/a&gt;; it&apos;s a model-view-controller framework that&apos;s meant to save the time for all PHP&amp;nbsp;developers out there. &lt;br /&gt;&lt;br /&gt;If you&apos;ve ever written a form in PHP, you know what i&apos;m talking about. Those pesky HTML&amp;nbsp;tags, client-side validation, server-side validation, flow control (multi-page forms...), database interactions... Mess that nobody wants to deal with. Well, QCubed solves all of these problems. An object-relational mapper creates nice&amp;nbsp;PHP5 objects out of your database entities. A library of powerful&amp;nbsp;AJAX controls makes interacting with your form a breeze. &lt;br /&gt;&lt;br /&gt;Event-driven, stateful approach to form development gives PHP&amp;nbsp;developers the time to do what actually &lt;em&gt;matters - &lt;/em&gt;code the business logic (and not the stupid SQL&amp;nbsp;queries). It also enforces strict standards that set good precedents for separation of model, view, and controller - so that the junior developer on your team is not tempted to put the SQL, PHP, and HTML&amp;nbsp;all in one file. And man, am I guilty of this :-). &lt;br /&gt;&lt;br /&gt;Anyway... the QCubed framework recently hit a major milestone - the &lt;a href=&quot;http://trac.qcu.be/projects/qcubed/wiki/download&quot; rel=&quot;nofollow&quot;&gt;second release candidate for the 1.0 release&lt;/a&gt;. It&apos;s stable enough to build businesses on (and, in fact, there are &lt;a href=&quot;http://trac.qcu.be/projects/qcubed/wiki/CompaniesUsingQcubed&quot; rel=&quot;nofollow&quot;&gt;many businesses using it already&lt;/a&gt;). There are numerous &lt;a href=&quot;http://trac.qcu.be/projects/qcubed/wiki/Tutorials&quot; rel=&quot;nofollow&quot;&gt;tutorials&lt;/a&gt; to help you get started. Give it a shot!&lt;br /&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/8366.html</comments>
  <category>mvc</category>
  <category>qcodo</category>
  <category>framework</category>
  <category>php</category>
  <category>qcubed</category>
  <lj:security>public</lj:security>
  <lj:reply-count>11</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/8138.html</guid>
  <pubDate>Thu, 02 Oct 2008 19:30:05 GMT</pubDate>
  <title>Another award and an interview!</title>
  <link>http://cocktailbuilder.livejournal.com/8138.html</link>
  <description>&lt;br /&gt;Cocktail Builder won another award!&amp;nbsp;SEOMoz gave the site a &lt;a href=&quot;http://www.seomoz.org/web2.0#cat_117&quot; rel=&quot;nofollow&quot;&gt;Gold Medal&lt;/a&gt; in the Web 2.0 Fun Stuff category again this year! And hey, they even asked me for an interview...&amp;nbsp;&lt;a href=&quot;http://www.seomoz.org/web2.0/interview/cocktailbuilder&quot; rel=&quot;nofollow&quot;&gt;Here&apos;s it is&lt;/a&gt;.</description>
  <comments>http://cocktailbuilder.livejournal.com/8138.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>2</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/7815.html</guid>
  <pubDate>Mon, 18 Aug 2008 03:10:46 GMT</pubDate>
  <title>Looking for a designer</title>
  <link>http://cocktailbuilder.livejournal.com/7815.html</link>
  <description>&lt;a&gt;Cocktail Builder&lt;/a&gt; needs a talented graphic designer to take the site to the next level. Do you like the site and think you can make it even better? Are you a Photoshop guru with a great understanding of CSS and web standards? If so, I want to hear from you. &lt;br /&gt;&lt;br /&gt;This is an equity share opportunity - depending on your skills and contribution, you will earn a share of ownership and revenue of the site. &lt;br /&gt;&lt;br /&gt;Contact me (Alex) at [alex94040 at yahoo dot com] if interested; please make sure to include your portfolio in the email.&lt;br /&gt;&lt;br /&gt;Cheers!&lt;br /&gt;-cb</description>
  <comments>http://cocktailbuilder.livejournal.com/7815.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>4</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/7389.html</guid>
  <pubDate>Wed, 28 Nov 2007 02:41:32 GMT</pubDate>
  <title>The Query to Power It All</title>
  <link>http://cocktailbuilder.livejournal.com/7389.html</link>
  <description>&lt;p&gt;This article is only meant for major, major SQL geeks. You&apos;ll be bored out of your mind if you&apos;re not one; don&apos;t tell me I didn&apos;t warn you. &lt;/p&gt; &lt;p&gt;As you might have guessed, there&apos;s one, only one query powering the basic cocktail builder functionality. It&apos;s the query that answers the question &quot;if I have ingredients A, B, and C, give me a list of all cocktails that I can make&quot;. Also: &lt;/p&gt; &lt;ul&gt; &lt;li&gt;give me cocktails that I can almost make, ordered by the number of missing ingredients (i.e. those that I can make right away go first, those that are missing a single ingredient second, etc). &lt;/li&gt; &lt;li&gt;consider ingredient substitutions (a concept I refer to as &quot;normalized ingredients&quot;): if you have Sky Vodka, and the recipe calls for Stoli, you can make the cocktail just fine.&lt;/li&gt; &lt;li&gt;calculate cocktail ratings from the user feedback&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;As you might guess, the SQL is rather involved. Here&apos;s the statement that gets executed when the user has two ingredients - items with ID&apos;s 73 and 76: &lt;/p&gt;&lt;br /&gt;&lt;blockquote&gt;&lt;br /&gt;&lt;div&gt;&lt;pre&gt;&lt;span&gt;SELECT&lt;/span&gt; &lt;/pre&gt;&lt;pre&gt;    InnerCountsTable.CocktailID, &lt;/pre&gt;&lt;pre&gt;    InnerCountsTable.CocktailName, &lt;/pre&gt;&lt;pre&gt;    InnerCountsTable.CocktailShortName,     &lt;/pre&gt;&lt;pre&gt;    CountPresent, &lt;/pre&gt;&lt;pre&gt;    CountMissing, &lt;/pre&gt;&lt;pre&gt;    FORMAT(round((&lt;span&gt;AVG&lt;/span&gt;(rating.Rating)*2))/2, 1) &lt;span&gt;AS&lt;/span&gt; Rating, &lt;/pre&gt;&lt;pre&gt;    (CountPresent - CountMissing) &lt;span&gt;as&lt;/span&gt; Difference &lt;/pre&gt;&lt;pre&gt;&lt;span&gt;FROM&lt;/span&gt; &lt;/pre&gt;&lt;pre&gt;    (&lt;span&gt;SELECT&lt;/span&gt; &lt;/pre&gt;&lt;pre&gt;        cocktail.ID &lt;span&gt;as&lt;/span&gt; CocktailID, &lt;/pre&gt;&lt;pre&gt;        cocktail.Name &lt;span&gt;as&lt;/span&gt; CocktailName, &lt;/pre&gt;&lt;pre&gt;        cocktail.ShortName &lt;span&gt;as&lt;/span&gt; CocktailShortName, &lt;/pre&gt;&lt;pre&gt;        cocktail.Instructions &lt;span&gt;as&lt;/span&gt; CocktailInstructions,&lt;/pre&gt;&lt;pre&gt;        &lt;span&gt;SUM&lt;/span&gt;(&lt;span&gt;IF&lt;/span&gt;(userHas.NormalizedIngredientID &lt;span&gt;IS&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt;, 1, 0)) &lt;/pre&gt;&lt;pre&gt;            &lt;span&gt;as&lt;/span&gt; CountMissing, &lt;/pre&gt;&lt;pre&gt;        &lt;span&gt;SUM&lt;/span&gt;(&lt;span&gt;IF&lt;/span&gt;(userHas.NormalizedIngredientID &lt;span&gt;IS&lt;/span&gt; &lt;span&gt;NULL&lt;/span&gt;, 0, 1))&lt;/pre&gt;&lt;pre&gt;            &lt;span&gt;as&lt;/span&gt; CountPresent &lt;/pre&gt;&lt;pre&gt;        &lt;span&gt;FROM&lt;/span&gt; &lt;/pre&gt;&lt;pre&gt;            mix &lt;span&gt;INNER&lt;/span&gt; &lt;span&gt;JOIN&lt;/span&gt; ingredient &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;ON&lt;/span&gt; mix.IngredientID = ingredient.ID &lt;/pre&gt;&lt;pre&gt;            &lt;span&gt;LEFT&lt;/span&gt; &lt;span&gt;JOIN&lt;/span&gt; (&lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;SELECT&lt;/span&gt; NormalizedIngredientID &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;FROM&lt;/span&gt; ingredient &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;WHERE&lt;/span&gt; ID=76 &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;UNION&lt;/span&gt; &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;SELECT&lt;/span&gt; NormalizedIngredientID &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;FROM&lt;/span&gt; ingredient &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;WHERE&lt;/span&gt; ID=73) &lt;span&gt;AS&lt;/span&gt; userHas &lt;/pre&gt;&lt;pre&gt;                &lt;span&gt;ON&lt;/span&gt; userHas.NormalizedIngredientID =&lt;/pre&gt;&lt;pre&gt;                    ingredient.NormalizedIngredientID &lt;/pre&gt;&lt;pre&gt;            &lt;span&gt;INNER&lt;/span&gt; &lt;span&gt;JOIN&lt;/span&gt; cocktail &lt;span&gt;ON&lt;/span&gt; mix.CocktailID = cocktail.ID &lt;/pre&gt;&lt;pre&gt;            &lt;span&gt;WHERE&lt;/span&gt; &lt;span&gt;NOT&lt;/span&gt; (cocktail.Status = &lt;span&gt;&apos;Cut&apos;&lt;/span&gt;) &lt;/pre&gt;&lt;pre&gt;            &lt;span&gt;GROUP&lt;/span&gt; &lt;span&gt;BY&lt;/span&gt; mix.CocktailID) &lt;span&gt;AS&lt;/span&gt; InnerCountsTable &lt;/pre&gt;&lt;pre&gt;&lt;span&gt;LEFT&lt;/span&gt; &lt;span&gt;JOIN&lt;/span&gt; rating &lt;/pre&gt;&lt;pre&gt;    &lt;span&gt;ON&lt;/span&gt; InnerCountsTable.CocktailID = rating.CocktailID &lt;/pre&gt;&lt;pre&gt;&lt;span&gt;WHERE&lt;/span&gt; CountPresent &amp;gt; 0 &lt;/pre&gt;&lt;pre&gt;&lt;span&gt;GROUP&lt;/span&gt; &lt;span&gt;BY&lt;/span&gt; InnerCountsTable.CocktailID &lt;/pre&gt;&lt;pre&gt;&lt;span&gt;ORDER&lt;/span&gt; &lt;span&gt;BY&lt;/span&gt; &lt;/pre&gt;&lt;pre&gt;    CountMissing &lt;span&gt;ASC&lt;/span&gt;, &lt;/pre&gt;&lt;pre&gt;    Difference &lt;span&gt;DESC&lt;/span&gt;, &lt;/pre&gt;&lt;pre&gt;    CountPresent &lt;span&gt;DESC&lt;/span&gt; &lt;span&gt;LIMIT&lt;/span&gt; 30&lt;/pre&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;&lt;p&gt;A weird part of the query is the &quot;UNION&quot; part inside (the one that&apos;s creating the &lt;strong&gt;&lt;em&gt;userHas &lt;/em&gt;&lt;/strong&gt;part). Basically, I don&apos;t believe there&apos;s any other way to create an in-memory virtual table in MySQL to do a JOIN with. Anyone got better ideas? &lt;/p&gt;&lt;br /&gt;&lt;p&gt;Or, maybe you can offer a completely different approach that will work faster?&lt;/p&gt;&lt;br /&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/7389.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>0</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/6996.html</guid>
  <pubDate>Fri, 18 May 2007 14:42:29 GMT</pubDate>
  <title>The first place!</title>
  <link>http://cocktailbuilder.livejournal.com/6996.html</link>
  <description>&lt;p&gt;So, here I am, looking at the &lt;a href=&quot;http://analytics.google.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Google Analytics&lt;/a&gt; for &lt;a title=&quot;Cocktail Builder&quot; href=&quot;http://www.cocktailbuilder.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;CocktailBuilder&lt;/a&gt;&amp;nbsp;last week, and the graph looks approximately like this: &lt;/p&gt; &lt;p&gt;&lt;em&gt;&lt;img style=&quot;border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; margin: 0px 20px 0px 0px; border-right-width: 0px&quot; height=&quot;143&quot; alt=&quot;image&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/da2d4c8426e6_68D6/image.png&quot; width=&quot;252&quot; align=&quot;left&quot; border=&quot;0&quot;&gt; Whaaaaat? &lt;/em&gt;was exactly my reaction. Here I am, having about 30 visitors on a usual day for about a month or so, and suddenly, a jump to 300, and then 700 visitors a day! &lt;/p&gt; &lt;p&gt;&lt;a href=&quot;http://www.seomoz.org/web2.0&quot; rel=&quot;nofollow&quot;&gt;&lt;img style=&quot;border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px&quot; alt=&quot;2007 Web 2.0 Awards Winner&quot; src=&quot;http://www.seomoz.org/img/web20/2007/badges/web20_winner2.png&quot; align=&quot;right&quot;&gt;&lt;/a&gt;Wooooooot?&lt;/p&gt; &lt;p&gt;&amp;nbsp;&lt;/p&gt; &lt;p&gt;The reason was really easy to find. SeoMoz.org &lt;a href=&quot;http://www.seomoz.org/web2.0&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;ranked&lt;/a&gt; Web 2.0 sites, and &lt;a title=&quot;Cocktail Builder&quot; href=&quot;http://www.cocktailbuilder.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;CocktailBuilder&lt;/a&gt;&amp;nbsp;won the first place in the Fun Stuff category! &lt;/p&gt; &lt;p&gt;Here&apos;s &lt;a href=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/fullReview.png&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;their full review&lt;/a&gt;, for historical purposes.&lt;/p&gt; &lt;p&gt;Woooo hoo! :-)&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/6996.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>10</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/6662.html</guid>
  <pubDate>Fri, 20 Apr 2007 03:05:26 GMT</pubDate>
  <title>Cross-Domain Ajax</title>
  <link>http://cocktailbuilder.livejournal.com/6662.html</link>
  <description>&lt;p&gt;&amp;lt;rants&amp;gt;&lt;/p&gt;&lt;p&gt;Oh, friends, friends. Whoever thought of setting up the so-called Same Origin Policy was, ahem, not very forward-looking. &lt;/p&gt;&lt;p&gt;A bit of history: Microsoft. Internet Explorer&amp;nbsp;5.0 (released in 1999). Outlook Web Access (OWA) team comes to Trident (IE) and asks for a way for the web page to talk to the Exchange server, asynchronously. They get the XMLHTTPRequest object, currently more famously known as XHR. Seven years later, the industry catches up and understands that XHR can be, indeed, used for Ajax. &lt;/p&gt;&lt;p&gt;That XHR object has a very interesting property: you can&apos;t initiate requests to pages other than the one that the web page is originating from. For example: your site is &lt;a href=&quot;http://www.cocktailbuilder.com&quot; rel=&quot;nofollow&quot;&gt;cocktailbuilder.com&lt;/a&gt;, but you want to asynchronously communicate with &lt;a target=&quot;_blank&quot; href=&quot;http://maps.google.com&quot; rel=&quot;nofollow&quot;&gt;maps.google.com&lt;/a&gt; through their REST API to get to something useful. &lt;/p&gt;&lt;p&gt;Tough luck, not gonna happen. Your XHR object has a security restriction: talk only to cocktailbuilder.com. Why? Well, because you could, theoretically, sniff the local network, then send the results to evilhacker.com.&lt;/p&gt;&lt;p&gt;BUT WHY IN THE WORLD DO YOU HAVE TO USE XHR FOR THAT??? Who in his right mind was setting up that restriction??? If I&apos;m running JavaScript on your browser, I can just freaking insert an IMG tag, point it to evilhacker.com, and through clever parameters, pass any info you want. &lt;/p&gt;&lt;p&gt;There&apos;s a huge hole, and they put a tiny little plug over it. &lt;/p&gt;&lt;p&gt;Results: &lt;/p&gt;&lt;p&gt;1) It&apos;s much harder to code legitimate applications - mashups - and any kind of JavaScript-based services. &lt;/p&gt;&lt;p&gt;2) Security hole is as glaring as it ever was. &lt;/p&gt;&lt;p&gt;&amp;lt;/rants&amp;gt;&lt;/p&gt;&lt;p&gt;OK, now to the useful stuff. There are a couple ways to set up two-way asynchronous communications across domains.&lt;/p&gt;&lt;p&gt;1) Outright ugly methods: &lt;b&gt;Flash and&amp;nbsp;iframes&lt;/b&gt;.&lt;/p&gt;&lt;p&gt;2) &lt;b&gt;Proxy&lt;/b&gt;. You set up a service on &lt;i&gt;your&lt;/i&gt; domain that relays all requests to the target domain. Your XHR is talking to your box, your box is being the middleman between the other box and the browser.&lt;br /&gt;+ You can still use XHR and related frameworks. &lt;/p&gt;&lt;p&gt;- Load on your server goes up; plus, if you&apos;re thinking about providing a service (I am - &lt;a href=&quot;http://www.ajaxmetrics.com&quot; rel=&quot;nofollow&quot;&gt;ajaxmetrics.com&lt;/a&gt;), this means that your clients will need to install some PHP/ASP/JSP on their server. That&apos;s a very bad thing; this implies lots of trust between the service provider and the client (if I have my PHP code running on your server, I own your box). That&apos;s why guys from &lt;a href=&quot;http://www.instacomment.com&quot; rel=&quot;nofollow&quot;&gt;instacomment.com&lt;/a&gt;&amp;nbsp;are not going to make it big any time soon. &lt;/p&gt;&lt;p&gt;3) &lt;b&gt;Script tag monkeying, &lt;/b&gt;also known as on-demand JavaScript&lt;b&gt;.&lt;/b&gt; You all know that you can insert a &amp;lt;script&amp;gt; tag into your HTML, and have that tag point to an external server. But wait, you can insert that script tag dynamically! What does that mean? That you can dynamically inject JS code into your app, and that JS is coming asynchronously from the server. &lt;/p&gt;&lt;p&gt;Here&apos;s the code to make that &quot;fake Ajax&quot; call: &lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;function hackyAjaxCall (op, params, callbackFunctionName) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Inject the script tag into the document and pass parameters&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // through the URL. Get and execute the JS that the server . &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // returns. No, there are no security risks: the server is mine.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var head = document.getElementsByTagName(&quot;head&quot;)[0];&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var script = document.createElement(&apos;script&apos;);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; script.type = &apos;text/javascript&apos;;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (op.length &amp;lt; 1) {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; alert(&quot;Error: Unspecified op parameter for the Ajax call&quot;);&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var opUrlParam = &quot;op=&quot; + op + &quot;&amp;amp;&quot;;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var callbackUrlParam = callbackFunctionName ? &quot;callback=&quot; + &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; callbackFunctionName + &quot;&amp;amp;&quot;: &quot;&quot;;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; var UrlParams = &quot;p=&quot; + escape(Object.toJSON(params));&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; script.src = &quot;http://server/endpoint.php?&quot;+ opUrlParam + &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; callbackUrlParam + UrlParams; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; head.appendChild(script);&lt;br /&gt;&amp;nbsp;}&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;+ True, asynchronous communication between the browser and a web service on a different domain. &lt;/p&gt;&lt;p&gt;- This method is not as pleasant to work with as XHR, and there are no nice frameworks like Prototype, YUI, and Dojo. There was a project called &quot;Ajax Extended&quot;, which added things to Prototype to do this, but the website is now dead. I&apos;d love to see this feature in core Prototype. &lt;/p&gt;&lt;p&gt;- Security implications: if you don&apos;t trust the other end, don&apos;t do this. They can inject any JS they want into your browser-side app, and steal your cookies.&lt;/p&gt;&lt;p&gt;Further reading: &lt;/p&gt;&lt;p&gt;1) XML.com &lt;a target=&quot;_blank&quot; href=&quot;http://www.xml.com/pub/a/2005/11/09/fixing-ajax-xmlhttprequest-considered-harmful.html?page=last&quot; rel=&quot;nofollow&quot;&gt;article&lt;/a&gt; of both mechanisms mentioned above. &lt;br /&gt;2) Good &lt;a target=&quot;_blank&quot; href=&quot;http://snook.ca/archives/javascript/cross_domain_aj/&quot; rel=&quot;nofollow&quot;&gt;article&lt;/a&gt; on the subject from snook.ca.&lt;/p&gt;&lt;p&gt;Cheers!&lt;br /&gt;-cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/6662.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>3</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/6589.html</guid>
  <pubDate>Sun, 08 Apr 2007 09:10:48 GMT</pubDate>
  <title>AjaxMetrics</title>
  <link>http://cocktailbuilder.livejournal.com/6589.html</link>
  <description>&lt;p&gt;You&apos;ve seen my &lt;a href=&quot;http://cocktailbuilder.livejournal.com/#entry_5756&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;recent post&lt;/a&gt; about the importance of knowing what the users do on your site. My friend David gave me great feedback to actually productize the idea, considering there really aren&apos;t any good solutions out there - so I put together a &lt;a href=&quot;http://www.ajaxmetrics.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;site&lt;/a&gt; where&amp;nbsp;I discuss the problem in detail, and where eventually the tool will be available.&lt;/p&gt; &lt;p&gt;Check it out; it&apos;s called &lt;a href=&quot;http://www.ajaxmetrics.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;AjaxMetrics.com&lt;/a&gt;. &lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/6589.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>3</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/6271.html</guid>
  <pubDate>Fri, 06 Apr 2007 08:34:24 GMT</pubDate>
  <title>Homework: Analyzing Competition</title>
  <link>http://cocktailbuilder.livejournal.com/6271.html</link>
  <description>&lt;p&gt;It&apos;s good to know who else is trying to do the same thing. Why? &lt;/p&gt; &lt;p&gt;Obvious answers - so that you don&apos;t reinvent the wheel; so that you know what they are doing; so that you can differentiate yourself properly. &lt;/p&gt; &lt;p&gt;Non-obvious answers - presence of competition is actually a good thing, it means that the market you&apos;re about to enter in fact exists, and you&apos;re not delusional. If you&apos;re planning to provide Internet services using &lt;a href=&quot;http://www.datacenterknowledge.com/archives/2007/Apr/01/googles_toilet_isp_not_so_far_fetched.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;toilets as hotspots&lt;/a&gt;, and noone else is doing anything similar, you are probably a bit off base (&lt;em&gt;crap, &lt;/em&gt;how did they - CityNet - get the VC funding with that idea??). On the other hand, if other [sucky] solutions to the same problem exist, you&apos;re on the right track. &lt;/p&gt; &lt;p&gt;Well, of course, there are revolutionary things out there, too, but hey, if you&apos;re Einstein, you better realize it yourself. As one of my favorite bloggers &lt;a href=&quot;http://blog.guykawasaki.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Guy Kawasaki&lt;/a&gt; says, toilet paper is radically better than leaves.&lt;/p&gt; &lt;p&gt;But back to the topic. Competitive analysis. Let&apos;s assume for a moment that you decided to believe the crazy author of this blog, and actually do it. Well, I admire your trust in me - even I don&apos;t have that kind of trust in me :-). &lt;/p&gt; &lt;p&gt;All right, all right, back on track. Here&apos;s what you may want to cover: &lt;/p&gt; &lt;p&gt;1) What&apos;s the problem that you&apos;re trying to solve? For &lt;a href=&quot;http://www.cocktailbuilder.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;cocktail builder&lt;/a&gt;: find out cocktails you can make from ingredients in your bar. &lt;/p&gt; &lt;p&gt;2) What are adjacent problems? Where to buy liquor; history of cocktails; cocktail pictures; etc.&lt;/p&gt; &lt;p&gt;3) What are the Google search terms for the problem you&apos;re trying to solve? For me, it&apos;s cocktail, bar, ingredient, etc.&lt;/p&gt; &lt;p&gt;4) Who comes up on top in search results for these keywords? These are likely your top current competitors, if you&apos;re doing an Internet business. &lt;/p&gt; &lt;p&gt;5) Who does things right nearby? For cocktail builder, it&apos;s food sites that let you put in ingredients you have and find out the meals you can make from &apos;em. These folks may come in to your market at any moment. &lt;/p&gt; &lt;p&gt;To inspire you a little bit, I&apos;m attaching the &lt;a href=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/CocktailBuilder%20Competitive%20Analysis.pdf&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;competitive analysis doc&lt;/a&gt; I used when doing the initial homework for my site. Take a look if you&apos;d like. &lt;/p&gt; &lt;p&gt;Cheers!&lt;/p&gt; &lt;p&gt;-cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/6271.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>2</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/5929.html</guid>
  <pubDate>Sat, 24 Mar 2007 11:16:21 GMT</pubDate>
  <title>Vision vNext: Organizing a Party</title>
  <link>http://cocktailbuilder.livejournal.com/5929.html</link>
  <description>&lt;p&gt;I&apos;m thinking about the next version of the &lt;a title=&quot;Cocktail Builder&quot; href=&quot;http://www.cocktailbuilder.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;CocktailBuilder&lt;/a&gt;, and I&apos;d like to share an idea that a friend of mine gave me. Basically, it&apos;s about one good end-to-end scenario for the website that I&apos;m &lt;em&gt;almost&lt;/em&gt; achieving, but not quite. &lt;/p&gt; &lt;p&gt;What&apos;s the most obvious use case for the site today? &quot;Enter what you have in your bar, and you&apos;ll see the cocktails you can make from it&quot;. Great. &lt;/p&gt; &lt;p&gt;We want more. &lt;/p&gt; &lt;p&gt;We want to think end-to-end. What is the user trying to do? Probably, organize a party. A good party host wants to post a bar menu somewhere, to disincline the cute&amp;nbsp;and pleasantly un-intelligent&amp;nbsp;freshmen from the nearby dorm from asking &quot;hey, can you make a sex on the beach?&quot; RTFM, baby. &lt;/p&gt; &lt;p&gt;&lt;a href=&quot;http://farm1.static.flickr.com/40/79474235_005a8784a4_b.jpg&quot; rel=&quot;nofollow&quot;&gt;&lt;img src=&quot;http://farm1.static.flickr.com/40/79474235_005a8784a4.jpg&quot;&gt;&lt;/a&gt;&lt;/p&gt; &lt;p&gt;To make that scenario work, you need to help the savvy sysadmin of the party print out that M, I mean the manual, oops, the bar menu. And you need to help him manage that bar menu. Really, the bar menu is just a list of favorite cocktails; and printing of the menu is just a matter of converting the pretty HTML page to a pretty PDF file. &lt;/p&gt; &lt;p&gt;I&apos;m half-way there on the implementation - favorites are done, but I&apos;m still thinking about whether doing a PDF exporter is the right way to go .&lt;/p&gt; &lt;p&gt;What do you think about this scenario? Anything I&apos;m missing? Any nearby scenario that we can get for cheap? &lt;/p&gt; &lt;p&gt;Cheers!&lt;br&gt;-cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/5929.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>8</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/5756.html</guid>
  <pubDate>Sat, 24 Mar 2007 10:44:40 GMT</pubDate>
  <title>What are users doing on your site?</title>
  <link>http://cocktailbuilder.livejournal.com/5756.html</link>
  <description>&lt;p&gt;So, you have a great-looking AJAX website. You&apos;re &lt;a href=&quot;http://www.zillow.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Zillow&lt;/a&gt;, or &lt;a href=&quot;http://maps.google.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Google Maps&lt;/a&gt;, or whatever. You have a billion visitors, but as a great entrepreneur, you understand that the only way to keep afloat is to give users the features that they want. &lt;/p&gt; &lt;p&gt;How do you do this? &lt;/p&gt; &lt;p&gt;1) Ask your users. Focus groups and similar methods still apply. &lt;/p&gt; &lt;p&gt;2) Talk to your users. Open a blog, get the comments. Put a comment box right there on the website. Have you noticed the &quot;rate this content&quot; concept on &lt;a href=&quot;http://msdn2.microsoft.com/en-us/library/aa338201.aspx&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;MSDN&lt;/a&gt;? &lt;/p&gt; &lt;p&gt;&lt;a href=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Whatareusersdoingonyoursite_2BD7/image3.png&quot; atomicselection=&quot;true&quot; rel=&quot;nofollow&quot;&gt;&lt;img style=&quot;border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px&quot; height=&quot;143&quot; alt=&quot;image&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Whatareusersdoingonyoursite_2BD7/image_thumb3.png&quot; width=&quot;400&quot; border=&quot;0&quot;&gt;&lt;/a&gt; &lt;/p&gt; &lt;p&gt;Do it. &lt;/p&gt; &lt;p&gt;3) Gather statistics on what users are doing on your site. We&apos;ll be talking about this method in depth here. &lt;/p&gt; &lt;p&gt;You surely noticed that the first two methods require either a major money investment, or proactive users; the vast majority of your users, however, won&apos;t be this &quot;early adopters&quot; crowd, willing to debug your JavaScript and bring you coffee just for making the coolest Ajax app around. &lt;/p&gt; &lt;p&gt;Average users want their problems solved. They don&apos;t want to help you. They want to help themselves. You want to help them, too, but how do you help, if they don&apos;t say what they want?? You track their behavior. You use tracking cookies. You record every single click they make within your application, look through the session information, and research interesting sessions. You personally, the site owner, need to read through these logs to understand what users do in your app. &lt;/p&gt; &lt;p&gt;Let me give you a more concrete feeling of what I&apos;m talking about. Here&apos;s how a sample log excerpt may look like in &lt;a href=&quot;http://www.cocktailbuilder.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;CocktailBuilder&lt;/a&gt;: &lt;/p&gt; &lt;ol&gt; &lt;li&gt;Added ingredient &quot;absolut vodka&quot;  &lt;li&gt;Tried to add an ingredient &quot;absinte&quot;, but failed, because there was no match in the database of ingredients.  &lt;li&gt;Added ingredient &quot;orange juice&quot;  &lt;li&gt;Expanded the details for the &quot;melon ball&quot; cocktail  &lt;li&gt;Expanded the details for the &quot;mandarin passion&quot; cocktail  &lt;li&gt;Added the &quot;mandarin passion&quot; cocktail to favorites&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;This is just like conducting a customer visit: you&apos;re observing the user do their thing. Except that it&apos;s not happening in real-time, and you&apos;re not really seeing a video, but you know your app enough to make sense of it.&lt;/p&gt; &lt;p&gt;Plus, it&apos;s all quantitative, unlike videos: you can make a query such as &quot;how many times in the last month folks tried to add an ingredient, but failed&quot;. Or you can draw conclusions over time, determining whether your efforts to improve the ingredient database is paying off. This, my friends, really is &lt;em&gt;business intelligence&lt;/em&gt; for the Ajax world. &lt;/p&gt; &lt;p&gt;How do you set this kind of logging system up? You need to do this by hand; I haven&apos;t seen any good frameworks for this just yet, unfortunately. I told you about my love for &lt;a href=&quot;http://analytics.google.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Google Analytics&lt;/a&gt;&amp;nbsp;- it&apos;s a great product to measure your site-wide performance (i.e. how many visitors came to your site in the last month? which country were they from?), but it&apos;s utterly useless for any kind application usage analysis&amp;nbsp;for Ajax apps (since everything is on the same page, likely, with the same URL). So old-school metrics tools won&apos;t help you, and Apache log files won&apos;t, either. &lt;/p&gt; &lt;p&gt;You need a custom solution. At it&apos;s basis, there are 2 components: &lt;/p&gt; &lt;p&gt;1) A database table that defines the &quot;datapoints&quot; that you want to collect. Sample: &lt;/p&gt; &lt;p&gt;&lt;a href=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Whatareusersdoingonyoursite_2BD7/image8.png&quot; atomicselection=&quot;true&quot; rel=&quot;nofollow&quot;&gt;&lt;img height=&quot;132&quot; alt=&quot;image&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Whatareusersdoingonyoursite_2BD7/image_thumb8.png&quot; width=&quot;516&quot;&gt;&lt;/a&gt; &lt;/p&gt; &lt;p&gt;2) Client-side data collection mechanism. Really, just a backlog of things that happened on the client side. Things that &lt;em&gt;may&lt;/em&gt;&amp;nbsp;happen are defined in the first step. The backlog will be sent to the server every 5-10 seconds, and when that&apos;s done, the backlog is back to empty. &lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;// submit user actions to the server every X milliseconds&lt;br&gt;window.setInterval(submitUserActions, 5000);&amp;nbsp;  &lt;p&gt;function registerUserAction(id, data) { &lt;br&gt;&amp;nbsp;&amp;nbsp; userActions[userActions.length] = [id, data];&lt;br&gt;}  &lt;p&gt;// Send the usage data back to the server to track what &lt;br&gt;// the user was doing on the site&lt;br&gt;function submitUserActions() {&lt;br&gt;&amp;nbsp;&amp;nbsp; if (userActions.length &amp;gt; 0) {&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Ajax call&amp;nbsp;sending userActions to the server&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...&lt;br&gt;&lt;/p&gt; &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// We&apos;ve sent the data to the server, &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // so let&apos;s clear the buffer&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;userActions = [];&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br&gt;}&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Every time something interesting happens in the app, I&apos;d just call registerUserAction; for example, when the user is adding an ingredient, I call&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;registerUserAction(DataPoint.ADD_INGREDIENT_BY_NAME, &lt;br&gt;&amp;nbsp;&amp;nbsp; inputTextBox.value);&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Simple, isn&apos;t it? &lt;/p&gt; &lt;p&gt;One question remains: where is the DataPoint array coming from? At some point in the past, I mentioned a &lt;a href=&quot;http://cocktailbuilder.livejournal.com/3007.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;technique for generating semi-dynamic content&lt;/a&gt;. You&apos;d use just that here: at build time, your script will dump the contents of the datapoints table into a .js file (it would have to do some minimal formatting to be syntactically correct). Then, at runtime, you just include that .js file as you would any other JavaScript resource. Several good things about this approach:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Your code remains readable. You can easily tell that in the code fragment above I was trying to capture the fact that someone entered an ingredient. That fact could have been captured by using the datapoint id (1), not it&apos;s name (ADD_INGREDIENT_BY_NAME). This would have saved us some JS size, but I&apos;ll trade maintainability for download size any day.  &lt;li&gt;It&apos;s really easy to add new datapoints. Just add them to your database table, and code a single registerUserAction() call. Then, at build-time, everything will start working automagically. Again, ease of maintenance. &lt;/li&gt;&lt;/ol&gt; &lt;p&gt;For dessert, take a look at &lt;a href=&quot;http://www.cocktailbuilder.com/stats/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;some reports&lt;/a&gt; that I was able to generate from CocktailBuilder usage data. Answering the &quot;how did you make this??&quot; question: these were done using Rico/Prototype and the LiveGridPlus extension. 200 lines of code (PHP, JS, HTML, SQL, all intermingled - yeah, I don&apos;t drink my own&amp;nbsp;Koolaid when I build quick-and-dirty things).&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/5756.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>2</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/5219.html</guid>
  <pubDate>Thu, 25 Jan 2007 16:15:04 GMT</pubDate>
  <title>AJAX Podcast</title>
  <link>http://cocktailbuilder.livejournal.com/5219.html</link>
  <description>I&apos;ve been quite hooked to podcasting in the last few weeks - mainly because I&apos;ve been taking the bus to work (Seattle&apos;s weather is, ahem, weird this winter), and the best thing to do on the bus is to try to create an interesting enough shell around yourself that you don&apos;t notice the 150 people squeezed in a little box around you, and don&apos;t hear the banging of the chains against the nearby cars.&lt;br /&gt;&lt;br /&gt;Funny thing is, I found a very cool podcasting series that&apos;s been open for over a year now; it&apos;s hosted by folks running &lt;a href=&quot;http://www.ajaxian.com&quot; rel=&quot;nofollow&quot;&gt;ajaxian.com&lt;/a&gt;. They are fun, very knowledgeable, and their sole focus is AJAX and a tiny bit of JavaScript. The guys bring in influential people from the industry, and interview them; other of their podcasts are about &quot;the state of AJAX&quot; - new tools and best practices. Here&apos;s their podcast URL: &lt;a href=&quot;http://media.ajaxian.com/&quot; rel=&quot;nofollow&quot;&gt;http://media.ajaxian.com&lt;/a&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/5219.html</comments>
  <lj:security>public</lj:security>
  <lj:reply-count>0</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/4926.html</guid>
  <pubDate>Fri, 12 Jan 2007 06:04:37 GMT</pubDate>
  <title>New Build: 90 new cocktails, 110 new ingredients</title>
  <link>http://cocktailbuilder.livejournal.com/4926.html</link>
  <description>&lt;p&gt;The title says it all. It&apos;s the first time I&apos;m doing a &quot;release announcement&quot; like this, so I think I&apos;ll share the new things I added to the database this time around. &lt;/p&gt; &lt;p&gt;By the way, you may wonder, how did I prioritize the ingredients to add? There&apos;s been more folks visiting the site in the last few days - just look at the screenshot from Google Analytics below:&lt;/p&gt; &lt;p&gt;&lt;img style=&quot;border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px&quot; height=&quot;224&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/NewBuild90newcocktails110newingredients_13270/image03.png&quot; width=&quot;349&quot; border=&quot;0&quot;&gt; &lt;/p&gt; &lt;p&gt;So I was able to analyze the things that they were looking for and didn&apos;t find, and added them to the database. Here&apos;s the list of new stuff:&amp;nbsp;&amp;nbsp;&lt;/p&gt; &lt;table&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;Cocktails&lt;/td&gt; &lt;td&gt;Ingredients&lt;/td&gt;&lt;/tr&gt; &lt;tr style=&quot;font-size: 9px; vertical-align: top; line-height: 11px&quot;&gt; &lt;td&gt; &lt;p&gt;1960s Black Devil&lt;br&gt;Absecon Fruit Punch&lt;br&gt;Absinthe Cocktail&lt;br&gt;Absolute Pineapple Martini&lt;br&gt;Adios&lt;br&gt;Admiral Nelsons Brew&lt;br&gt;Adonis Cocktail&lt;br&gt;Alabama Cocktail &lt;br&gt;Alaska Cocktail &lt;br&gt;Angels Kiss &lt;br&gt;Angels Kiss 2 &lt;br&gt;Angels Wing &lt;br&gt;Bahama Mama 2 &lt;br&gt;Barracuda Bite &lt;br&gt;Basil Haydens Mint Julep &lt;br&gt;Bermuda Triangle &lt;br&gt;Bo Diddley Shooter &lt;br&gt;Bourbon Sidecar &lt;br&gt;Brandy Alexander &lt;br&gt;Brandy Alexander Classic, 1930 &lt;br&gt;Brandy Strudel &lt;br&gt;Brisk Blue Blazer &lt;br&gt;Bronx Cocktail No2 Dry &lt;br&gt;Californian &lt;br&gt;Candy Apple &lt;br&gt;Captains French Kiss &lt;br&gt;Chocolate Chip Cookie &lt;br&gt;Colorado Prarie Fire &lt;br&gt;Deach Desire &lt;br&gt;Dutch Orange &lt;br&gt;Exterminator Coffee &lt;br&gt;Flirt &lt;br&gt;Funkadelic &lt;br&gt;Green Monster &lt;br&gt;Gunbarrel &lt;br&gt;Illusions &lt;br&gt;Irish Flag &lt;br&gt;Jamaican Me Crazy &lt;br&gt;Jealous Love &lt;br&gt;Keoke Coffee &lt;br&gt;Latino Last Scream &lt;br&gt;Lauries Favorite &lt;br&gt;Mama &lt;br&gt;Martini Cocktail, Dry &lt;br&gt;Martini Cocktail, Sweet &lt;br&gt;Midori Margarita &lt;br&gt;Mollys Milk &lt;br&gt;Mountain Altitude &lt;br&gt;Mountain Grog &lt;br&gt;Mountain Madness &lt;br&gt;Mrs. Greens LMC &lt;br&gt;Mustang &lt;br&gt;Original Bushwacker &lt;br&gt;Original Manhattan &lt;br&gt;Pass It On &lt;br&gt;Pegu &lt;br&gt;Perfect Manhattan &lt;br&gt;Perrier Lemonade &lt;br&gt;Pineapple Smooch &lt;br&gt;Piss In The Snow &lt;br&gt;Pousse Cafe &lt;br&gt;Santa Monica Splash &lt;br&gt;Sapphire Sensation &lt;br&gt;Scooter &lt;br&gt;Sex On Mountain &lt;br&gt;Sex on the Slopes &lt;br&gt;Sexy Lexy &lt;br&gt;Sidecar Cocktail &lt;br&gt;Sidecar Special &lt;br&gt;Ski Tip Coffee &lt;br&gt;Stairway To Heaven &lt;br&gt;Stars And Stripes &lt;br&gt;Stroll Through The Woods &lt;br&gt;Sugar and Spice &lt;br&gt;Sun Burn &lt;br&gt;Sweet Kisses &lt;br&gt;Tic Tac &lt;br&gt;Tokyo Tea &lt;br&gt;Triple Kiss &lt;br&gt;Trouble In Paradise &lt;br&gt;Vitamine Maidas &lt;br&gt;Wake Me In An Hour &lt;br&gt;Ward Eight &lt;br&gt;Westin St. Francis Chocolate Martini &lt;br&gt;Whirlaway &lt;br&gt;White Christmas &lt;br&gt;Wicky Wacky Woo &lt;br&gt;Woodfather &lt;br&gt;Wooly Mamouth &lt;br&gt;Zentini&lt;/p&gt;&lt;/td&gt; &lt;td&gt; &lt;p&gt;151 rum &lt;br&gt;7-UP or Sprite &lt;br&gt;aguardiente &lt;br&gt;Angostura &lt;br&gt;Angostura bitters &lt;br&gt;anise liqueur &lt;br&gt;anisette &lt;br&gt;Applejack &lt;br&gt;Applejack Brandy &lt;br&gt;Bacardi 151 &lt;br&gt;Bacardi 151 rum &lt;br&gt;Bacardi Breezer Orange &lt;br&gt;Bacardi white rum &lt;br&gt;black olive &lt;br&gt;black tea &lt;br&gt;Bols Blue Curacao &lt;br&gt;Brandy &lt;br&gt;brut Champagne &lt;br&gt;burgundy wine &lt;br&gt;California brandy &lt;br&gt;campari &lt;br&gt;cham pagne &lt;br&gt;Chandon sparkling wine &lt;br&gt;chartreuse &lt;br&gt;chilled champagne &lt;br&gt;chopped strawberries &lt;br&gt;Coca Cola &lt;br&gt;Coca-Cola &lt;br&gt;coconut creme &lt;br&gt;coconut liqueur &lt;br&gt;concentrated simple syrup &lt;br&gt;creme de banana &lt;br&gt;creme de bananes &lt;br&gt;creme de cassis &lt;br&gt;creme de coconut &lt;br&gt;creme de monte &lt;br&gt;creme de yvette &lt;br&gt;creme yvette &lt;br&gt;crushed pineapple &lt;br&gt;DeKuyper Tropical Pineapple Liqueur &lt;br&gt;dr pepper &lt;br&gt;dry sherry wine &lt;br&gt;dry white wine &lt;br&gt;egg &lt;br&gt;egg white &lt;br&gt;French roast vanilla bean coffee &lt;br&gt;genever &lt;br&gt;Godiva white chocolate liqueur &lt;br&gt;grappa &lt;br&gt;green chartreuse &lt;br&gt;green olive &lt;br&gt;guava juice &lt;br&gt;heavy sweet cream &lt;br&gt;Hennessy Cognac &lt;br&gt;iced tea &lt;br&gt;Jack Daniels &lt;br&gt;jenever &lt;br&gt;jeniever &lt;br&gt;Johnny Walker Black Label Scotch &lt;br&gt;juice from one lemon &lt;br&gt;Ketel One Vodka &lt;br&gt;korenwijn &lt;br&gt;lemon slice &lt;br&gt;lime cordial &lt;br&gt;lime zest &lt;br&gt;Malibu coconut liqueur &lt;br&gt;margarita mix &lt;br&gt;margarita mix (non-alcoholic) &lt;br&gt;Margarita mixture ( lemon lime mix ) &lt;br&gt;Moet Rose NV (Non Vintage/house blend) Champagne &lt;br&gt;olive &lt;br&gt;Opal Nera &lt;br&gt;orujo &lt;br&gt;Ouzo &lt;br&gt;overproof rum &lt;br&gt;parfait amour &lt;br&gt;parfait damour &lt;br&gt;pepsi &lt;br&gt;Pina Colada Mix &lt;br&gt;pineapple &lt;br&gt;pineapple chunks &lt;br&gt;pink grapfruit juice &lt;br&gt;pink lemonade &lt;br&gt;Pisang Ambon &lt;br&gt;pisco &lt;br&gt;pomace brandy &lt;br&gt;prunelle &lt;br&gt;Que Pentas silver tequila &lt;br&gt;red dinner wine &lt;br&gt;Red vodka &lt;br&gt;rum, 151 &lt;br&gt;Rumple Minze peppermint schnapps &lt;br&gt;Sauza Tres Generations Tequila &lt;br&gt;sparkling white wine &lt;br&gt;Sprite or 7Up &lt;br&gt;squeezed lime juice &lt;br&gt;Stolichnaya Gold vodka &lt;br&gt;Stolichnaya Limonaya &lt;br&gt;tabasco pepper sauce &lt;br&gt;Tanqueray gin &lt;br&gt;tea &lt;br&gt;Tresterbrand &lt;br&gt;Ursus vodka &lt;br&gt;versinthe absinthe &lt;br&gt;white dinner wine &lt;br&gt;worchestershire sauce &lt;br&gt;yellow chartreuse &lt;br&gt;Yellow Galliano&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt; &lt;p&gt;My goal is to make a site that&apos;s useful for the community - and the only way to do that is to look into what folks are trying to do with it and failing, and enabling those things. &lt;/p&gt; &lt;p&gt;Cheers!&lt;br&gt;-cb&lt;/p&gt;</description>
  <category>release announcements</category>
  <lj:security>public</lj:security>
  <lj:reply-count>3</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/4625.html</guid>
  <pubDate>Wed, 10 Jan 2007 19:30:21 GMT</pubDate>
  <title>Persisting User Data Across Visits</title>
  <link>http://cocktailbuilder.livejournal.com/4625.html</link>
  <description>&lt;p&gt;Almost every non-trivial website out there is facing a classical problem: persisting user data across visits (also knows as sessions). For your MySpace account, it&apos;s a list of your friends, and that clever joke you put into the profile to help &lt;a href=&quot;http://www.joelonsoftware.com/items/2006/12/15.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;replicate the DNA&lt;/a&gt;. For &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;CocktailBuilder&lt;/a&gt;, it&apos;s a list of your ingredients and&amp;nbsp;your favorite cocktails. You don&apos;t want to type them in every time, do you? &lt;/p&gt; &lt;p&gt;Now comes the big problem. How do you remember these in a seamless, non-intrusive fashion?&lt;/p&gt; &lt;p&gt;The obvious answer is a login system. Heck, go to myspace, the first thing you see is a logon box. You can&apos;t access almost any of the site&apos;s features without being logged in. Why is that? Because much majority of the site&apos;s information is private (others would get pissed if you saw their mailbox without their permission), so it&apos;s an absolute must to know who the user is before showing them their mailbox. &lt;/p&gt; &lt;p&gt;So, the plus of this approach is relative security of user data. Another plus: you can access your account from any computer. What are the minuses? You know already - you have to register for yet another darn site. Uhhhh. Pick a login. What&apos;s your email. Check that email. Click the link. Your account is now active, please log in. Next time, type the password again. Uhhhhhhh.&lt;/p&gt; &lt;p&gt;What&apos;s the alternative? &lt;/p&gt; &lt;p&gt;Modern, and not-so-modern, browsers provide a feature called &quot;cookies&quot;. Essentially, it&apos;s a way for a website to store a piece of information on a user&apos;s computer, and access it later. All of this can be done seamlessly, without any explicit user action. For example, many websites store tracking cookies on your computer so that they can later on identify you and offer you customized services based on your previous actions. &lt;/p&gt; &lt;p&gt;What&apos;s good? Transparency. Plus it&apos;s reeeally easy to get the desired effect of &quot;hey, they remembered!&quot;. Minuses: cookies are not secure. If you&apos;re a bank, don&apos;t do this. If you&apos;re a bank IT guy, you read this, and you were surprised, please realize that the sky is blue. Cookies can be stolen by other sites, if the browser has a security issue (and browsers have security issues in this area very frequently). Another minus: cookies are an ancient technology. It&apos;s not very pleasant to work with them from JavaScript (you have to serialize your data into a string less than 4KB; I&apos;ll talk about this at a different time). Another minus: cookie data is not accessible on other computers, so you wouldn&apos;t be able to share data between your home and work computers. &lt;/p&gt; &lt;p&gt;There are many websites that go with the third approach - combination of a login system with cookies, so that you only need to log in once a day, and throughout that day, your cookie serves as your identity. Think Amazon: when you come in, you see &quot;Welcome, Bob (or whatever)&quot;. How do they know? The obviously don&apos;t store your IP address, that would be silly (IP addresses change for many consumers relatively frequently). They store a cookie. &lt;/p&gt; &lt;p&gt;So far, for CocktailBuilder, I decided to go with a plain cookies approach. There isn&apos;t any secure data in there - just a list of ingredients and favorite cocktails. Simplicity is key here. Yes, the &quot;travel with my data&quot; feature is missing. I&apos;m not sure of the best way to cover for it. &lt;/p&gt; &lt;p&gt;One idea I had was &quot;send my settings to another computer&quot;, which would generate an email with a hyperlink that has all of the cookie data. User would be able to send this to themself (or a friend), and get all of their settings on another computer. I haven&apos;t thought about this much, do tell me what you think. &lt;/p&gt; &lt;p&gt;Another idea - non-mandatory login system. You want to save your settings - register for an account. You want to load your settings from another computer - log in. Otherwise, everything else is done through cookies. &lt;/p&gt; &lt;p&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/4625.html</comments>
  <category>design decisions</category>
  <lj:security>public</lj:security>
  <lj:reply-count>0</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/4383.html</guid>
  <pubDate>Sat, 06 Jan 2007 14:38:48 GMT</pubDate>
  <title>JavaScript Validation and Compression</title>
  <link>http://cocktailbuilder.livejournal.com/4383.html</link>
  <description>&lt;p&gt;I know it, you’re an AJAX geek. Just like me. You think it’s the next Win32, or GTK, or .NET, .ABC, .DEF, or .whatever. There’s one problem, though: Web 2.0 sites are not just about AJAX. They are about JavaScript and client-side code, as much as they are about client-server interaction.&lt;/p&gt; &lt;p&gt;Look at &lt;a href=&quot;http://analytics.google.com&quot; rel=&quot;nofollow&quot;&gt;Google Analytics&lt;/a&gt;. Their sexiness comes from their gorgeous client-side Flash, not because they get stuff asynchronously from the server. Look at &lt;a href=&quot;http://maps.google.com&quot; rel=&quot;nofollow&quot;&gt;Google Maps&lt;/a&gt;, the original Ajax app. You may say “#$@%, what are you talking about: Google Maps are all about the async stuff!”. No, Google Maps are about drag-and-drop. If you needed to click the stupid “left arrow” button to get the map of what’s to the west, you wouldn’t care that only a part of the screen is being refreshed. &lt;p&gt;One of my friends frequently makes me repeat the following phrase: “Ajax is not a silver bullet”. He’s so damn right.  &lt;p&gt;What is the silver bullet? Beautiful user models. Intuitive interactions. Visual effects on-par with rich client applications (fade in/out, cinematic expand/collapse, sexy highlight, animated movements). And if you want to get those in your Web 2.0 app, you need to do some major JavaScript work. &lt;p&gt;Yes, there are platforms out there that will help. I won’t talk about those here. Instead, I’ll focus on two categories of tools that I found to be really helpful in my JavaScript development efforts lately; these two aren’t talked about much, to my sadness.  &lt;p&gt;&lt;b&gt;JavaScript Validation&lt;/b&gt; &lt;p&gt;JavaScript is a beautiful language, too bad it’s abused too often. An inexperienced developer can write something (== vs ===, or “var vs no var”), not even know what the difference is, and have it work &lt;i&gt;most of the time&lt;/i&gt;. And that’s the problem. JS is a language without a standard compiler, without type checking, without normal IDE’s. This means that JS developers have to be really careful; but humans make mistakes, and they need tools.  &lt;p&gt;I found one great tool to do JS validation; I use it to do automatic code review of my JavaScript files before I check in. The tool is called &lt;a href=&quot;http://www.jslint.com&quot; rel=&quot;nofollow&quot;&gt;JSLint&lt;/a&gt;; here’s the DOS batch file snippet I use:  &lt;blockquote&gt; &lt;p&gt;echo Verifying Javascript files using JSLint:  &lt;p&gt;FOR %%f IN (*.js) DO (&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp; echo Processing %%f..&lt;br&gt;&amp;nbsp;&amp;nbsp; cscript //B //E:jscript ../tools/jslint.js &amp;lt; %%f&lt;br&gt;)&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;If something’s wrong with any of your js files, JSLint will complain, pointing out the exact line and type of issue, for example:  &lt;blockquote&gt; &lt;p&gt;Lint at line 298 character 23: Use &apos;!==&apos; to compare with &apos;null&apos;.&lt;br&gt;if(el.currentStyle != null)&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;&lt;b&gt;&lt;/b&gt; &lt;p&gt;&lt;b&gt;JavaScript Compression&lt;/b&gt; &lt;p&gt;When you have lots of JavaScript (and man do I do), you need some way to compress it. The less bytes each client has to download, the snappier the load performance is; this also helps to scale your server to more users. &lt;p&gt;There are a few ways to compress your JS: the easiest one involves removing all comments and whitespaces (browsers don’t usually execute those :-)). More complex ones involve renaming variables and methods to shorter names: this helps if you’re like me, who loves using really descriptive variable names, even in JS. Others do obfuscation, which compresses the JS further, and prohibits humans from easily parsing your client-side code.  &lt;p&gt;I’ve used several tools in the last few months: one is called JSMin; I believe it’s written by the same person who wrote JSLint. This tool does everything except obfuscation. I’ve had good experience with it, but have come to use a different tool as it turned out to be a tad more powerful: a tool called Packer from &lt;a href=&quot;http://dean.edwards.name/packer/&quot; rel=&quot;nofollow&quot;&gt;http://dean.edwards.name/packer/&lt;/a&gt;.  &lt;p&gt;Here’s the way I use it:  &lt;p&gt;1) I have many .js files, a few of which were written by third parties (those are in separate&amp;nbsp;.js files to keep me sane). Before compression, I append all of these files together into a single temp.js file to achieve higher compression ratio. &lt;br&gt;2) Compress the temp.js file using Packer.  &lt;p&gt;Here’s a snippet from my build script (DOS batch file):  &lt;blockquote style=&quot;height: 200px; overflow: auto&quot;&gt; &lt;p&gt;echo Merging all javascript files into one...&lt;br&gt;SET MERGED_FILE_NAME=&quot;compressed\\core.js&quot;&lt;br&gt;SET TEMP_FILE_NAME=&quot;temp.txt&quot;&lt;/p&gt; &lt;p&gt;IF EXIST %TEMP_FILE_NAME% del %TEMP_FILE_NAME%&lt;/p&gt; &lt;p&gt;FOR %%f IN (*.js) DO (&lt;br&gt;&amp;nbsp;&amp;nbsp; echo. &amp;gt;&amp;gt; %TEMP_FILE_NAME%&lt;br&gt;&amp;nbsp;&amp;nbsp; type %%f &amp;gt;&amp;gt; %TEMP_FILE_NAME%&lt;br&gt;&amp;nbsp;&amp;nbsp; echo. &amp;gt;&amp;gt; %TEMP_FILE_NAME%&lt;br&gt;)&lt;/p&gt; &lt;p&gt;echo Compressing Javascript files using Packer..&lt;br&gt;IF EXIST %MERGED_FILE_NAME% del %MERGED_FILE_NAME%&lt;/p&gt; &lt;p&gt;CScript /nologo ..\devtools\packer\pack.wsf %TEMP_FILE_NAME% &amp;gt;&amp;gt; %MERGED_FILE_NAME% &lt;p&gt;del %TEMP_FILE_NAME%&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Some stats for your mathematical pleasures: uncompressed JavaScript for the &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;cocktail builder&lt;/a&gt; takes up 91.1KB. Compressed, it’s 33.2KB. I’m sure you can calculate the square root of the rate of change of flow of these kilobytes, if transmission is happening on the equator at the top of a 1500ft mountain.  &lt;p&gt;Whatever. Enjoy your JavaScript :-)&lt;br&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/4383.html</comments>
  <category>development practices</category>
  <lj:security>public</lj:security>
  <lj:reply-count>0</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/4192.html</guid>
  <pubDate>Sat, 06 Jan 2007 14:31:38 GMT</pubDate>
  <title>SQL Injection Defense</title>
  <link>http://cocktailbuilder.livejournal.com/4192.html</link>
  <description>&lt;p&gt;We all heard of it. Sites going down because of SQL injections. Embarrassed admins. Credibility loss among users. Really simple concept – if your server-side script takes user input verbatim, and issues SQL statements based on it, you will be screwed. There are great scripts out there that will screw you automatically, without personal involvement of someone evil.&lt;/p&gt; &lt;p&gt;SQL injection is scary. Let’s talk about ways you can (and must!) protect your web app against it.  &lt;p&gt;&lt;b&gt;1. Verify data types of all non-string user inputs&lt;br&gt;&lt;/b&gt;Here’s what I do: create a separate class that does all communications to the database. I call it DataRetrieval. In that class, every method just gets you some data from the DB (i.e. executes a SQL query given some parameters), for example,  &lt;blockquote&gt; &lt;p&gt;&lt;span style=&quot;color: #0000ff&quot;&gt;class&lt;/span&gt; DataModification {&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #0000ff&quot;&gt;&amp;nbsp;&amp;nbsp; public&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;static&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;function&lt;/span&gt; saveCocktailFeedback&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ($cocktailID,&amp;nbsp;$starRating,&amp;nbsp;$userInfo) &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&amp;nbsp;... }&lt;br&gt;&amp;nbsp; &lt;span style=&quot;color: #0000ff&quot;&gt;public&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;static&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;function&lt;/span&gt; saveSiteFeedback($feedbackText, $userInfo) &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { ... }&lt;br&gt;&amp;nbsp; &lt;span style=&quot;color: #0000ff&quot;&gt;public&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;static&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;function&lt;/span&gt; registerUserSession($userToken)&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { ... }&lt;br&gt;&amp;nbsp; ...&lt;br&gt;}&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Make it a habit to verify each and every non-string parameter in the method body before doing any SQL commands, for example:  &lt;p&gt;&lt;b&gt;&lt;/b&gt; &lt;blockquote&gt; &lt;p&gt;&lt;span style=&quot;color: #0000ff&quot;&gt;public&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;static&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;function&lt;/span&gt; saveCocktailFeedback &lt;br&gt;&amp;nbsp;&amp;nbsp; ($cocktailID,&amp;nbsp;$starRating,&amp;nbsp;$userInfo) { &lt;/p&gt; &lt;p&gt;&lt;span style=&quot;color: #0000ff&quot;&gt;&amp;nbsp;&amp;nbsp; if&lt;/span&gt; (!&lt;span style=&quot;color: #0000ff&quot;&gt;is_numeric&lt;/span&gt;($cocktailID) || !&lt;span style=&quot;color: #0000ff&quot;&gt;is_numeric&lt;/span&gt;($starRating)) { &lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;color: #008000&quot;&gt;// someone is trying to hack us; do something (more below) &lt;br&gt;&lt;/span&gt;&amp;nbsp;&amp;nbsp; } &lt;/p&gt; &lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;...&lt;br&gt;} &lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;&lt;b&gt;2. Escape all string inputs &lt;/b&gt; &lt;p&gt;On MySQL, this is as simple as  &lt;blockquote&gt; &lt;p&gt;$str = &lt;span style=&quot;color: #0000ff&quot;&gt;mysql_escape_string&lt;/span&gt;(&lt;span style=&quot;color: #0000ff&quot;&gt;&lt;span style=&quot;color: #0000ff&quot;&gt;stripslashes&lt;/span&gt;&lt;/span&gt;($str));&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;I usually create a wrapper class for working with the database (even when I’m using a prepackaged DB class as excellent as ezSQL), and define a function called DB::escape(), that does exactly that.  &lt;p&gt;&lt;b&gt;3. Keep track of security violation attempts&lt;/b&gt;  &lt;p&gt;If someone’s trying to hack you, it would be nice to know (1) when it happens and (2) who was it (3) how hard did they try  &lt;p&gt;Remember the “someone’s trying to hack us, do something” comment in suggestion 1? Set up a function similar to the following  &lt;blockquote&gt; &lt;p&gt;&lt;span style=&quot;color: #0000ff&quot;&gt;public&lt;/span&gt; &lt;span style=&quot;color: #0000ff&quot;&gt;static&lt;/span&gt; function log_security_event() { &lt;br&gt;&lt;span style=&quot;color: #008000&quot;&gt;&amp;nbsp;&amp;nbsp; // get stack trace into the $trace variable&lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span style=&quot;color: #008000&quot;&gt;&lt;/span&gt;&amp;nbsp;&amp;nbsp; $sql = &quot;VALUES INSERT INTO securityevent (Trace, IP)&lt;span style=&quot;color: #8b0000&quot;&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;VALUES (&apos;&quot;&lt;span style=&quot;color: #8b0000&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color: #0000ff&quot;&gt;DB::escape&lt;/span&gt;($trace)&lt;font color=&quot;#8b0000&quot;&gt; &lt;/font&gt;&lt;span style=&quot;color: #8b0000&quot;&gt;. &lt;/span&gt;&quot;&apos;, &apos;&quot;&lt;span style=&quot;color: #8b0000&quot;&gt;.&amp;nbsp;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;$_SERVER[&apos;REMOTE_ADDR&apos;] . &quot;&apos;)&quot;&lt;span style=&quot;color: #8b0000&quot;&gt;;&lt;/span&gt;&lt;/p&gt; &lt;p&gt;&lt;span style=&quot;color: #8b0000&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #008000&quot;&gt;&amp;nbsp;&amp;nbsp; // execute the statement &lt;br&gt;&lt;/span&gt;}&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;This way, when someone tries to hack you, you know exactly what they tried to use, and you can see patterns in their search; you could then determine whether you’re dealing with an input error (oops, someone typed in a quote in a field accidentally) or an intentionally malicious user. You can even gauge the experience level of the hacker by the queries he tries to issue.  &lt;p&gt;This function would fit very nicely into the DB wrapper class that I mentioned earlier.  &lt;p&gt;&lt;b&gt;&lt;/b&gt; &lt;p&gt;&lt;b&gt;4. Defense-in-depth: limit SQL user rights&lt;/b&gt;  &lt;p&gt;Your PHP application issues a ton of queries to the production database. Poor database, I tell you. If I had to do all those queries, I’d just spit out junk and see if the app server can handle it :-).  &lt;p&gt;Now, seriously: if the attacker was somehow able to run queries on your MySQL server, you want to limit the impact. For example, in the &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;cocktail builder&lt;/a&gt;, PHP code should be able to query the known-and-sexy list of cocktails (read only! not modify!); report security violations (write only – only insert, not even modify; no reading allowed, either), and track and report on user feedback around cocktails (read and write).  &lt;p&gt;So, as a part of your build script, you need to set the most-restrictive-possible permissions for the MySQL user that your PHP script will be running as. Here’s what mine looks like, abridged:  &lt;blockquote style=&quot;overflow: hidden&quot;&gt; &lt;p&gt;&lt;font color=&quot;green&quot;&gt;/* By default, all tables can be read, unless revoked below */&lt;/font&gt; &lt;br&gt;&lt;font color=&quot;blue&quot;&gt;grant&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;SELECT&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;on&lt;/font&gt; cocktailbuilder.* &lt;font color=&quot;blue&quot;&gt;to&lt;/font&gt; cbuser@localhost identified &lt;font color=&quot;blue&quot;&gt;by&lt;/font&gt; &apos;xxxxxxx’; &lt;/p&gt; &lt;p&gt;&lt;font color=&quot;green&quot;&gt;/* Only the following tables can be modified, and only by performing &lt;br&gt;an INSERT (not UPDATE!). Note that some tables (i.e. securityevent) &lt;br&gt;will have INSERT, but not SELECT rights. &lt;/font&gt;&lt;font color=&quot;green&quot;&gt;*/&lt;/font&gt; &lt;br&gt;&lt;font color=&quot;blue&quot;&gt;grant&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;INSERT&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;on&lt;/font&gt; cocktailbuilder.rating &lt;font color=&quot;blue&quot;&gt;to&lt;/font&gt; cbuser@localhost; &lt;br&gt;&lt;font color=&quot;blue&quot;&gt;grant&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;INSERT&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;on&lt;/font&gt; cocktailbuilder.securityevent &lt;font color=&quot;blue&quot;&gt;to&lt;/font&gt; cbuser@localhost; &lt;br&gt;&lt;font color=&quot;blue&quot;&gt;grant&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;INSERT&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;on&lt;/font&gt; cocktailbuilder.userfeedback &lt;font color=&quot;blue&quot;&gt;to&lt;/font&gt;cbuser@localhost; &lt;/p&gt; &lt;p&gt;&lt;font color=&quot;green&quot;&gt;/* Some tables aren’t meant to be read by the app, only the DBA */&lt;/font&gt; &lt;br&gt;&lt;font color=&quot;blue&quot;&gt;revoke&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;SELECT&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;on&lt;/font&gt; cocktailbuilder.securityevent &lt;font color=&quot;blue&quot;&gt;from&lt;/font&gt; cbuser@localhost; &lt;br&gt;&lt;font color=&quot;blue&quot;&gt;revoke&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;SELECT&lt;/font&gt; &lt;font color=&quot;blue&quot;&gt;on&lt;/font&gt; cocktailbuilder.userfeedback &lt;font color=&quot;blue&quot;&gt;from&lt;/font&gt; cbuser@localhost; &lt;br&gt;&lt;font color=&quot;blue&quot;&gt;flush privileges&lt;/font&gt;; &lt;br&gt;&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;Unfortunately, this script can’t be generated automatically (mysqldump can’t dump permissions – at least I don’t know of a good portable way to do this), so you just have to keep this file updated by hand. Good news is that this shouldn’t be too hard: you don’t add tables to your DB that often, do you? &lt;/p&gt; &lt;p&gt;Oh, and one more thing: if you have several applications running on a single MySQL box, make sure that each app gets its own login (like &lt;i&gt;cbuser&lt;/i&gt; in my sample script above), and that the logins have zero permissions outside of the designated database. This way, if a hacker screws up one of your apps, others stand a chance.  &lt;p&gt;&lt;b&gt;5. Defense-in-depth: production database backups&lt;/b&gt;  &lt;p&gt;This is good against hackers, fires, and your cat chewing the network cable while database sync is happening. Do the (full, not incremental!) database backups regularly, and keep the dumps on a different box (or, even better, at a remote site).  &lt;p&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/4192.html</comments>
  <category>development practices</category>
  <lj:security>public</lj:security>
  <lj:reply-count>3</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/3851.html</guid>
  <pubDate>Sat, 06 Jan 2007 13:58:30 GMT</pubDate>
  <title>Developing Against a Live Database</title>
  <link>http://cocktailbuilder.livejournal.com/3851.html</link>
  <description>&lt;p&gt;All right, friends, it’s time for some serious development talk. You and a couple of your friends are building the next &lt;i&gt;it, &lt;/i&gt;the Web 2.0 app of the future. You have a source control system set up, you even have a bug tracking system, you even have a spec and someone (not you!) who tests your software (may I remind you of the famous &lt;a href=&quot;http://www.joelonsoftware.com/articles/fog0000000043.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Joel Test&lt;/a&gt;?). You’re heading for success. &lt;/p&gt; &lt;p&gt;Your web app has a database back end, and you already have a few users out there, playing with your system; their data lives in your “production” database. You also have local dev boxes, with complete copies of the production environment, with development versions of the database. Anyone can mess with the development databases without causing havoc to real users’ data.  &lt;p&gt;If you don’t have any of that, don’t read this article. Instead, go read Joel Spolsky’s &lt;a href=&quot;http://www.joelonsoftware.com/&quot; rel=&quot;nofollow&quot;&gt;blog&lt;/a&gt; or book “Joel on Software”. He’ll explain why all of these pieces are necessary.  &lt;p&gt;If you do, however, you’re on the right track. But.  &lt;p&gt;There’s always a “but” (or a butt?).  &lt;p&gt;How do you propagate the changes to the database schema, if there are any, to the production version of the database? You’re still doing development on the system, schema and data are changing with each build, you can’t afford a DBA to do crazy DIFF’s between your dev database and production database…  &lt;p&gt;&lt;br&gt;&lt;b&gt;Simple, and flawed, approach&lt;/b&gt;  &lt;p&gt;You already have a version control system. Let’s assume you’re using MySQL as your database (I am). You can just use &lt;a href=&quot;http://dev.mysql.com/doc/refman/5.0/en/mysqldump.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;mysqldump&lt;/a&gt; to dump the schema and data of your development database, and check in those .sql files into source control. Then, your build script just drops the production database, and recreates it from scratch using the .sql files from the source tree.  &lt;p&gt;Note that you definitely want two dumps: one for the schema, and one for the data. This way it’s much easier to track schema changes in your source control system.  &lt;p&gt;Only one, but very big problem: user-generated content gets lost.  &lt;p&gt;You may say “copy over user generated content to the dev box before checking stuff in”. But the build isn’t instantaneous, and everyone who’s been doing something on your site during the time between your “sync” and the build will lose their data. Not good.  &lt;p&gt;You may say “screw user content”. No, you didn’t say that. I just heard it from some weirdo in the back of the room. Screwing your users is a safe way to lose early adopters, who are the only way for you to deliver a quality app &lt;i&gt;fast&lt;/i&gt;. If you don’t get feedback from those enthusiasts (and you won’t if you piss them off by continuously throwing away the data that they’ve put in), you’ll lose.&lt;p&gt;&lt;br&gt;&lt;b&gt;Complex, and successful, approach&lt;/b&gt;  &lt;p&gt;Identify which tables in your database will contain user-generated content. For the &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;cocktail builder&lt;/a&gt; site, these are the mainly the tables with user ratings and user actions, and table for reporting crashes.  &lt;p&gt;Your build script will no longer drop the entire database and rebuild it from scratch; it will drop all tables except for tables with user content. That is, as a part of the check-in process, split your output .sql files as following:  &lt;p&gt;&lt;b&gt;schema.sql&lt;/b&gt;: DDL for all tables except those with user-generated content&lt;br&gt;&lt;b&gt;schema-extra.sql&lt;/b&gt;: DDL for tables with user-generated content&lt;br&gt;&lt;b&gt;data.sql&lt;/b&gt;: actual INSERT statements for all tables other than those with user-generated content&lt;br&gt;&lt;b&gt;data-extra.sql&lt;/b&gt; (optional): you get the drill. This is optional because there’s not always a point in including user-generated data into your build – just back it up on the production database side, and sync it to the dev boxes once in a while.  &lt;p&gt;Your script to dump the dev databases may look like this file below (this is a batch script – I do my development on a Windows box; please don’t flame me, I’m built of a fire-retardant):  &lt;blockquote style=&quot;height: 300px; overflow: auto&quot;&gt; &lt;p&gt;@echo off  &lt;p&gt;REM Full path to the MYSQL utilities. Required. &lt;br&gt;SET MYSQLDUMP=&quot;C:\xampp\xampp\mysql\bin\mysqldump&quot;&lt;br&gt;SET MYSQL=&quot;C:\xampp\xampp\mysql\bin\mysql&quot;  &lt;p&gt;REM Database settings&lt;br&gt;SET DBNAME=mydb&lt;br&gt;SET DBUSER=myuser &lt;p&gt;cd ..\setup_sql  &lt;p&gt;REM Producing schema and data files for the build. They are &lt;br&gt;REM called &quot;schema.sql&quot; and &quot;data.sql&quot;. &lt;br&gt;REM Using MySQLDump. Path is specified in the variable above.  &lt;p&gt;REM ----- Delete old versions of the files. &lt;br&gt;del schema.sql&lt;br&gt;del data.sql  &lt;p&gt;echo Generating core schema and data files...&lt;br&gt;REM ----- Using settings specified in the configuration file, ignore &lt;br&gt;REM ----- a few tables that shouldn&apos;t go into the incremental build &lt;br&gt;REM ----- (user-generated content should not be discarded on upgrade).&lt;br&gt;REM ----- The schemas for those tables will go into a separate file&lt;br&gt;REM ----- that can be used to initiate a &quot;full from scratch&quot; build. &lt;br&gt;REM ----- Content for those is not a part of the build.&lt;br&gt;SET ResultSkipString=&lt;br&gt;SET ResultIncludeString=--tables &lt;br&gt;echo The following tables will not be included in the core build:&lt;br&gt;FOR /F %%t IN (userContentTables.conf) DO (&lt;br&gt;&amp;nbsp;&amp;nbsp; echo - %%t&lt;br&gt;&amp;nbsp;&amp;nbsp; SET ResultSkipString=!ResultSkipString! --ignore-table=%DBNAME%.%%t&lt;br&gt;&amp;nbsp;&amp;nbsp; SET ResultIncludeString=!ResultIncludeString! %%t&lt;br&gt;)&lt;/p&gt; &lt;p&gt;REM ----- Dump the actual SQL schema/data creation scripts...&lt;br&gt;SET CommonDumpSettings=--comments=false --user=%DBUSER% --single-transaction --no-set-names --skip-add-locks -B %DBNAME%&lt;br&gt;&quot;%MYSQLDUMP%&quot; --result-file=schema.sql --no-data %ResultSkipString% %CommonDumpSettings%  &lt;p&gt;echo USE `%DBNAME%` &amp;gt; schema-extra.sql&lt;br&gt;&quot;%MYSQLDUMP%&quot; --no-data %CommonDumpSettings% %ResultIncludeString% &amp;gt;&amp;gt; schema-extra.sql  &lt;p&gt;&quot;%MYSQLDUMP%&quot; --result-file=data.sql --no-create-db --no-create-info --skip-extended-insert --skip-disable-keys %ResultSkipString% %CommonDumpSettings% &lt;br&gt;&lt;/p&gt; &lt;p&gt;echo Done&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;And the mentioned file &lt;em&gt;userContentTables.conf &lt;/em&gt;is just a newline-separated list of table names&amp;nbsp;that contain user content.&lt;/p&gt; &lt;p&gt;Have fun!&lt;br&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/3851.html</comments>
  <category>development practices</category>
  <lj:security>public</lj:security>
  <lj:reply-count>3</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/3007.html</guid>
  <pubDate>Sat, 06 Jan 2007 13:33:35 GMT</pubDate>
  <title>Generating Semi-Dynamic Content</title>
  <link>http://cocktailbuilder.livejournal.com/3007.html</link>
  <description>&lt;p&gt;Remember how we talked about using a static list of ingredients for auto-complete, and downloading that static list to the client during first load? Well, that static list needs to come from somewhere; besides, it’s not really &lt;i&gt;static.&lt;/i&gt;&lt;/p&gt;&lt;p&gt;Sometimes, a recipe will come along, quoting an ingredient that the system doesn’t yet have; all ingredients are stored in a database, and it’s relatively easy to add an ingredient there. But hey, we have that static lookup list now, how do we get that to work? &lt;/p&gt;&lt;p&gt;The “static” list is just a JavaScript file that looks similar to the following: &lt;/p&gt;&lt;blockquote&gt;ingredientOptions = [&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;&lt;font color=&quot;#242424&quot;&gt;&amp;nbsp;&amp;nbsp; &lt;/font&gt;new&lt;/span&gt; Ingredient(&quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;7-up&lt;/span&gt;&quot;, 8), &lt;br /&gt;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;COLOR: #0000ff&quot;&gt;new&lt;/span&gt; Ingredient(&quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;absinthe&lt;/span&gt;&quot;, 5), &lt;br /&gt;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;COLOR: #0000ff&quot;&gt;new&lt;/span&gt; Ingredient(&quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;absolut citron&lt;/span&gt;&quot;, 3), &lt;br /&gt;&amp;nbsp;&amp;nbsp; ... &lt;br /&gt;]&lt;/blockquote&gt;&lt;p&gt;Basically, it’s just a list of 500 ingredient names and their popularities (for sorting). This list changes relatively rarely (when new ingredients are added – I’d say once a week at most). However, it does change, so we have to have a way to generate it from the database (copy-paste is a bad idea :-)). &lt;/p&gt;&lt;p&gt;I just used a PHP script that talks to the database, asks for the ingredient list, and generates the entire JS file from scratch. Roughly, the code looks like this: &lt;/p&gt;&lt;blockquote style=&quot;overflow: hidden&quot;&gt;&lt;p&gt;&lt;span style=&quot;COLOR: #008000&quot;&gt;// Get the list of ingredients from the database &lt;br /&gt;&lt;/span&gt;$data = DataRetrieval::getAllIngredients(); &lt;/p&gt;&lt;p&gt;&lt;span style=&quot;COLOR: #008000&quot;&gt;// Create the output string &lt;br /&gt;&lt;/span&gt;$output = &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;ingredientOptions = [\r\n&lt;/span&gt;&quot;; &lt;br /&gt;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;foreach&lt;/span&gt; ($data &lt;span style=&quot;COLOR: #0000ff&quot;&gt;as&lt;/span&gt; $item) { &lt;br /&gt;&amp;nbsp;&amp;nbsp; $output .= &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;new Ingredient(\&quot;&lt;/span&gt;&quot; . strtolower($item-&amp;gt;Name) . &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;\&quot;&lt;/span&gt;&quot; . $item-&amp;gt;popularity . &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;)&lt;/span&gt;&quot;;&amp;nbsp;&lt;br /&gt;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;&amp;nbsp;&amp;nbsp; if&lt;/span&gt; ($item != $data[&lt;span style=&quot;COLOR: #0000ff&quot;&gt;sizeof&lt;/span&gt;($data) - 1]) { &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; $output .= &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;,\r\n&lt;/span&gt;&quot;; &lt;br /&gt;&amp;nbsp;&amp;nbsp; } &lt;br /&gt;} &lt;br /&gt;$output .= &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;\r\n];&lt;/span&gt;&quot;; &lt;/p&gt;&lt;p&gt;&lt;span style=&quot;COLOR: #008000&quot;&gt;// Actually write the JavaScript file &lt;br /&gt;&lt;/span&gt;$OUTPUT_FILE_NAME = ‘ingredients.js&apos;; &lt;br /&gt;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;if&lt;/span&gt; (!$file_handle = fopen($OUTPUT_FILE_NAME,&quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;w&lt;/span&gt;&quot;)) { &lt;br /&gt;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;COLOR: #0000ff&quot;&gt;die&lt;/span&gt;(&quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;Cannot open file&lt;/span&gt;&quot;); &lt;br /&gt;} &lt;br /&gt;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;if&lt;/span&gt; (!fwrite($file_handle, $output)) { &lt;br /&gt;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;COLOR: #0000ff&quot;&gt;die&lt;/span&gt;(&quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;Cannot write to file&lt;/span&gt;&quot;); &lt;br /&gt;} &lt;/p&gt;&lt;p&gt;fclose($file_handle); &lt;br /&gt;echo &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;Success. Output saved to &lt;/span&gt;&quot; . $OUTPUT_FILE_NAME . &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;\n&lt;/span&gt;&quot;; &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;There are many other places in the &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;cocktail builder&lt;/a&gt; where I’m using this “js generation” technique. For example, it’s known to be good to have all application UI strings separately from application logic and even presentation. It helps with several things: &lt;/p&gt;&lt;p&gt;1) Localization&lt;br /&gt;2) Strings consistency &lt;/p&gt;&lt;p&gt;One more thing to note is that UI strings may be references in two completely different contexts: in .php files, where strings are used to generate server-side presentation piece, and in .js files, where strings are most frequently error messages or tiny things like the “no matches” string for auto-complete. Some weird strings may appear in either context. But, as you might have guessed, it would be really nice to have &lt;i&gt;all &lt;/i&gt;UI strings live in one place, no matter where they are referenced – in client code or in server code. &lt;/p&gt;&lt;p&gt;So I created a table in MySQL, called UIStrings, where all strings will be stored an edited: &lt;/p&gt;&lt;p&gt;&lt;a atomicselection=&quot;true&quot; href=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/8034c0bb36de_4BA0/clip_image0022.jpg&quot; rel=&quot;nofollow&quot;&gt;&lt;img style=&quot;BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px&quot; height=&quot;133&quot; alt=&quot;&quot; width=&quot;470&quot; border=&quot;0&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/8034c0bb36de_4BA0/clip_image0021.jpg&quot; /&gt;&lt;/a&gt; &lt;/p&gt;&lt;p&gt;Then, I created two scripts – one to generate a server side include file strings.php.inc, and another one for client side strings: strings.js. These scripts are trivially different from the one quoted above. &lt;/p&gt;&lt;p&gt;Here’s a part of the listing of the generated strings.php.inc: &lt;/p&gt;&lt;blockquote style=&quot;overflow: hidden&quot;&gt;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;class&lt;/span&gt; UIText { &lt;br /&gt;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;COLOR: #0000ff&quot;&gt;const&lt;/span&gt; COCKTAIL_DETAILS_ACTION_ADD_TO_FAVORITES = &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;add to favorites&lt;/span&gt;&quot;;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp; const&lt;/span&gt; COCKTAIL_DETAILS_ACTION_EMAIL = &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;email a friend&lt;/span&gt;&quot;; &lt;br /&gt;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;COLOR: #0000ff&quot;&gt;const&lt;/span&gt; COCKTAIL_DETAILS_ACTION_EMAIL_MAIL_SUBJECT = &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;Cocktail: %s&lt;/span&gt;&quot;;&lt;br /&gt;&amp;nbsp;&amp;nbsp; ...&lt;br /&gt;}&lt;/blockquote&gt;&lt;p&gt;In the actual PHP code, if I need to reference a UI string, I’ll just write &lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;UIText:: COCKTAIL_DETAILS_ACTION_EMAIL &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Similarly, the listing of the generated strings.js file looks like: &lt;/p&gt;&lt;blockquote&gt;&lt;span style=&quot;COLOR: #0000ff&quot;&gt;var&lt;/span&gt; UIText = []; &lt;br /&gt;UIText.INPUT_BOX_HINT_TEXT = &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;add ingredient...&lt;/span&gt;&quot;; &lt;br /&gt;UIText.INPUT_BOX_NO_MATCHES = &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;No matches&lt;/span&gt;&quot;; &lt;br /&gt;UIText.INPUT_FEEDBACK_HINT_TEXT = &quot;&lt;span style=&quot;COLOR: #8b0000&quot;&gt;give feedback..&lt;/span&gt;&quot;; &lt;/blockquote&gt;&lt;p&gt;And if I need to reference a UI string in JavaScript code, I’ll just write &lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;UIText.INPUT_BOX_HINT_TEXT &lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;One last thing to note is that these generation steps need to be completely automated – they need to be a part of your build or checkin processes. &lt;/p&gt;&lt;p&gt;cb&lt;/p&gt;&lt;p&gt;P.S. I use the same “generated” .js for client-side usage statistics gathering, but that’s a different story. Let me know if you want me to share it ;-)&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/3007.html</comments>
  <category>development practices</category>
  <lj:security>public</lj:security>
  <lj:reply-count>0</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/2758.html</guid>
  <pubDate>Sat, 06 Jan 2007 13:17:04 GMT</pubDate>
  <title>AutoComplete: Which items to show?</title>
  <link>http://cocktailbuilder.livejournal.com/2758.html</link>
  <description>&lt;p&gt;Some time ago, I &lt;a href=&quot;http://cocktailbuilder.livejournal.com/2181.html&quot; rel=&quot;nofollow&quot;&gt;wrote&lt;/a&gt; how freaking cool the auto-complete system is for ingredient entry. If you don’t believe me, go take a look at its implementation – I actually have it &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;done&lt;/a&gt;. For the lazy ones – folks like me - it ended up looking something like this: &lt;/p&gt; &lt;p&gt;&lt;img height=&quot;184&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/AutoCompleteWhichitemstoshow_4A4B/clip_image002.jpg&quot; width=&quot;182&quot; border=&quot;0&quot;&gt; &lt;p&gt;Sexy, huh?  &lt;p&gt;Yea, whatever. I know what you’re gonna say – I’ve seen this a billion times.  &lt;p&gt;You have, you’re right. But one thing you most likely never thought about when you used a similar sexy-auto-complete textbox: which matches show up? &lt;p&gt;Obviously, you want to show ingredient names that have the stuff that the user typed in as a substring. So, user types “vo”, you show “&lt;b&gt;vo&lt;/b&gt;dka” and “absolute &lt;b&gt;vo&lt;/b&gt;dka”. But you don’t want to show “Cuer&lt;b&gt;vo&lt;/b&gt; tequila”, do you? So, rule #1: only show stuff that has user’s substring as the beginning of the word. &lt;p&gt;Now, the more interesting question: how do you want the suggestions ordered? Few options here: &lt;ol&gt; &lt;li&gt;Alphabetically. Simple to implement, easy to understand, cool. One problem: if there are 50 items that match (as there are with the substring “vo”), your auto-complete text box is not really useful. &lt;/li&gt; &lt;li&gt;Depending on how frequently the item is used in cocktails. This is a pretty good measure of popularity; if there’s 100 cocktails that use orange juice, it’s probably because lots of people have orange juice in their bars, so you can infer that orange juice should show up before Stoli Orange for the query “ora”. However, this has drawbacks: what if your cocktail database is skewed?&amp;nbsp;&lt;/li&gt; &lt;li&gt;Depending on the number of previous users of the system that have added this ingredient. This means that the system is getting smarter and smarter as the number of its users increases; this is definitely a good thing. Drawback: chicken-and-egg problem, you need initial users to make the system smart, and those initial users will suffer. &lt;/li&gt;&lt;/ol&gt; &lt;p&gt;So far, I went with approach 2 – just because the &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;site&lt;/a&gt; doesn’t have too many users yet (do let your friends know about it if you like it!), but I instrumented the tools necessary to know what ingredients users are adding, so I’ll switch to 3 at some point.  &lt;p&gt;Cheers!&lt;br&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/2758.html</comments>
  <category>design decisions</category>
  <lj:security>public</lj:security>
  <lj:reply-count>1</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/2343.html</guid>
  <pubDate>Sat, 06 Jan 2007 12:48:58 GMT</pubDate>
  <title>Which Cocktails to Show?</title>
  <link>http://cocktailbuilder.livejournal.com/2343.html</link>
  <description>&lt;p&gt;All right, we’re trying to show the user what they can make from what they got in the bar. Cool. &lt;/p&gt; &lt;p&gt;We know what they got in the bar (see previous post); now, we better show them some useful cocktail recipes, or they’ll sue the hell out of poor me for false advertising :-). And hey, I gotta pay for hosting, no dollars to waste on legal fees, sorry. &lt;p&gt;[Side note: &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;cocktailbuilder.com&lt;/a&gt; is currently hosted on my home server, an ancient Linux box - PIII 500MHz. 40KB/s upload rate. $30/month for cable internet with Millennium Digital Media – btw, wonderful ISP.] &lt;p&gt;All right, jokes aside. We need some alcohol here.  &lt;p&gt;I (my second self), the semi drunk college student Jane, am holding a party. I go to San Jose State, so I managed to sneak out some lemons out of my auntie’s back yard, so these lemons end up on my ingredient list. Now back to &lt;i&gt;cb&lt;/i&gt;, the poor site owner that’s about to be sued; he’s thinking whether to show &lt;i&gt;&lt;a href=&quot;http://www.cocktailbuilder.com/1940s_blue_blazer.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;1940s blue blazer&lt;/a&gt;&lt;/i&gt;, a cocktail that involves lemon peel, to our tipsy Jane. One more piece of data: &lt;i&gt;CB &lt;/i&gt;doesn’t know that Jane is so drunk that she wouldn’t be able to peel the lemon without converting the lemon into lemon juice.  &lt;p&gt;So: do we show a cocktail that has lemon peel to a user that has lemons in the ingredient list? &lt;p&gt;Before you answer, let me ask you a different question. A cocktail called &lt;i&gt;Purple Passion&lt;/i&gt; calls for Absolut vodka, but the user has Stoli. Do we show the cocktail?  &lt;p&gt;Another one: a cocktail calls for a raspberry vodka, but the user has an orange-flavored vodka. Do we show the cocktail? What if the user has just “vodka”, and they didn’t specify the type of vodka they have?  &lt;p&gt;Yeah. It gets complicated, doesn’t it.  &lt;p&gt;For the first two questions (lemon peel vs lemon, Absolut vs Stoli), I’m guessing you answered “yes”, show the cocktail, let ‘em have some booze. For the last one (raspberry vodka vs orange vodka), you probably said “no, it will taste different”.  &lt;p&gt;Crap. And I had such a good theory here. Now I’m totally gonna get sued, cause I don’t know how to build this :-) &lt;p&gt;Fine-fine, no more theatre, let’s talk about solutions.  &lt;p&gt;&lt;b&gt;Option 1&lt;/b&gt;: Define “homogeneous groups”: all cocktails that contain &lt;i&gt;any &lt;/i&gt;light rum will show up if the user has &lt;i&gt;any other&lt;/i&gt; light rum. That is, all members of the group are considered “synonyms” for cocktail-making purposes.  &lt;p&gt;&lt;b&gt;Option 2: &lt;/b&gt;Define a hierarchy of ingredients that looks something like the following: &lt;p&gt;&lt;img height=&quot;142&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/WhichCocktailstoShow_43B3/clip_image002.gif&quot; width=&quot;372&quot; border=&quot;0&quot;&gt; &lt;p&gt;If we let the user enter any one of the LEAF nodes as one of their ingredients (i.e. Stoli Orange, but not Orange Vodka), cocktail matches will become relatively simple. If a user holds a particular node, show all cocktails that include that node’s siblings or ancestors. For example, if the user has Stoli Orange, we’ll show recipes that include the following highlighted nodes:  &lt;p&gt;&lt;img height=&quot;142&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/WhichCocktailstoShow_43B3/clip_image004.gif&quot; width=&quot;372&quot; border=&quot;0&quot;&gt; &lt;p&gt;However, this may become a bit constraining:  &lt;p&gt;1) Users can’t just type in that they have some vodka in the bar. They gotta say exactly what kind it is. Make users suffer (i.e. stand up from their computer), and they will leave. &lt;br&gt;2) What do you do with recipes that call for stuff that doesn’t quite fit in one category, for example, what if a recipe says “use raspberry or orange vodka” (bear with me, I can’t imagine what the consequences of this substitution would be)? Our hierarchy would then have to become sort-of unclean, involving “virtual” nodes; these virtual nodes break the lovely tree structure, converting the whole thing into a directed graph:  &lt;p&gt;&lt;img height=&quot;153&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/WhichCocktailstoShow_43B3/clip_image006.jpg&quot; width=&quot;376&quot; border=&quot;0&quot;&gt; &lt;p&gt;Uhh, decisions, decisions… What’s your take? &lt;br&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/2343.html</comments>
  <category>design decisions</category>
  <lj:security>public</lj:security>
  <lj:reply-count>8</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/2181.html</guid>
  <pubDate>Sat, 06 Jan 2007 05:50:09 GMT</pubDate>
  <title>Give me your ingredients..</title>
  <link>http://cocktailbuilder.livejournal.com/2181.html</link>
  <description>&lt;p&gt;All right, we know what we want to build: a system that tells you the cocktails that you can make from what you got in your bar. I won’t get tired repeating this mantra until the website comes live, or I come dead, whichever comes first :-).&lt;/p&gt; &lt;p&gt;So, the first task: we need to find out what the user has in their bar. How do we do that?  &lt;p&gt;The problem, really, is another incarnation of a very well-known issue: how do we let the user select several items from a predefined list? One major tweak: the list is &lt;i&gt;freaking huge. &lt;/i&gt;Gi-normous. You know how many possible types of vodkas are out there? My database currently has 60, and that’s &lt;i&gt;just vodkas!&lt;/i&gt; Altogether, we’re looking at something like 500 valid entries. &lt;p&gt;But there are also invalid entries! What if one of our users thinks that Chartreuse is spelled with an SH instead of CH? &lt;p&gt;So, taking all of these requirements into account, let’s look into possible solutions.  &lt;p&gt;&lt;b&gt;Option 1 &lt;/b&gt;&lt;br&gt;The obvious solution: drop-down list box, also known as an HTML &amp;lt;&lt;i&gt;SELECT&amp;gt;&lt;/i&gt; tag. Looks like this: &lt;p&gt;&lt;img height=&quot;96&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Givemeyouringredients_132E6/clip_image002.jpg&quot; width=&quot;141&quot;&gt; &lt;p&gt;I’ll allow myself to remind my faithful readers that drop-down list boxes are generally known to be good with average-sized lists (for example, they are good for a list of states). However, they have quite a few limitations: &lt;p&gt;1) You need several dropdown controls to let the user pick several items. Each dropdown needs to have quite a bit of HTML associated with it – each entry needs an &amp;lt;OPTION&amp;gt; tag.&lt;br&gt;2) It’s pretty hard to navigate the dropdown that has more than, say, 100 items, by using a mouse: the little scroll bar is not quite usable: it’s too small.&lt;br&gt;3) Navigating entries using the keyboard is hard, too: up until Internet Explorer 7, typing “ora” with the keyboard focus on the above dropdown would take you to an entry that starts with “a”, instead of the desired orange juice. &lt;br&gt;4) Think of “Stoli Raspberry” vodka. Some users may refer to it as “Raspberry Stoli”, others – as “Stoli Raspberry”. Does this mean that we need two different items for the same thing in our dropdown?  &lt;p&gt;&lt;b&gt;Option 2&lt;/b&gt;&lt;br&gt;Multiple-selection list box, or just a regular list box with multi-select enabled. These two look like this:  &lt;p&gt;&lt;img height=&quot;99&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Givemeyouringredients_132E6/clip_image004.jpg&quot; width=&quot;142&quot;&gt; &lt;img height=&quot;87&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Givemeyouringredients_132E6/clip_image006.jpg&quot; width=&quot;148&quot;&gt; &lt;p&gt;Other cocktail sites that I looked through (for example, &lt;a href=&quot;http://www.idrink.com/drinkrecipes.html&quot; rel=&quot;nofollow&quot;&gt;idrink.com&lt;/a&gt;, or &lt;a href=&quot;http://cocktail.uk.com&quot; rel=&quot;nofollow&quot;&gt;cocktail.uk.com&lt;/a&gt;), use this kind of user interface.  &lt;p&gt;You know that option 1 stinks – but there’s someone out there, at least cocktail.uk.com, that doesn’t understand that this option is almost just as bad. And that exact thing is one of the reasons why I feel obligated to make the life of all casual alcoholics better by creating the &lt;a href=&quot;http://www.cocktailbuilder.com/&quot; rel=&quot;nofollow&quot;&gt;cocktail builder&lt;/a&gt; website :-). &lt;p&gt;Now, why is this option good from afar, but far from good? &lt;p&gt;1) If you’re using a list box with multiple-selection enabled: do you really think people know that CTRL-clicking will select several items? Plus, when you’re doing this CTRL-clicking voodoo, you’ll inevitably click outside of the control, which will lose all items you worked so hard on. &lt;br&gt;2) For the multiple checkboxes: a much better user model, but hey, I still need to scroll through an immensely long list, and there’s no good way for me to quickly go to an item (besides using the browser’s CTRL+F, but how is this better than just a text box for entry?)&lt;br&gt;3) Still no good answer for “Raspberry Stoli” vs “Stoli Raspberry” &lt;p&gt;&lt;b&gt;&lt;/b&gt; &lt;p&gt;&lt;b&gt;Option 3&lt;br&gt;&lt;/b&gt;The holy grail of Web 2.0, Google AutoSuggest. Looks like this:  &lt;p&gt;&lt;img height=&quot;198&quot; src=&quot;http://nrl.cs.ucla.edu/cocktailbuilder/Givemeyouringredients_132E6/clip_image008.jpg&quot; width=&quot;107&quot; border=&quot;0&quot;&gt; &lt;p&gt;You’re probably expecting me to start screaming about how freaking cool this is, but I’ll let you decide this yourself :-). One of my favorite sayings goes: “Diplomacy is an art of letting others have it your&lt;i&gt; &lt;/i&gt;way&lt;i&gt;”. &lt;/i&gt; &lt;p&gt;Why is this cool? &lt;p&gt;1) Super-fast entry: type in “vodka”, and see all vodkas out there. Even if vodka is not the first word in the ingredient name, you can still show that option in your auto-suggest box (because you’re the one controlling the logic for what to show)&lt;br&gt;2) Almost no issues with scalability and real estate on the screen: it doesn’t matter how many items the user has to choose from; they only see options that fit the context of what they typed in. &lt;br&gt;3) Keyboard navigation: unlike previous options, you can enable up/down arrow navigation through the list of choices, offering enter/tab to actually pick an option. This makes the site much more accessible to people who need it. Recall Joel Spolsky, and design for users that can’t use do precise mouse movements, that don’t have time, that have their cats chewing the mouse cord and babies screaming in the room nearby. Users of my website will very likely be pretty drunk :)&lt;br&gt;4) Multiple entries require multiple auto-complete text boxes, but c’est-la-vie.  &lt;p&gt;There are a few options as to how to implement this auto-complete text box – one big design decision is “get the list live” or “download the static list”. &lt;p&gt;The Internet, including Google Suggest in particular, is full of examples as to how to implement a “live lookup” auto-complete. For example, user types “vod”, and your program goes back to the server, asks what are the matches for “vod”, and then returns the results to the browser for display. Not a bad idea if you have many, many items to search through. &lt;p&gt;However, in our case, the list is reasonably small (something like 500 items?), so maybe we can store the entire list on the client? Maybe, as the user downloads the initial webpage, we’ll also pump in all possible options of ingredients they might enter, and then as they type in “vod”, we’ll just go through 500 items on the client side, in JavaScript?  &lt;p&gt;This second approach is what I chose for cocktail builder; there is one big tradeoff here: initial download time of the 500 item list vs on-demand, lazy download of the results. Several outcomes of this tradeoff: &lt;p&gt;- the price of downloading the list is paid only once per user (remember, your stuff gets cached by the browser); no matter how many ingredients they add – there will be no roundtrips. The users will, I betcha, have 5-10 ingredients in their bar, which will translate to 5-10 queries with the first approach, and only one payoff with the second approach.&lt;br&gt;- users that come back won’t have the roundtrips.&lt;br&gt;- server load: less roundtrips means that I don’t need to buy a 10-rack super-powerful server :-). Which means no annoying ads for the users to cover the costs. &lt;br&gt;- showing matches for user input: clear performance win. As soon as the users types in “vod”, I do snappy JavaScript processing, all on the client side, to run through an ordered list of ingredients (how long does it take a 3GHz computer to do 500 string matches?). If I went with a lazy approach, users would have to wait for a roundtrip, which takes time.  &lt;p&gt;What exactly is the cost of downloading the full ingredient list at load time? My current list of ingredients has 250 items; it’s 13KB, when not compressed (we’ll talk about JavaScript compression at a different time). You can make the judgment yourself, I’m sure.  &lt;p&gt;Cheers!&lt;br&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/2181.html</comments>
  <category>design decisions</category>
  <lj:security>public</lj:security>
  <lj:reply-count>0</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/1977.html</guid>
  <pubDate>Sat, 06 Jan 2007 05:21:50 GMT</pubDate>
  <title>Who is this?</title>
  <link>http://cocktailbuilder.livejournal.com/1977.html</link>
  <description>&lt;p&gt;You’re probably wondering: who in the world is doing this super-altruistic free cocktail site thing, hoping to make millions?&lt;/p&gt; &lt;p&gt;You’d be wrong on just one count: the millions. You’d be almost right on the altruistic part.  &lt;p&gt;Let’s go slowly: what do you already know about me from the last post? I have a girlfriend; this doesn’t, however, imply that I’m a guy or a girl; you know that my girlfriend takes showers (important piece of quality of life!); you can guess that&lt;i&gt; &lt;/i&gt;I like probably cocktails, and that the narration is happening somewhere in Seattle. You also know that I – or someone I know – or something – built some website.  &lt;p&gt;Not enough for your voyeuristic pleasure. Well, I admit, it wouldn’t be enough for mine, either. &lt;p&gt;&lt;i&gt;I’m just a girl… &lt;/i&gt; &lt;p&gt;Sorry, can’t get that stupid song out of my head now. &lt;p&gt;I’m not a girl, I’m a guy. I’m reasonably young, reasonably geeky, reasonably fashionable. Seattle-ite. Live in downtown.  &lt;p&gt;Love coding. I don’t write code for living, just for pleasure.  &lt;p&gt;Love making people happy. Biggest fault I’m willing to admit: watching someone use the system I built. &lt;p&gt;Love creating usable systems. Love &lt;a href=&quot;http://www.joelonsoftware.com&quot; rel=&quot;nofollow&quot;&gt;Joel Spolsky&lt;/a&gt;. Love PHP and MySQL. &lt;p&gt;Would &lt;i&gt;love&lt;/i&gt; to create a cocktails website that will make you happy with your bar. Tell me what you’d like in it, and we’ll discuss in comments.  &lt;p&gt;Cheers, &lt;br&gt;cb&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/1977.html</comments>
  <category>introductions</category>
  <lj:security>public</lj:security>
  <lj:reply-count>0</lj:reply-count>
</item>
<item>
  <guid isPermaLink='true'>http://cocktailbuilder.livejournal.com/1286.html</guid>
  <pubDate>Sat, 06 Jan 2007 03:26:26 GMT</pubDate>
  <title>Well, HI!</title>
  <link>http://cocktailbuilder.livejournal.com/1286.html</link>
  <description>&lt;p&gt;Hello there! I proclaim the blog of the cocktail builder officially open. &lt;/p&gt; &lt;p&gt;“Cocktail what?”, you might say, and you’d be right. It’s a brand new project that you most likely never heard of; this post aims to shed some light on it.  &lt;p&gt;In one sentence: website that helps you find out what cocktails you can make from the ingredients you have in your bar.  &lt;p&gt;Imagine: you come home from class; there was a party at your house yesterday. Damn, I’d want a drink too. But the only thing you got in your bar is tiny bit of vodka, some champagne, and leftovers of a couple of juices in the fridge – pineapple and cranberry… You’re wondering whether mixing all that stuff together will make a delicious drink of gods or a disgusting bleah… Nope, you’re not wondering. You know where to go: &lt;a href=&quot;http://www.cocktailbuilder.com&quot; rel=&quot;nofollow&quot;&gt;www.cocktailbuilder.com&lt;/a&gt;, where you type up the stuff you got and find the stuff you can make – &lt;a href=&quot;http://www.cocktailbuilder.com/pucks_potion.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Puck’s Potion&lt;/a&gt; and &lt;a href=&quot;http://www.cocktailbuilder.com/bay_breeze.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt;Bay Breeze&lt;/a&gt;, each with detailed instructions and user reviews. Now your quickly put-together cocktail is much more likely to impress you – and your possible guest.  &lt;p&gt;You may ask – damn, why do I need yet another cocktail site? There’s the &lt;a href=&quot;http://www.cocktail.com&quot; rel=&quot;nofollow&quot;&gt;cocktail.com&lt;/a&gt;, and &lt;a href=&quot;http://www.webtender.com&quot; rel=&quot;nofollow&quot;&gt;webtender&lt;/a&gt;, and a bunch of others… I thought so too. But then I changed my mind; you will too, I’m sure, but first – here’s a story.  &lt;p&gt;One rainy Seattle morning, I was brushing my teeth, and my girlfriend was taking a shower. I hear a earth-shattering question from behind the shower curtain: “hey, we have a party tomorrow; do you know what cocktails we’ll be serving”? I - the procrastinator of all times - of course say yes, but we both knew I didn’t. So she asks: “Hey, is there a website where I can put in the stuff we have in our bar, and find out what we can make”?  &lt;p&gt;The answer, at that time, was no. Soon, it will be “yes”.  &lt;p&gt;Now back to your original question – why not &lt;a href=&quot;http://www.cocktail.com&quot; rel=&quot;nofollow&quot;&gt;cocktail.com&lt;/a&gt; and others? Because all existing sites will help you make a drink if you know it by name, but are notoriously useless if you’re like me – want to experiment, or just want to use your bar to its fullest, and don’t feel like throwing money around for a special type of raspberry-based orange-flavored vodka that will only help you make one fancy drink.  &lt;p&gt;Why am I starting a blog? I have a few reasons – first off, I love to write. Second, I want to hear the feedback of enthusiasts, readers like yourself, - so that the website really helps you with &lt;i&gt;your &lt;/i&gt;bar. Lastly, I want to promote the cocktail builder project – which, of course, will be completely free (with a bare minimum of ads), and won’t require any registration.  &lt;p&gt;Cheers!&lt;br&gt;Alex Weinstein, aka cb (cocktail builder)&lt;/p&gt;</description>
  <comments>http://cocktailbuilder.livejournal.com/1286.html</comments>
  <category>introductions</category>
  <lj:security>public</lj:security>
  <lj:reply-count>14</lj:reply-count>
</item>
</channel>
</rss>
