tclhttpd session templates

This code implements a .stml file type, permitting per-session template expansion. CMcC 20040929

This facility now depends upon the tclhttpd Generic Caching module.

Note: there are some minor bugs in session.tcl in the released version of tclhttpd - to get the most out of this, you'd be well advised to get the HEAD from CVS. Also, it's written to tcl8.5, not 8.4 ... if you care, the backport is trivial.

Update: Adopted tclhttpd Generic Caching code CMcC 20040929


Within a session template, you get a few built-in commands:

save - saves all variables and arrays within session

exit - utterly destroys the session and any persistent data

dynamic - makes this page's generation dynamic (default)

cache - causes this page to be cached.

value - get session variable value (values pertaining to session management)

group - get value pertaining to this session 'group'

session - get session id

sequence - get session sequence (for strictly sequenced sessions)

You also get some arrays (these don't persist):

data - per-socket data for this transaction

page - some page related data

query - query data encoded in URL

Additionally, any arrays or variables you create in the .stml code will persist if you have [save]d the session.


custom/stml.tcl:

# stml.tcl
#
#        Per-session interpreter template substitution for tclhttpd
#
# CMcC 20040929 - Changed to make use of generic Cache module

DirList_IndexFile index.{stml,tml,html,shtml,thtml,htm,subst}

# Files with a .stml suffix or .sstml are templated and cached as with .tml files

# Template evaluation occurs within a cookie-session (a safe cookie session, for .ssmtl)
# Cookie session state can persist in file by calling Session_Save

Mtype_Add .stml application/x-tcl-session
Mtype_Add .sstml application/x-tcl-safe-session

proc Doc_application/x-tcl-safe-session {path suffix sock} {
    return [Doc_application/x-tcl-session $path $suffix $sock 1]
}

proc Doc_application/x-tcl-session {path suffix sock {safe 0}} {
    upvar #0 Httpd$sock data

    # return a cached version, if possible
    if {[Cache_Fetch $path]} {
        return
    }

    # fetch query
    array set query [list session new]        ;# session id to create a new session
    array set query [Url_DecodeQueryOnly $data(query)]

    # decode session type from file names
    # files like x.type.stml will create a session of type, default 'stml'
    set type [string trimleft [file extension [file rootname $suffix]] .]
    if {$type == ""} {
        set type stml
    }

    # create or restore a cookie session
    set session_error ""
    set session [Session_Cookie [array get query session*] $type session_error $safe]
    if {[string match Session:* $session_error]} {
        # somehow our cookie and our session are out of whack
        Session_Destroy $session        ;# forget the session
        Session_CookieDestroy $type        ;# destroy the session cookies
        Redirect_To $data(uri)        ;# reload to get the new cookies
        return        ;# redundant - Redirect_To throws an error
    }
    set query(session) $session

    # now we have a viable session with an interpreter
    upvar #0 Session:$session state
    set interp $state(interp)
    if {[interp alias $interp exit] == ""} {
        # create an 'exit' command
        interp alias $interp exit {} Session_Destroy $session
    }
    if {[interp alias $interp save] == ""} {
        # create a 'save' command
        interp alias $interp save {} Session_Save $session
    }
    interp alias $interp cache $interp set page(dynamic) 0
    interp alias $interp dynamic $interp set page(dynamic) 1

    # generate interpreter script to source .tml files from the root downward.
    global Template
    set libs [Doc_GetPath $sock $path]
    foreach libdir $libs {
        set libfile [file join $libdir $Template(tmlExt)]
        if {[file exists $libfile]} {
            append script "source $libfile" \n
        }
    }

    # generate interpreter initialization script
    append script [subst {
        catch {unset page}
        catch {unset query}
        catch {unset data}
    
        array set page {
            url $data(url)
            dynamic 1
            directory [file join {*}$libs]
        }
    
        array set query [list [array get query]]
        array set data [list [array get data]]
    }]

    set cleanup {
        catch {unset page}
        catch {unset query}
        catch {unset data}
    }

    # initialize the interpreter
    set code [catch {interp eval $interp $script} content eo]

    # Process the template itself
    if {!$code} {
        set code [catch {Subst_File $path $interp} content eo]
    }

    # Save return cookies, if any
    Cookie_Save $sock

    # process errors now
    if {$code} {
        # delete the per-page session data
        interp eval $interp $cleanup

        # pass errors up - specifically Redirect return code
        return -options $eo $content
    }

    # calculate mime type of return
    if {[interp eval $interp {info exists data(contentType)}]} {
        set ctype [interp eval $interp {set data(contentType)}] ;# set by a template
    } else {
        set ctype text/html
    }

    # delete the per-page session data
    interp eval $interp $cleanup

    # Cache the result
    if {![interp eval $interp {set page(dynamic)}]} {
        # this page is cacheable
        Cache_Store $sock $path $content $ctype
    } else {
        # return the result - filters will be applied en route
        return [Httpd_ReturnData $sock $ctype $content]
    }
}

Here's a test file: test.stml

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!-- [package require html] -->
<html>
  <head>
    <title>Test session templates</title>
  </head>

  <body>
    <h1>Test session templates</h1>
    <h2>Session: [session]</h2>
    <p>type: [value type]</p>
    <p>interp:[value interp]</p>
    <p>start: [value start]</p>
    <p>current: [value current]</p>
    <p>count: [value count]</p>
    <p>[::html::tableFromArray data]</p>
    <p>[::html::tableFromArray query]</p>
    <p>[::html::tableFromArray page]</p>

    <hr>
    <address><a href="mailto:colin at sharedtech dot dyndns dot org">Colin McCormack</a></address>
<!-- Created: Thu Sep 16 08:53:25 EST 2004 -->
<!-- hhmts start -->
Last modified: Thu Sep 16 09:14:32 EST 2004
<!-- hhmts end -->
  </body>
</html>

Colin, I NB could not get this test page to work, using a tclkit85a4, and tclhttpd from cvs. I loaded generic caching and stml w/o errors. When I tried the above example, in the root of my site, I got errors about not finding mypage, faq etc. I removed these from the main .tml file and got a error 'bout not finding the html package...Seems that the slave interp is not aware of the packages that the main interp uses ...Maybe?