] unit-test\r
\r
[ t ] [ now dup timestamp>rfc822 rfc822>timestamp time- 1 seconds before? ] unit-test\r
+\r
+[ t ] [ now dup timestamp>cookie-string cookie-string>timestamp time- 1 seconds before? ] unit-test\r
+\r
+[ "Sun, 4 May 2008 07:00:00" ] [\r
+ "Sun May 04 07:00:00 2008 GMT" cookie-string>timestamp\r
+ timestamp>string\r
+] unit-test\r
-USING: math math.parser kernel sequences io calendar\r
+USING: math math.parser kernel sequences io\r
accessors arrays io.streams.string splitting\r
-combinators accessors debugger ;\r
+combinators accessors debugger\r
+calendar calendar.format.macros ;\r
IN: calendar.format\r
\r
+: pad-00 number>string 2 CHAR: 0 pad-left ;\r
+\r
+: pad-0000 number>string 4 CHAR: 0 pad-left ;\r
+\r
+: pad-00000 number>string 5 CHAR: 0 pad-left ;\r
+\r
+: write-00 pad-00 write ;\r
+\r
+: write-0000 pad-0000 write ;\r
+\r
+: write-00000 pad-00000 write ;\r
+\r
+: hh hour>> write-00 ;\r
+\r
+: mm minute>> write-00 ;\r
+\r
+: ss second>> >integer write-00 ;\r
+\r
+: D day>> number>string write ;\r
+\r
+: DD day>> write-00 ;\r
+\r
+: DAY day-of-week day-abbreviations3 nth write ;\r
+\r
+: MM month>> write-00 ;\r
+\r
+: MONTH month>> month-abbreviations nth write ;\r
+\r
+: YYYY year>> write-0000 ;\r
+\r
+: YYYYY year>> write-00000 ;\r
+\r
+: expect ( str -- )\r
+ read1 swap member? [ "Parse error" throw ] unless ;\r
+\r
+: read-00 2 read string>number ;\r
+\r
+: read-000 3 read string>number ;\r
+\r
+: read-0000 4 read string>number ;\r
+\r
GENERIC: day. ( obj -- )\r
\r
M: integer day. ( n -- )\r
] with each nl ;\r
\r
M: timestamp month. ( timestamp -- )\r
- { year>> month>> } get-slots 2array month. ;\r
+ [ year>> ] [ month>> ] bi 2array month. ;\r
\r
GENERIC: year. ( obj -- )\r
\r
M: timestamp year. ( timestamp -- )\r
year>> year. ;\r
\r
-: pad-00 number>string 2 CHAR: 0 pad-left ;\r
-\r
-: pad-0000 number>string 4 CHAR: 0 pad-left ;\r
-\r
-: write-00 pad-00 write ;\r
-\r
-: write-0000 pad-0000 write ;\r
-\r
: (timestamp>string) ( timestamp -- )\r
- dup day-of-week day-abbreviations3 nth write ", " write\r
- dup day>> number>string write bl\r
- dup month>> month-abbreviations nth write bl\r
- dup year>> number>string write bl\r
- dup hour>> write-00 ":" write\r
- dup minute>> write-00 ":" write\r
- second>> >integer write-00 ;\r
+ { DAY ", " D " " MONTH " " YYYY " " hh ":" mm ":" ss } formatted ;\r
\r
: timestamp>string ( timestamp -- str )\r
[ (timestamp>string) ] with-string-writer ;\r
\r
: (write-gmt-offset) ( duration -- )\r
- [ hour>> write-00 ] [ minute>> write-00 ] bi ;\r
+ [ hh ] [ mm ] bi ;\r
\r
: write-gmt-offset ( gmt-offset -- )\r
dup instant <=> sgn {\r
#! RFC822 timestamp format\r
#! Example: Tue, 15 Nov 1994 08:12:31 +0200\r
[\r
- dup (timestamp>string)\r
- " " write\r
- gmt-offset>> write-gmt-offset\r
+ [ (timestamp>string) " " write ]\r
+ [ gmt-offset>> write-gmt-offset ]\r
+ bi\r
] with-string-writer ;\r
\r
: timestamp>http-string ( timestamp -- str )\r
#! Example: Tue, 15 Nov 1994 08:12:31 GMT\r
>gmt timestamp>rfc822 ;\r
\r
+: (timestamp>cookie-string) ( timestamp -- )\r
+ >gmt\r
+ { DAY ", " DD "-" MONTH "-" YYYY " " hh ":" mm ":" ss " GMT" } formatted ;\r
+\r
+: timestamp>cookie-string ( timestamp -- str )\r
+ [ (timestamp>cookie-string) ] with-string-writer ;\r
+\r
: (write-rfc3339-gmt-offset) ( duration -- )\r
- [ hour>> write-00 CHAR: : write1 ]\r
- [ minute>> write-00 ] bi ;\r
+ [ hh ":" write ] [ mm ] bi ;\r
\r
: write-rfc3339-gmt-offset ( duration -- )\r
dup instant <=> sgn {\r
{ 0 [ drop "Z" write ] }\r
- { -1 [ CHAR: - write1 before (write-rfc3339-gmt-offset) ] }\r
- { 1 [ CHAR: + write1 (write-rfc3339-gmt-offset) ] }\r
+ { -1 [ "-" write before (write-rfc3339-gmt-offset) ] }\r
+ { 1 [ "+" write (write-rfc3339-gmt-offset) ] }\r
} case ;\r
\r
: (timestamp>rfc3339) ( timestamp -- )\r
{\r
- [ year>> number>string write CHAR: - write1 ]\r
- [ month>> write-00 CHAR: - write1 ]\r
- [ day>> write-00 CHAR: T write1 ]\r
- [ hour>> write-00 CHAR: : write1 ]\r
- [ minute>> write-00 CHAR: : write1 ]\r
- [ second>> >fixnum write-00 ]\r
+ YYYY "-" MM "-" DD "T" hh ":" mm ":" ss\r
[ gmt-offset>> write-rfc3339-gmt-offset ]\r
- } cleave ;\r
+ } formatted ;\r
\r
: timestamp>rfc3339 ( timestamp -- str )\r
[ (timestamp>rfc3339) ] with-string-writer ;\r
\r
-: expect ( str -- )\r
- read1 swap member? [ "Parse error" throw ] unless ;\r
-\r
-: read-00 2 read string>number ;\r
-\r
-: read-000 3 read string>number ;\r
-\r
-: read-0000 4 read string>number ;\r
-\r
: signed-gmt-offset ( dt ch -- dt' )\r
{ { CHAR: + [ 1 ] } { CHAR: - [ -1 ] } } case time* ;\r
\r
: rfc3339>timestamp ( str -- timestamp )\r
[ (rfc3339>timestamp) ] with-string-reader ;\r
\r
-ERROR: invalid-rfc822-date ;\r
+ERROR: invalid-timestamp-format ;\r
\r
-: check-rfc822-date ( obj/f -- obj ) [ invalid-rfc822-date ] unless* ;\r
+: check-timestamp ( obj/f -- obj )\r
+ [ invalid-timestamp-format ] unless* ;\r
\r
: read-token ( seps -- token )\r
- [ read-until ] keep member? check-rfc822-date drop ;\r
+ [ read-until ] keep member? check-timestamp drop ;\r
\r
: read-sp ( -- token ) " " read-token ;\r
\r
: checked-number ( str -- n )\r
- string>number check-rfc822-date ;\r
+ string>number check-timestamp ;\r
\r
: parse-rfc822-gmt-offset ( string -- dt )\r
dup "GMT" = [ drop instant ] [\r
\r
: (rfc822>timestamp) ( -- timestamp )\r
timestamp new\r
- "," read-token day-abbreviations3 member? check-rfc822-date drop\r
+ "," read-token day-abbreviations3 member? check-timestamp drop\r
read1 CHAR: \s assert=\r
read-sp checked-number >>day\r
- read-sp month-abbreviations index check-rfc822-date >>month\r
+ read-sp month-abbreviations index check-timestamp >>month\r
read-sp checked-number >>year\r
":" read-token checked-number >>hour\r
":" read-token checked-number >>minute\r
: rfc822>timestamp ( str -- timestamp )\r
[ (rfc822>timestamp) ] with-string-reader ;\r
\r
+: (cookie-string>timestamp-1) ( -- timestamp )\r
+ timestamp new\r
+ "," read-token day-abbreviations3 member? check-timestamp drop\r
+ read1 CHAR: \s assert=\r
+ "-" read-token checked-number >>day\r
+ "-" read-token month-abbreviations index check-timestamp >>month\r
+ read-sp checked-number >>year\r
+ ":" read-token checked-number >>hour\r
+ ":" read-token checked-number >>minute\r
+ " " read-token checked-number >>second\r
+ readln parse-rfc822-gmt-offset >>gmt-offset ;\r
+\r
+: cookie-string>timestamp-1 ( str -- timestamp )\r
+ [ (cookie-string>timestamp-1) ] with-string-reader ;\r
+\r
+: (cookie-string>timestamp-2) ( -- timestamp )\r
+ timestamp new\r
+ read-sp day-abbreviations3 member? check-timestamp drop\r
+ read-sp month-abbreviations index check-timestamp >>month\r
+ read-sp checked-number >>day\r
+ ":" read-token checked-number >>hour\r
+ ":" read-token checked-number >>minute\r
+ " " read-token checked-number >>second\r
+ read-sp checked-number >>year\r
+ readln parse-rfc822-gmt-offset >>gmt-offset ;\r
+\r
+: cookie-string>timestamp-2 ( str -- timestamp )\r
+ [ (cookie-string>timestamp-2) ] with-string-reader ;\r
+\r
+: cookie-string>timestamp ( str -- timestamp )\r
+ {\r
+ [ cookie-string>timestamp-1 ]\r
+ [ cookie-string>timestamp-2 ]\r
+ [ rfc822>timestamp ]\r
+ } attempt-all-quots ;\r
+\r
: (ymdhms>timestamp) ( -- timestamp )\r
read-ymd " " expect read-hms instant <timestamp> ;\r
\r
[ (ymd>timestamp) ] with-string-reader ;\r
\r
: (timestamp>ymd) ( timestamp -- )\r
- dup timestamp-year write-0000\r
- "-" write\r
- dup timestamp-month write-00\r
- "-" write\r
- timestamp-day write-00 ;\r
+ { YYYY "-" MM "-" DD } formatted ;\r
\r
: timestamp>ymd ( timestamp -- str )\r
[ (timestamp>ymd) ] with-string-writer ;\r
\r
: (timestamp>hms)\r
- dup timestamp-hour write-00\r
- ":" write\r
- dup timestamp-minute write-00\r
- ":" write\r
- timestamp-second >integer write-00 ;\r
+ { hh ":" mm ":" ss } formatted ;\r
\r
: timestamp>hms ( timestamp -- str )\r
[ (timestamp>hms) ] with-string-writer ;\r
\r
: timestamp>ymdhms ( timestamp -- str )\r
- >gmt\r
[\r
- dup (timestamp>ymd)\r
- " " write\r
- (timestamp>hms)\r
+ >gmt\r
+ { (timestamp>ymd) " " (timestamp>hms) } formatted\r
] with-string-writer ;\r
\r
: file-time-string ( timestamp -- string )\r
[\r
- [ month>> month-abbreviations nth write ] keep bl\r
- [ day>> number>string 2 32 pad-left write ] keep bl\r
- dup now [ year>> ] bi@ = [\r
- [ hour>> write-00 ] keep ":" write\r
- minute>> write-00\r
- ] [\r
- year>> number>string 5 32 pad-left write\r
- ] if\r
+ {\r
+ MONTH " " DD " "\r
+ [\r
+ dup now [ year>> ] bi@ =\r
+ [ [ hh ":" write ] [ mm ] bi ] [ YYYYY ] if\r
+ ]\r
+ } formatted\r
] with-string-writer ;\r
--- /dev/null
+USING: tools.test kernel ;
+IN: calendar.format.macros
+
+[ 2 ] [ { [ 2 ] } attempt-all-quots ] unit-test
+
+[ 2 ] [ { [ 1 throw ] [ 2 ] } attempt-all-quots ] unit-test
+
+[ { [ 1 throw ] } attempt-all-quots ] [ 1 = ] must-fail-with
+
+: compiled-test-1 { [ 1 throw ] [ 2 ] } attempt-all-quots ;
+
+\ compiled-test-1 must-infer
+
+[ 2 ] [ compiled-test-1 ] unit-test
--- /dev/null
+USING: macros kernel words quotations io sequences combinators
+continuations ;
+IN: calendar.format.macros
+
+MACRO: formatted ( spec -- )
+ [
+ {
+ { [ dup word? ] [ 1quotation ] }
+ { [ dup quotation? ] [ ] }
+ [ [ nip write ] curry [ ] like ]
+ } cond
+ ] map [ cleave ] curry ;
+
+MACRO: attempt-all-quots ( quots -- )
+ dup length 1 = [ first ] [
+ unclip swap
+ [ nip attempt-all-quots ] curry
+ [ recover ] 2curry
+ ] if ;
"extra/http/test" resource-path <static> >>default
"nested" add-responder
<action>
- [ "redirect-loop" f <permanent-redirect> ] >>display
+ [ "redirect-loop" f <standard-redirect> ] >>display
"redirect-loop" add-responder
main-responder set
] { } assoc>map
"&" join ;
-TUPLE: cookie name value path domain expires http-only ;
+TUPLE: cookie name value path domain expires max-age http-only ;
: <cookie> ( value name -- cookie )
cookie new
- swap >>name swap >>value ;
+ swap >>name
+ swap >>value ;
: parse-cookies ( string -- seq )
[
";" split [
[ blank? ] trim "=" split1 swap >lower {
- { "expires" [ >>expires ] }
+ { "expires" [ cookie-string>timestamp >>expires ] }
+ { "max-age" [ string>number seconds ] }
{ "domain" [ >>domain ] }
{ "path" [ >>path ] }
{ "httponly" [ drop t >>http-only ] }
{
{ f [ drop ] }
{ t [ , ] }
- [ "=" swap 3append , ]
+ [
+ {
+ { [ dup timestamp? ] [ timestamp>cookie-string ] }
+ { [ dup duration? ] [ dt>seconds number>string ] }
+ [ ]
+ } cond
+ "=" swap 3append ,
+ ]
} case ;
: unparse-cookie ( cookie -- strings )
"path" over path>> (unparse-cookie)
"domain" over domain>> (unparse-cookie)
"expires" over expires>> (unparse-cookie)
+ "max-age" over max-age>> (unparse-cookie)
"httponly" over http-only>> (unparse-cookie)
drop
] { } make ;
;
[ 25 ] [
+ init-request
action-request-test-1 lf>crlf
[ read-request ] with-string-reader
request set
: validation-failed ( -- * )\r
action get display>> call exit-with ;\r
\r
-M: action call-responder ( path action -- response )\r
+M: action call-responder* ( path action -- response )\r
'[\r
, [ CHAR: / = ] right-trim empty? [\r
, action set\r
- request-params params set\r
- request get method>> {\r
- { "GET" [ handle-get ] }\r
- { "HEAD" [ handle-get ] }\r
- { "POST" [ handle-post ] }\r
- } case\r
+ request get\r
+ [ request-params params set ]\r
+ [\r
+ method>> {\r
+ { "GET" [ handle-get ] }\r
+ { "HEAD" [ handle-get ] }\r
+ { "POST" [ handle-post ] }\r
+ } case\r
+ ] bi\r
] [\r
<404>\r
] if\r
: logged-in? ( request responder -- ? )\r
provider>> swap "authorization" header authorization-ok? ;\r
\r
-M: basic-auth call-responder ( request path responder -- response )\r
+M: basic-auth call-responder* ( request path responder -- response )\r
pick over logged-in?\r
[ call-next-method ] [ 2nip realm>> <401> ] if ;\r
<t:title>Edit Profile</t:title>
- <t:form action="edit-profile">
+ <t:form t:action="edit-profile">
<table>
<tr>
<th class="field-label">User name:</th>
- <td><t:view component="username" /></td>
+ <td><t:view t:component="username" /></td>
</tr>
<tr>
<th class="field-label">Real name:</th>
- <td><t:edit component="realname" /></td>
+ <td><t:edit t:component="realname" /></td>
</tr>
<tr>
<tr>
<th class="field-label">Current password:</th>
- <td><t:edit component="password" /></td>
+ <td><t:edit t:component="password" /></td>
</tr>
<tr>
<tr>
<th class="field-label">New password:</th>
- <td><t:edit component="new-password" /></td>
+ <td><t:edit t:component="new-password" /></td>
</tr>
<tr>
<th class="field-label">Verify:</th>
- <td><t:edit component="verify-password" /></td>
+ <td><t:edit t:component="verify-password" /></td>
</tr>
<tr>
<tr>
<th class="field-label">E-mail:</th>
- <td><t:edit component="email" /></td>
+ <td><t:edit t:component="email" /></td>
</tr>
<tr>
<p>
<input type="submit" value="Update" />
- <t:if var="http.server.auth.login:login-failed?">
+ <t:if t:var="http.server.auth.login:login-failed?">
<t:error>invalid password</t:error>
</t:if>
- <t:if var="http.server.auth.login:password-mismatch?">
+ <t:if t:var="http.server.auth.login:password-mismatch?">
<t:error>passwords do not match</t:error>
</t:if>
</p>
<action>\r
[\r
f logged-in-user sset\r
- "$login/login" f <permanent-redirect>\r
+ "$login/login" end-flow\r
] >>submit ;\r
\r
! ! ! Authentication logic\r
\r
: show-login-page ( -- response )\r
begin-flow\r
- "$login/login" f <temporary-redirect> ;\r
+ "$login/login" f <standard-redirect> ;\r
\r
-M: protected call-responder ( path responder -- response )\r
+M: protected call-responder* ( path responder -- response )\r
logged-in-user sget dup [\r
save-user-after\r
call-next-method\r
] [\r
- 3drop\r
- request get method>> { "GET" "HEAD" } member?\r
- [ show-login-page ] [ <400> ] if\r
+ 3drop show-login-page\r
] if ;\r
\r
-M: login call-responder ( path responder -- response )\r
+M: login call-responder* ( path responder -- response )\r
dup login set\r
call-next-method ;\r
\r
<t:title>Login</t:title>
- <t:form action="login">
+ <t:form t:action="login">
<table>
<tr>
<th class="field-label">User name:</th>
- <td><t:edit component="username" /></td>
+ <td><t:edit t:component="username" /></td>
</tr>
<tr>
<th class="field-label">Password:</th>
- <td><t:edit component="password" /></td>
+ <td><t:edit t:component="password" /></td>
</tr>
</table>
<input type="submit" value="Log in" />
- <t:if var="http.server.auth.login:login-failed?">
+ <t:if t:var="http.server.auth.login:login-failed?">
<t:error>invalid username or password</t:error>
</t:if>
</p>
<p>
<t:if code="http.server.auth.login:login-failed?">
- <t:a href="register">Register</t:a>
+ <t:a t:href="register">Register</t:a>
</t:if>
|
<t:if code="http.server.auth.login:allow-password-recovery?">
- <t:a href="recover-password">Recover Password</t:a>
+ <t:a t:href="recover-password">Recover Password</t:a>
</t:if>
</p>
<p>Enter the username and e-mail address you used to register for this site, and you will receive a link for activating a new password.</p>
- <t:form action="recover-password">
+ <t:form t:action="recover-password">
<table>
<tr>
<th class="field-label">User name:</th>
- <td><t:edit component="username" /></td>
+ <td><t:edit t:component="username" /></td>
</tr>
<tr>
<th class="field-label">E-mail:</th>
- <td><t:edit component="email" /></td>
+ <td><t:edit t:component="email" /></td>
</tr>
<tr>
<th class="field-label">Captcha:</th>
- <td><t:edit component="captcha" /></td>
+ <td><t:edit t:component="captcha" /></td>
</tr>
<tr>
<p>Choose a new password for your account.</p>
- <t:form action="new-password">
+ <t:form t:action="new-password">
<table>
- <t:edit component="username" />
- <t:edit component="ticket" />
+ <t:edit t:component="username" />
+ <t:edit t:component="ticket" />
<tr>
<th class="field-label">Password:</th>
- <td><t:edit component="new-password" /></td>
+ <td><t:edit t:component="new-password" /></td>
</tr>
<tr>
<th class="field-label">Verify password:</th>
- <td><t:edit component="verify-password" /></td>
+ <td><t:edit t:component="verify-password" /></td>
</tr>
<tr>
<p>
<input type="submit" value="Set password" />
- <t:if var="http.server.auth.login:password-mismatch?">
+ <t:if t:var="http.server.auth.login:password-mismatch?">
<t:error>passwords do not match</t:error>
</t:if>
</p>
\r
<t:title>Recover lost password: step 4 of 4</t:title>\r
\r
- <p>Your password has been reset. You may now <t:a href="login">log in</t:a>.</p>\r
+ <p>Your password has been reset. You may now <t:a t:href="login">log in</t:a>.</p>\r
\r
</t:chloe>\r
<t:title>New User Registration</t:title>
- <t:form action="register">
+ <t:form t:action="register">
<table>
<tr>
<th class="field-label">User name:</th>
- <td><t:edit component="username" /></td>
+ <td><t:edit t:component="username" /></td>
</tr>
<tr>
<th class="field-label">Real name:</th>
- <td><t:edit component="realname" /></td>
+ <td><t:edit t:component="realname" /></td>
</tr>
<tr>
<tr>
<th class="field-label">Password:</th>
- <td><t:edit component="new-password" /></td>
+ <td><t:edit t:component="new-password" /></td>
</tr>
<tr>
<th class="field-label">Verify:</th>
- <td><t:edit component="verify-password" /></td>
+ <td><t:edit t:component="verify-password" /></td>
</tr>
<tr>
<tr>
<th class="field-label">E-mail:</th>
- <td><t:edit component="email" /></td>
+ <td><t:edit t:component="email" /></td>
</tr>
<tr>
<tr>
<th class="field-label">Captcha:</th>
- <td><t:edit component="captcha" /></td>
+ <td><t:edit t:component="captcha" /></td>
</tr>
<tr>
<input type="submit" value="Register" />
- <t:if var="http.server.auth.login:user-exists?">
+ <t:if t:var="http.server.auth.login:user-exists?">
<t:error>username taken</t:error>
</t:if>
- <t:if var="http.server.auth.login:password-mismatch?">
+ <t:if t:var="http.server.auth.login:password-mismatch?">
<t:error>passwords do not match</t:error>
</t:if>
bi*
] with-scope ; inline
-M: boilerplate call-responder
+M: boilerplate call-responder*
tuck call-next-method
dup "content-type" header "text/html" = [
clone swap template>>
: resuming-callback ( responder request -- id )\r
cont-id query-param swap callbacks>> at ;\r
\r
-M: callback-responder call-responder ( path responder -- response )\r
+M: callback-responder call-responder* ( path responder -- response )\r
'[\r
, ,\r
\r
[ form view-form ] >>display ;
: <id-redirect> ( id next -- response )
- swap number>string "id" associate <permanent-redirect> ;
+ swap number>string "id" associate <standard-redirect> ;
:: <edit-action> ( form ctor next -- action )
<action>
[
"id" get ctor call delete-tuple
- next f <permanent-redirect>
+ next f <standard-redirect>
] >>submit ;
:: <list-action> ( form ctor -- action )
[ db>> ] [ params>> ] bi make-db db-open\r
[ db set ] [ add-always-destructor ] bi ;\r
\r
-M: db-persistence call-responder\r
+M: db-persistence call-responder*\r
[ connect-db ] [ call-next-method ] bi ;\r
C: <flows> flows
: begin-flow* ( -- id )
- request get [ path>> ] [ query>> ] bi 2array
+ request get
+ [ path>> ] [ request-params ] [ method>> ] tri 3array
flows sget set-at-unique
session-changed ;
+: end-flow-post ( path params -- response )
+ request [
+ clone
+ "POST" >>method
+ swap >>post-data
+ swap >>path
+ ] change
+ request get path>> split-path
+ flows get responder>> call-responder ;
+
: end-flow* ( default id -- response )
- flows sget at [ first2 ] [ f ] ?if <permanent-redirect> ;
+ flows sget at
+ [ first3 "POST" = [ end-flow-post ] [ <standard-redirect> ] if ]
+ [ f <standard-redirect> ] ?if ;
SYMBOL: flow-id
input/>
] when* ;
-M: flows call-responder
+M: flows call-responder*
+ dup flows set
[ add-flow-id ] add-link-hook
[ flow-form-field ] add-form-hook
- flow-id-key request-params at flow-id set
+ flow-id-key request get request-params at flow-id set
call-next-method ;
M: flows init-session*
C: <mock-responder> mock-responder
-M: mock-responder call-responder
+M: mock-responder call-responder*
nip
path>> on
"text/plain" <content> ;
C: <path-check-responder> path-check-responder
-M: path-check-responder call-responder
+M: path-check-responder call-responder*
drop
"text/plain" <content> swap >array >>body ;
C: <base-path-check-responder> base-path-check-responder
-M: base-path-check-responder call-responder
+M: base-path-check-responder call-responder*
2drop
"$funny-dispatcher" resolve-base-path
"text/plain" <content> swap >>body ;
! path is a sequence of path component strings
-GENERIC: call-responder ( path responder -- response )
+GENERIC: call-responder* ( path responder -- response )
-: request-params ( -- assoc )
- request get dup method>> {
+: request-params ( request -- assoc )
+ dup method>> {
{ "GET" [ query>> ] }
{ "HEAD" [ query>> ] }
{ "POST" [ post-data>> ] }
C: <trivial-responder> trivial-responder
-M: trivial-responder call-responder nip response>> call ;
+M: trivial-responder call-responder* nip response>> call ;
: trivial-response-body ( code message -- )
<html>
[ invert-slice ] [ class word-name ] bi*
base-paths get set-at ;
+: call-responder ( path responder -- response )
+ [ add-base-path ] [ call-responder* ] 2bi ;
+
SYMBOL: link-hook
: add-link-hook ( quot -- )
: <temporary-redirect> ( to query -- response )
307 "Temporary Redirect" <redirect> ;
+: <standard-redirect> ( to query -- response )
+ request get method>> "POST" =
+ [ <permanent-redirect> ] [ <temporary-redirect> ] if ;
+
TUPLE: dispatcher default responders ;
: new-dispatcher ( class -- dispatcher )
[ >r drop 1 tail-slice r> ] [ drop default>> ] if
] if ;
-M: dispatcher call-responder ( path dispatcher -- response )
- [ add-base-path ] [ find-responder call-responder ] 2bi ;
+M: dispatcher call-responder* ( path dispatcher -- response )
+ find-responder call-responder ;
TUPLE: vhost-dispatcher default responders ;
request get host>> over responders>> at*
[ nip ] [ drop default>> ] if ;
-M: vhost-dispatcher call-responder ( path dispatcher -- response )
+M: vhost-dispatcher call-responder* ( path dispatcher -- response )
find-vhost call-responder ;
: add-responder ( dispatcher responder path -- dispatcher )
TUPLE: filter-responder responder ;
-M: filter-responder call-responder
+M: filter-responder call-responder*
responder>> call-responder ;
SYMBOL: main-responder
: split-path ( string -- path )
"/" split [ empty? not ] subset ;
+: init-request ( -- )
+ H{ } clone base-paths set
+ [ ] link-hook set
+ [ ] form-hook set ;
+
: do-request ( request -- response )
[
- H{ } clone base-paths set
- [ ] link-hook set
- [ ] form-hook set
-
- [ log-request ]
+ init-request
[ request set ]
+ [ log-request ]
[ path>> split-path main-responder get call-responder ] tri
[ <404> ] unless*
] [
\r
M: foo init-session* drop 0 "x" sset ;\r
\r
-M: foo call-responder\r
+M: foo call-responder*\r
2drop\r
"x" [ 1+ ] schange\r
"text/html" <content> [ "x" sget pprint ] >>body ;\r
\r
"auth-test.db" temp-file sqlite-db [\r
\r
+ init-request\r
init-sessions-table\r
\r
+ [ ] [\r
+ <foo> <session-manager>\r
+ sessions-in-db >>sessions\r
+ session-manager set\r
+ ] unit-test\r
+\r
[\r
empty-session\r
123 >>id session set\r
[ t ] [ session get changed?>> ] unit-test\r
] with-scope\r
\r
- [ ] [\r
- <foo> <session-manager>\r
- sessions-in-db >>sessions\r
- session-manager set\r
- ] unit-test\r
-\r
[ t ] [\r
session-manager get begin-session id>>\r
session-manager get sessions>> get-session session?\r
html.elements ;
IN: http.server.sessions
-TUPLE: session id expiry namespace changed? ;
+TUPLE: session id expires namespace changed? ;
: <session> ( id -- session )
session new
M: filter-responder init-session* responder>> init-session* ;
-TUPLE: session-manager < filter-responder sessions ;
+TUPLE: session-manager < filter-responder sessions timeout domain ;
: <session-manager> ( responder -- responder' )
- null-sessions session-manager boa ;
+ session-manager new
+ swap >>responder
+ null-sessions >>sessions
+ 20 minutes >>timeout ;
: (session-changed) ( session -- )
t >>changed? drop ;
[ namespace>> swap change-at ] keep
(session-changed) ; inline
-: sessions session-manager get sessions>> ;
-
: init-session ( session managed -- )
>r session r> '[ , init-session* ] with-variable ;
-: timeout 20 minutes ;
-
: cutoff-time ( -- time )
- now timeout time+ timestamp>millis ;
+ session-manager get timeout>> from-now timestamp>millis ;
: touch-session ( session -- )
- cutoff-time >>expiry drop ;
+ cutoff-time >>expires drop ;
: empty-session ( -- session )
f <session>
2tri ;
! Destructor
-TUPLE: session-saver session ;
+TUPLE: session-saver manager session ;
C: <session-saver> session-saver
M: session-saver dispose
- session>> dup changed?>> [
- [ touch-session ] [ sessions update-session ] bi
- ] [ drop ] if ;
+ [ session>> ] [ manager>> sessions>> ] bi
+ over changed?>> [
+ [ drop touch-session ] [ update-session ] 2bi
+ ] [ 2drop ] if ;
-: save-session-after ( session -- )
+: save-session-after ( manager session -- )
<session-saver> add-always-destructor ;
-: existing-session ( path responder session -- response )
- [ session set ] [ save-session-after ] bi
- [ session-manager set ] [ responder>> call-responder ] bi ;
+: existing-session ( path manager session -- response )
+ [ nip session set ]
+ [ save-session-after ]
+ [ drop responder>> ] 2tri
+ call-responder ;
: session-id-key "factorsessid" ;
>r request-session-id r> sessions>> get-session ;
: <session-cookie> ( id -- cookie )
- session-id-key <cookie> ;
+ session-id-key <cookie>
+ "$session-manager" resolve-base-path >>path
+ session-manager get timeout>> from-now >>expires
+ session-manager get domain>> >>domain ;
-: new-session ( path responder -- response )
- dup begin-session
- [ existing-session ]
- [ id>> number>string <session-cookie> ] bi
- put-cookie ;
+: put-session-cookie ( response -- response' )
+ session get id>> number>string <session-cookie> put-cookie ;
: session-form-field ( -- )
<input
session get id>> number>string =value
input/> ;
-M: session-manager call-responder ( path responder -- response )
+M: session-manager call-responder* ( path responder -- response )
[ session-form-field ] add-form-hook
- dup request-session [ existing-session ] [ new-session ] if* ;
+ dup session-manager set
+ dup request-session [ dup begin-session ] unless*
+ existing-session put-session-cookie ;
{
! { "id" "ID" +random-id+ system-random-generator }
{ "id" "ID" INTEGER +native-id+ }
- { "expiry" "EXPIRY" BIG-INTEGER +not-null+ }
+ { "expires" "EXPIRES" BIG-INTEGER +not-null+ }
{ "namespace" "NAMESPACE" FACTOR-BLOB }
} define-persistent
: expired-sessions ( -- session )
f <session>
- USE: math now timestamp>millis [ 60 60 * 1000 * - ] keep [a,b] >>expiry
+ USE: math now timestamp>millis [ 60 60 * 1000 * - ] keep [a,b] >>expires
select-tuples ;
: start-expiring-sessions ( db seq -- )
find-index [ serve-file ] [ list-directory ] ?if\r
] [\r
drop\r
- request get path>> "/" append f <permanent-redirect>\r
+ request get path>> "/" append f <standard-redirect>\r
] if ;\r
\r
: serve-object ( filename -- response )\r
[ drop <404> ]\r
if ;\r
\r
-M: file-responder call-responder ( path responder -- response )\r
+M: file-responder call-responder* ( path responder -- response )\r
file-responder set\r
".." over member?\r
[ drop <400> ] [ "/" join serve-object ] if ;\r
USING: accessors kernel sequences combinators kernel namespaces
-classes.tuple assocs splitting words arrays
+classes.tuple assocs splitting words arrays memoize
io io.files io.encodings.utf8 html.elements unicode.case
tuple-syntax xml xml.data xml.writer xml.utilities
http.server
DEFER: process-template
-: chloe-ns TUPLE{ name url: "http://factorcode.org/chloe/1.0" } ;
+: chloe-ns "http://factorcode.org/chloe/1.0" ; inline
+
+: filter-chloe-attrs ( assoc -- assoc' )
+ [ drop name-url chloe-ns = not ] assoc-subset ;
: chloe-tag? ( tag -- ? )
{
{ [ dup tag? not ] [ f ] }
- { [ dup chloe-ns names-match? not ] [ f ] }
+ { [ dup url>> chloe-ns = not ] [ f ] }
[ t ]
} cond nip ;
SYMBOL: tags
+MEMO: chloe-name ( string -- name )
+ name new
+ swap >>tag
+ chloe-ns >>url ;
+
: required-attr ( tag name -- value )
- dup rot at*
+ dup chloe-name rot at*
[ nip ] [ drop " attribute is required" append throw ] if ;
: optional-attr ( tag name -- value )
- swap at ;
+ chloe-name swap at ;
: write-title-tag ( tag -- )
drop
dup empty?
[ drop f ] [ "," split [ dup value ] H{ } map>assoc ] if ;
-: a-flow-attr ( tag -- )
+: flow-attr ( tag -- )
"flow" optional-attr {
{ "none" [ flow-id off ] }
{ "begin" [ begin-flow ] }
{ f [ ] }
} case ;
-: a-session-attr ( tag -- )
+: session-attr ( tag -- )
"session" optional-attr {
{ "none" [ session off flow-id off ] }
{ "current" [ ] }
: a-start-tag ( tag -- )
[
<a
- dup a-flow-attr
- dup a-session-attr
+ dup flow-attr
+ dup session-attr
dup "value" optional-attr [ value f ] [
[ "href" required-attr ]
[ "query" optional-attr parse-query-attr ]
tri ;
: form-start-tag ( tag -- )
- <form
- "POST" =method
- [ "action" required-attr resolve-base-path =action ]
- [ tag-attrs [ drop name-tag "action" = not ] assoc-subset print-attrs ] bi
- form>
- hidden-form-field ;
+ [
+ <form
+ "POST" =method
+ {
+ [ flow-attr ]
+ [ session-attr ]
+ [ "action" required-attr resolve-base-path =action ]
+ [ tag-attrs filter-chloe-attrs print-attrs ]
+ } cleave
+ form>
+ hidden-form-field
+ ] with-scope ;
: form-tag ( tag -- )
[ form-start-tag ]
<boilerplate>
"page" factor-template >>template
<flows>
- <url-sessions>
+ <session-manager>
sessions-in-db >>sessions
test-db <db-persistence> ;
<head>
<t:write-title />
- <t:style include="resource:extra/xmode/code2html/stylesheet.css" />
+ <t:style t:include="resource:extra/xmode/code2html/stylesheet.css" />
<t:style>
body, button {
<t:chloe xmlns:t="http://factorcode.org/chloe/1.0">
- <h2>Annotation: <t:view component="summary" /></h2>
+ <h2>Annotation: <t:view t:component="summary" /></h2>
<table>
- <tr><th class="field-label">Author: </th><td><t:view component="author" /></td></tr>
- <tr><th class="field-label">Mode: </th><td><t:view component="mode" /></td></tr>
- <tr><th class="field-label">Date: </th><td><t:view component="date" /></td></tr>
+ <tr><th class="field-label">Author: </th><td><t:view t:component="author" /></td></tr>
+ <tr><th class="field-label">Mode: </th><td><t:view t:component="mode" /></td></tr>
+ <tr><th class="field-label">Date: </th><td><t:view t:component="date" /></td></tr>
</table>
<div class="description">
- <t:view component="contents" />
+ <t:view t:component="contents" />
</div>
- <t:form action="$pastebin/delete-annotation" class="inline">
- <t:edit component="id" />
- <t:edit component="aid" />
+ <t:form t:action="$pastebin/delete-annotation" class="inline">
+ <t:edit t:component="id" />
+ <t:edit t:component="aid" />
<button class="link-button link">Delete Annotation</button>
</t:form>
<t:title>New Annotation</t:title>
- <t:form action="$pastebin/annotate">
- <t:edit component="id" />
+ <t:form t:action="$pastebin/annotate">
+ <t:edit t:component="id" />
<table>
- <tr><th class="field-label">Summary: </th><td><t:edit component="summary" /></td></tr>
- <tr><th class="field-label">Author: </th><td><t:edit component="author" /></td></tr>
- <tr><th class="field-label">Mode: </th><td><t:edit component="mode" /></td></tr>
- <tr><th class="field-label big-field-label">Description:</th><td><t:edit component="contents" /></td></tr>
- <tr><th class="field-label">Captcha: </th><td><t:edit component="captcha" /></td></tr>
+ <tr><th class="field-label">Summary: </th><td><t:edit t:component="summary" /></td></tr>
+ <tr><th class="field-label">Author: </th><td><t:edit t:component="author" /></td></tr>
+ <tr><th class="field-label">Mode: </th><td><t:edit t:component="mode" /></td></tr>
+ <tr><th class="field-label big-field-label">Description:</th><td><t:edit t:component="contents" /></td></tr>
+ <tr><th class="field-label">Captcha: </th><td><t:edit t:component="captcha" /></td></tr>
<tr>
<td></td>
<td>Leave the captcha blank. Spam-bots will fill it indiscriminantly, so their attempts to register will be blocked.</td>
<t:title>New Paste</t:title>
- <t:form action="$pastebin/new-paste">
+ <t:form t:action="$pastebin/new-paste">
<table>
- <tr><th class="field-label">Summary: </th><td><t:edit component="summary" /></td></tr>
- <tr><th class="field-label">Author: </th><td><t:edit component="author" /></td></tr>
- <tr><th class="field-label">Mode: </th><td><t:edit component="mode" /></td></tr>
- <tr><th class="field-label big-field-label">Description: </th><td><t:edit component="contents" /></td></tr>
- <tr><th class="field-label">Captcha: </th><td><t:edit component="captcha" /></td></tr>
+ <tr><th class="field-label">Summary: </th><td><t:edit t:component="summary" /></td></tr>
+ <tr><th class="field-label">Author: </th><td><t:edit t:component="author" /></td></tr>
+ <tr><th class="field-label">Mode: </th><td><t:edit t:component="mode" /></td></tr>
+ <tr><th class="field-label big-field-label">Description: </th><td><t:edit t:component="contents" /></td></tr>
+ <tr><th class="field-label">Captcha: </th><td><t:edit t:component="captcha" /></td></tr>
<tr>
<td></td>
<td>Leave the captcha blank. Spam-bots will fill it indiscriminantly, so their attempts to register will be blocked.</td>
<th align="left" width="100">Paste by:</th>
<th align="left" width="200">Date:</th>
- <t:summary component="pastes" />
+ <t:summary t:component="pastes" />
</table>
</t:chloe>
<t:chloe xmlns:t="http://factorcode.org/chloe/1.0">
<tr>
- <td><t:a href="view-paste" query="id"><t:view component="summary" /></t:a></td>
- <td><t:view component="author" /></td>
- <td><t:view component="date" /></td>
+ <td><t:a t:href="view-paste" query="id"><t:view t:component="summary" /></t:a></td>
+ <td><t:view t:component="author" /></td>
+ <td><t:view t:component="date" /></td>
</tr>
</t:chloe>
<t:title>Pastebin</t:title>
- <h2>Paste: <t:view component="summary" /></h2>
+ <h2>Paste: <t:view t:component="summary" /></h2>
<table>
- <tr><th class="field-label">Author: </th><td><t:view component="author" /></td></tr>
- <tr><th class="field-label">Mode: </th><td><t:view component="mode" /></td></tr>
- <tr><th class="field-label">Date: </th><td><t:view component="date" /></td></tr>
+ <tr><th class="field-label">Author: </th><td><t:view t:component="author" /></td></tr>
+ <tr><th class="field-label">Mode: </th><td><t:view t:component="mode" /></td></tr>
+ <tr><th class="field-label">Date: </th><td><t:view t:component="date" /></td></tr>
</table>
- <div class="description">
- <t:view component="contents" />
- </div>
+ <pre class="description"><t:view t:component="contents" /></pre>
- <t:form action="$pastebin/delete-paste" class="inline">
- <t:edit component="id" />
+ <t:form t:action="$pastebin/delete-paste" class="inline">
+ <t:edit t:component="id" />
<button class="link-button link">Delete Paste</button>
</t:form>
|
- <t:a href="$pastebin/annotate" query="id">Annotate</t:a>
+ <t:a t:href="$pastebin/annotate" t:query="id">Annotate</t:a>
- <t:view component="annotations" />
+ <t:view t:component="annotations" />
</t:chloe>
<feed-action> "feed.xml" add-responder
<paste-form> [ <paste> ] <view-paste-action> "view-paste" add-responder
[ <paste> ] "$pastebin/list" <delete-paste-action> <protected> "delete-paste" add-responder
- [ <annotation> ] "$pastebin/view-paste" <protected> <delete-annotation-action> "delete-annotation" add-responder
+ [ <annotation> ] "$pastebin/view-paste" <delete-annotation-action> <protected> "delete-annotation" add-responder
<paste-form> [ <paste> ] <view-paste-action> "$pastebin/view-paste" add-responder
<new-paste-form> [ <paste> now >>date ] "$pastebin/view-paste" <new-paste-action> "new-paste" add-responder
<new-annotation-form> [ <annotation> now >>date ] "$pastebin/view-paste" <annotate-action> "annotate" add-responder
<t:chloe xmlns:t="http://factorcode.org/chloe/1.0">
- <t:atom title="Pastebin - Atom" href="$pastebin/feed.xml" />
+ <t:atom t:title="Pastebin - Atom" t:href="$pastebin/feed.xml" />
- <t:style include="resource:extra/webapps/pastebin/pastebin.css" />
+ <t:style t:include="resource:extra/webapps/pastebin/pastebin.css" />
<div class="navbar">
- <t:a href="$pastebin/list">Pastes</t:a>
- | <t:a href="$pastebin/new-paste">New Paste</t:a>
- | <t:a href="$pastebin/feed.xml">Atom Feed</t:a>
+ <t:a t:href="$pastebin/list">Pastes</t:a>
+ | <t:a t:href="$pastebin/new-paste">New Paste</t:a>
+ | <t:a t:href="$pastebin/feed.xml">Atom Feed</t:a>
+
+ <t:if t:svar="http.server.auth:logged-in-user">
+
+ <t:if t:code="http.server.auth.login:allow-edit-profile?">
+ | <t:a t:href="$login/edit-profile" t:flow="begin">Edit Profile</t:a>
+ </t:if>
+
+ <t:form t:action="$login/logout" t:flow="begin" class="inline">
+ | <button type="submit" class="link-button link">Logout</button>
+ </t:form>
- <t:comment>
- <t:if code="http.server.auth.login:allow-edit-profile?">
- | <t:a href="$login/edit-profile" flow="begin">Edit Profile</t:a>
</t:if>
- <t:form action="$login/logout" class="inline">
- | <button type="submit" class="link-button link">Logout</button>
- </t:form>
- </t:comment>
</div>
<h1><t:write-title /></h1>
<t:title>Planet Factor Administration</t:title>
- <t:summary component="blogroll" />
+ <t:summary t:component="blogroll" />
<p>
- <t:a href="$planet-factor/admin/edit-blog">Add Blog</t:a>
- | <t:a href="$planet-factor/admin/update">Update</t:a>
+ <t:a t:href="$planet-factor/admin/edit-blog">Add Blog</t:a>
+ | <t:a t:href="$planet-factor/admin/update">Update</t:a>
</p>
</t:chloe>
<t:chloe xmlns:t="http://factorcode.org/chloe/1.0">
- <t:a href="$planet-factor/admin/edit-blog" query="id"><t:view component="name" /></t:a>
+ <t:a t:href="$planet-factor/admin/edit-blog" t:query="id"><t:view t:component="name" /></t:a>
</t:chloe>
<t:title>Edit Blog</t:title>
- <t:form action="$planet-factor/admin/edit-blog">
+ <t:form t:action="$planet-factor/admin/edit-blog">
- <t:edit component="id" />
+ <t:edit t:component="id" />
<table>
<tr>
<th class="field-label">Blog name:</th>
- <td><t:edit component="name" /></td>
+ <td><t:edit t:component="name" /></td>
</tr>
<tr>
<th class="field-label">Home page:</th>
- <td><t:edit component="www-url" /></td>
+ <td><t:edit t:component="www-url" /></td>
</tr>
<tr>
<th class="field-label">Feed:</th>
- <td><t:edit component="feed-url" /></td>
+ <td><t:edit t:component="feed-url" /></td>
</tr>
</table>
</t:form>
- <t:form action="$planet-factor/admin/delete-blog" class="inline">
- <t:edit component="id" />
+ <t:form t:action="$planet-factor/admin/delete-blog" class="inline">
+ <t:edit t:component="id" />
<button type="submit" class="link-button link">Delete</button>
</t:form>
</t:chloe>
<t:chloe xmlns:t="http://factorcode.org/chloe/1.0">
<p class="news">
- <strong><t:view component="title" /></strong> <br/>
- <t:a value="link" session="none" class="more">Read More...</t:a>
+ <strong><t:view t:component="title" /></strong> <br/>
+ <t:a value="link" t:session="none" class="more">Read More...</t:a>
</p>
</t:chloe>
<t:chloe xmlns:t="http://factorcode.org/chloe/1.0">
<h2 class="posting-title">
- <t:a value="link" session="none"><t:view component="title" /></t:a>
+ <t:a t:value="link" t:session="none"><t:view t:component="title" /></t:a>
</h2>
<p class="posting-body">
- <t:view component="description" />
+ <t:view t:component="description" />
</p>
<p class="posting-date">
- <t:a value="link" session="none"><t:view component="pub-date" /></t:a>
+ <t:a t:value="link" t:session="none"><t:view t:component="pub-date" /></t:a>
</p>
</t:chloe>
: start-update-task ( planet db seq -- )
'[
- , , , [ update-cached-postings ] with-db
+ , , , [
+ dup filter-responder? [ responder>> ] when
+ update-cached-postings
+ ] with-db
] 10 minutes every drop ;
<t:style include="resource:extra/webapps/planet/planet.css" />
<div class="navbar">
- <t:a href="$planet-factor/list">Front Page</t:a>
- | <t:a href="$planet-factor/feed.xml">Atom Feed</t:a>
- | <t:a href="$planet-factor/admin">Admin</t:a>
-
- <t:comment>
- <t:if code="http.server.auth.login:allow-edit-profile?">
- | <t:a href="$login/edit-profile" flow="begin">Edit Profile</t:a>
+ <t:a t:href="$planet-factor/list">Front Page</t:a>
+ | <t:a t:href="$planet-factor/feed.xml">Atom Feed</t:a>
+ | <t:a t:href="$planet-factor/admin">Admin</t:a>
+
+ <t:if t:svar="http.server.auth:logged-in-user">
+ <t:if t:code="http.server.auth.login:allow-edit-profile?">
+ | <t:a t:href="$login/edit-profile" t:flow="begin">Edit Profile</t:a>
+ </t:if>
+
+ <t:form t:action="$login/logout" t:flow="begin" class="inline">
+ | <button type="submit" class="link-button link">Logout</button>
+ </t:form>
</t:if>
-
- <t:form action="$login/logout" class="inline">
- | <button type="submit" class="link-button link">Logout</button>
- </t:form>
- </t:comment>
</div>
<h1><t:write-title /></h1>
<t:chloe xmlns:t="http://factorcode.org/chloe/1.0">
- <t:summary component="postings" />
+ <t:summary t:component="postings" />
</t:chloe>
<table width="100%" cellpadding="10">
<tr>
- <td> <t:view component="postings" /> </td>
+ <td> <t:view t:component="postings" /> </td>
<td valign="top" width="25%" class="infobox">
<h2>Blogroll</h2>
- <t:summary component="blogroll" />
+ <t:summary t:component="blogroll" />
</td>
</tr>
</table>
<table class="todo-list">
<tr><th>Summary</th><th>Priority</th><th>View</th><th>Edit</th></tr>
- <t:summary component="list" />
+ <t:summary t:component="list" />
</table>
</t:chloe>
<tr>
<td>
- <t:view component="summary" />
+ <t:view t:component="summary" />
</td>
<td>
- <t:view component="priority" />
+ <t:view t:component="priority" />
</td>
<td>
- <t:a href="$todo-list/view" query="id">View</t:a>
+ <t:a t:href="$todo-list/view" t:query="id">View</t:a>
</td>
<td>
- <t:a href="$todo-list/edit" query="id">Edit</t:a>
+ <t:a t:href="$todo-list/edit" t:query="id">Edit</t:a>
</td>
</tr>
<t:style include="resource:extra/webapps/todo/todo.css" />
<div class="navbar">
- <t:a href="$todo-list/list">List Items</t:a>
- | <t:a href="$todo-list/edit">Add Item</t:a>
+ <t:a t:href="$todo-list/list">List Items</t:a>
+ | <t:a t:href="$todo-list/edit">Add Item</t:a>
- <t:if code="http.server.auth.login:allow-edit-profile?">
- | <t:a href="$login/edit-profile" flow="begin">Edit Profile</t:a>
+ <t:if t:code="http.server.auth.login:allow-edit-profile?">
+ | <t:a t:href="$login/edit-profile" t:flow="begin">Edit Profile</t:a>
</t:if>
- <t:form action="$login/logout" class="inline">
+ <t:form t:action="$login/logout" t:flow="begin" class="inline">
| <button type="submit" class="link-button link">Logout</button>
</t:form>
</div>
</table>
<div class="description">
- <t:view component="description" />
+ <t:view t:component="description" />
</div>
- <t:a href="$todo-list/edit" query="id">Edit</t:a>
+ <t:a t:href="$todo-list/edit" t:query="id">Edit</t:a>
|
- <t:form action="$todo-list/delete" class="inline">
- <t:edit component="id" />
+ <t:form t:action="$todo-list/delete" class="inline">
+ <t:edit t:component="id" />
<button class="link-button link">Delete</button>
</t:form>