Wibble examples

AMG: This page contains example pages and applications for the Wibble web server. There may be some overlap with the Wibble zone handlers page. If all you have is a zone handler, put it in the zone handlers page. If you are explaining it in the context of a complete application, put it here. If you're showing off a useful modification of Wibble, maybe put that here too. And if you have a sample page or template, that's also welcome here.

Fetching backrefs...

Hello world

AMG: Put this in a file called index.html.tmpl and see what happens.

% dict set response header content-type text/html
<html><head><title>Hello from Wibble! - $uri</title></head><body>
% set rand [expr {rand()}]
% if {$rand > 0.5} {
random=[format %.3f $rand] &gt; 0.5<br/>
% } else {
random=[format %.3f $rand] &lt;= 0.5<br/>
% }
time/date=[clock format [clock seconds]]<br/>
milliseconds=[clock milliseconds]<br/>
clicks=[clock clicks]<br/>
% if {[info exists query] && ![dict exists $query noiframe]} {
<iframe src="?noiframe" width="100%"/>
% }

AMG: Thanks JBLZ for the bug fix. I had to make a similar one to [dirslash]; I don't think I've posted it yet.

Image file upload, cookies, sessions

AMG: Step 1. Make a directory, then create a file called wibble.tcl in that directory. Paste the Wibble implementation into the file. For this example, I used this version: [L1 ]. (Later versions may be incompatible, and I may forget to update this example!)

Step 2. Create a file in the same directory called index.html.tmpl with the following contents:

% dict set response header content-type text/html
% if {[dict exists $header cookie sessionid ""]} {
%   set sessionid [dict get $header cookie sessionid ""]
%   upvar #0 sessions($sessionid) session
% }
% set now [clock seconds]
% if {![info exists session]} {
%   set sessionid [format %llX [string reverse\
%       [string range [expr rand()] 2 end]]]
%   upvar #0 sessions($sessionid) session
%   set timeout 60
%   dict set response header set-cookie\
%       sessionid=$sessionid\;Max-Age=$timeout\;Version=1
%   dict set session expiration [clock add $now $timeout seconds]
%   after [expr {$timeout * 1000}] [list apply {{sessionid} {
%     unset -nocomplain ::sessions($sessionid)
%   }} $sessionid]
% }
% if {[dict exists $post imagetitle ""]
%  && [dict exists $post imagedata ""]
%  && [dict exists $post imagedata content-disposition filename]
%  && [dict exists $post imagedata content-type ""]
%  && [string length [dict get $post imagedata ""]]} {
%   set imagetitle [dict get $post imagetitle ""]
%   set imagefilename [dict get $post imagedata content-disposition filename]
%   set imagetype [dict get $post imagedata content-type ""]
%   dict set session imagetitle $imagetitle
%   dict set session imagedata [dict get $post imagedata ""]
%   dict set session imagefilename $imagefilename
%   dict set session imagetype $imagetype
% } elseif {[dict exists $session imagetitle]} {
%   set imagetitle [dict get $session imagetitle]
% } else {
%   set imagetitle "My Image"
% }
<html><head><title>Image file upload test</title></head><body>
<form method="post" enctype="multipart/form-data">
  <table border="1"><tr><th>
    Session ID
    [enhtml $sessionid]
% set expiration [dict get $session expiration]
    [enhtml [clock format $expiration]], [expr {$expiration - $now}]s left
% foreach id [lsort [array names ::sessions]] {
%   set expiration [dict get $::sessions($id) expiration]
      [enhtml $id]
      [enhtml [clock format $expiration]], [expr {$expiration - $now}]s left
% }
    Image title
    <input type="text" name="imagetitle" value="[enattr $imagetitle]" />
    Image file
    <input type="file" name="imagedata" />
    <input type="submit" value="Upload Image" />
% if {[dict exists $session imagedata]} {
    File name
    [enhtml [dict get $session imagefilename]]
    [enhtml [dict get $session imagetype]]
    <img src="image" />
% }

Step 3. Create another file in the same directory called image.script with the following contents:

if {[dict exists $header cookie sessionid ""]} {
  upvar #0 sessions([dict get $header cookie sessionid ""]) session

if {[info exists session] && [dict exists $session imagetype]
 && [dict exists $session imagedata]} {
  dict set response header content-type [dict get $session imagetype]
  dict set response content [dict get $session imagedata]
} else {
  dict set response status 404

Step 4. Run wibble.tcl using tclsh, wish, tclkit, or whatever you prefer. You're now up and running.

Step 5. Point your Web browser to http://localhost:8080/ , and upload images. Access the site from multiple browsers on multiple computers. If you have Firecookie [L2 ] or similar, try messing with your cookies. Edit the files you created. Look at the index.html.script file that Wibble generated from index.html.tmpl. Have fun.


AMG: AJAX long polling can be done in Wibble. Here, I'll show you. Make a file called delay.html and put it in your docroot:

<html><head><script type="text/javascript">
  function delay() {
    if (window.XMLHttpRequest) {
      xmlhttp = new XMLHttpRequest();
    } else {
      xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
        document.getElementById("result").innerHTML = xmlhttp.responseText;
    document.getElementById("result").innerHTML = "Waiting...";
    time = document.getElementById("time").value;
    xmlhttp.open("GET", "delay?time=" + time, true);
  <p>How long shall I wait?</p>
  <p><input type="text" id="time" /> milliseconds</p>
  <p><button type="button" onclick="delay()">Do It!</button></p>
  <div id="result" />

And another called delay.script:

after [dict get $query time ""] [resume timer]
set start [clock milliseconds]
suspend timer
set finish [clock milliseconds]
dict set response header content-type text/plain
dict set response content "I waited for [expr {$finish - $start}] milliseconds."

Go to http://localhost:8080/delay.html , type a number in the box, click the button, and wait. ;^)

dzach 2011-3-1: Looks like something is missing. I'm getting an error invalid command name "resume" while executing "resume timer".

AMG: Oops, you're right. I made a bunch of changes to Wibble since writing this example, which is now out of date, and I forgot to update it. Try again using this version: [L3 ]. It's the second-to-most recent version. This change log entry [L4 ] shows what all has been done since then.

Part of the reason I haven't updated the examples is that I'm not sure I have ICC quite right. When I get some time (who knows when that will be...), I'll finish the large example AJAX application I've been working on, quite possibly redoing ICC in the process. You see, I can't know that I've chosen the right set of functionality until I've tried using it. Anyway, at that time I'll update the other examples too.

dzach: Thanks for the reply, it works now.

Example form

AMG: By JM's request, here's a template file that serves a form that simply echoes its own data back. It supports both GET and POST, and for POST it supports application/x-www-form-urlencoded, multipart/form-data, and text/plain. It allows file upload, but this only works fully for multipart/form-data POST (otherwise, the browser only sends the filename). This example also demonstrates how to deal with multiple form elements having the same name. You can change the method and enctype right in the form, though the new values won't take effect until the page reloads after hitting Submit. To run this example, just put in a file called "echo.html.tmpl" located inside your docroot. Then point your browser to http://localhost:8080/echo.html or wherever you put it.

%;# Declare the content type.
% dict set state response header content-type "" text/html
%;# Just return the value.
% proc x {val} {return $val}
%;# Set some defaults.
% set method POST
% set enctype multipart/form-data
% set numrows 5
% set rowdata {}
%;# Import data from query string and post data.
% foreach source {query post} {
%;  # Check if there is any query or post data.
%   if {[info exists $source]} {
%;    # Get and validate the method.
%     if {[dict exists [set $source] method ""]
%      && [dict get [set $source] method ""] in {GET POST}} {
%       set method [dict get [set $source] method ""]
%     }
%;    # Get and validate the enctype.
%     if {[dict exists [set $source] enctype ""]
%      && [dict get [set $source] enctype ""] in {
%         application/x-www-form-urlencoded multipart/form-data text/plain}} {
%       set enctype [dict get [set $source] enctype ""]
%     }
%;    # Get and validate the number of rows.
%     if {[dict exists [set $source] numrows ""]
%      && [regexp {^\d+$} [dict get [set $source] numrows ""]]
%      && [dict get [set $source] numrows ""] < 1000} {
%       set numrows [dict get [set $source] numrows ""]
%     }
%;    # Get rowdata.  This demonstrates how to access multiple keys all with
%;    # the same name.  Yes, it's less efficient than when everything has a
%;    # unique name, but at least it works.
%     foreach {key val} [set $source] {
%       if {$key eq "rowdata" && [dict exists $val ""]} {
%         lappend rowdata [dict get $val ""]
%       }
%     }
%;    # Get uploaded file dictionary.
%     if {[dict exists [set $source] file]} {
%       set file [dict get [set $source] file]
%     }
%   }
% }
%;# Construct the form attributes.
% set formattrs "method=\"[enattr $method]\""
% if {$method eq "POST"} {
%   append formattrs " enctype=\"[enattr $enctype]\""
% }
  <form $formattrs accept-charset="iso-8859-1">
%;# Method selector.
% foreach option {GET POST} {
    <input type="radio" name="method" value="[enattr $option]"\
      [if {$method eq $option} {x {checked="checked"}}]>[enhtml $option]</input>
% }
    <br />

%;# Enctype selector.
    POST enctype:\
    <select name="enctype">
% foreach option {
%   application/x-www-form-urlencoded multipart/form-data text/plain
% } {
      <option [if {$enctype eq $option} {x {selected="selected"}}]\
        value="[enattr $option]">[enhtml $option]</option>
% }
    <br />

%;# Row count selector.
    Number of rows:\
    <input type="text" name="numrows" value="[enattr $numrows]" />\
    <br />

%;# Row data.
% for {set i 0} {$i < $numrows} {incr i} {
    Row #[enhtml [expr {$i + 1}]]: <input type="text" name="rowdata"
%   if {$i < [llength $rowdata]} {
      value="[enattr [lindex $rowdata $i]]"
%   }
    /><br />
% }
%;# File upload selector.
    File upload:\
    <input type="file" name="file" />\
    <br />

%;# Submit button.
    <input type="submit" value="Submit" />

%;# Uploaded file information.
% if {[info exists file]} {
    <tr><th colspan="3">Uploaded file</th></tr>
%   dict for {key val} $file {
%     if {$key eq ""} {
    <tr><td colspan="3"><pre>[enpre $val]</pre></td></tr>
%     } else {
%       dict for {key2 val2} $val {
    <tr><td>[enhtml $key]</td><td>[enhtml $key2]</td>
    <td><tt>[enhtml $val2]</tt></td></tr>
%       }
%     }
%   }
% }

JM 26 May 2012 - the example works fine under IE, however, I had to change the first line to make it work under chrome and safari...

 %;# Declare the content type.
 %   dict set       response header content-type    text/html
 %;# dict set state response header content-type "" text/html

AMG: I just tested this form in Firefox 12.0, MSIE 8, Safari 5.1.7, and Chrome 19.0.1084.52m, and I had no problems. My test was simply POST with five rows, set to a, b, c, d, e, respectively, and with one of the Wibble example files as the upload.

Your change should have the same effect as deleting the first line altogether. What you're doing is putting the content-type data into a variable called $response, not $state. $response isn't used anywhere in the template file (above) or [::wibble::template] or [::wibble::zone::template], so it will be ignored, the same as if the line didn't exist in the first place.

Are you sure you're using the latest version of Wibble? At this moment, that would be version 0.4 [L5 ]. If I recall correctly, older versions of Wibble did use the $response variable.

If you're using the latest Wibble, this indicates a problem with how the browser is processing the content-type. If the server doesn't sent content-type, the browser gets to decide for itself how the page should be interpreted. (MSIE usually does this anyway, content-type or no.) However, I could not reproduce this problem. Could you give me more information on what works and what doesn't? Also try removing (or commenting out) the first line (the one you changed), and confirm that it behaves the same both with the change you describe above and with the line removed.

JM yes, I was using an older version of Wibble. For some reason, the example did not break with IE. Then I believed there was a typo on such first line, which I now learned I was wrong. Also, now that I have the latest version of wibble, I can confirm the example works as originally posted on different browsers. btw, the symptom was that the html was not rendered, the browser just shows the full html markup.
Now (I think) all these comments could be deleted if you agree. One Question: as the dictionary is called "state", then, I was expecting pairs of key values after that...response with header, content-type with "" and then text/html would be with no value?

AMG: [dict set] accepts a dictionary "path" followed by a single value. [dict set] only sets one value at a time. If you want to set more than one, e.g. as you suggest (alternating key/value pairs), use [dict merge] or a custom command that calls [dict set] in a loop.

Wibble uses a hierarchy of nested dictionaries. Use the vars zone handler to help see how this works. Here's an example showing part of the hierarchy:

key                             -> value
-------------------------------    --------------
  |-- request
  |     |-- socket              -> sock264
  |     |-- header
  |     |     `-- host          -> localhost:8080
  |     `-- accept
  |           `-- encoding      -> gzip deflate
  |-- response
  |     `-- header
  |           `-- content-type
  |                 |-- ""      -> text/html
  |                 `-- charset -> utf-8
  `-- options
        `-- prefix              -> / 

For header elements that have both a value and attributes, the value's key is "", which is specially chosen because no attribute is named empty string. content-type is one such header element.

JM Thanks!

aspect 2014-02: in reference to JM's issues above, some of the examples here invoke dict set response and others dict set state response -- it looks like the latter is required for the current version of Wibble implementation, while jcowgar's github mirror is of a version that wants the former. I haven't updated the examples as I'm not sure the API change was clearly intentional -- in my local experiments I've put dict with state at the beginning of template and scriptfile handlers to permit the older form with (some) success.

AMG: Ugh, sorry for my horrendous neglect... looks like I'm going to have to do something to reclaim my life. Oh well, I'm stuck for now, but that doesn't mean I can't take a moment to answer your question!

The API changed to group request and response into a single variable called state. This was done to make room for options and custom data, plus to better organize the simultaneous collection of states called the system. Some of the mirrors and forks out there are based on old versions that don't have this change.