Upped version in Catalyst.pm
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Manual / Cookbook.pod
CommitLineData
fc7ec1d9 1=head1 NAME
2
3Catalyst::Manual::Cookbook - Cooking with Catalyst
4
5=head1 DESCRIPTION
6
aba94964 7Yummy code like your mum used to bake!
fc7ec1d9 8
9=head1 RECIPES
10
11=head2 Force debug screen
12
eff5f524 13You can force Catalyst to display the debug screen at the end of the request by
14placing a C<die()> call in the C<end> action.
fc7ec1d9 15
61b1e958 16 sub end : Private {
17 my ( $self, $c ) = @_;
2343e117 18 die "forced debug";
61b1e958 19 }
fc7ec1d9 20
379ca371 21If you're tired of removing and adding this all the time, you can add a
22condition in the C<end> action. For example:
23
24 sub end : Private {
25 my ( $self, $c ) = @_;
26 die "forced debug" if $c->req->params->{dump_info};
27 }
28
29Then just add to your query string C<"&dump_info=1">, or the like, to
30force debug output.
aff93052 31
aff93052 32
fc7ec1d9 33=head2 Disable statistics
34
35Just add this line to your application class if you don't want those nifty
36statistics in your debug messages.
37
38 sub Catalyst::Log::info { }
39
40=head2 Scaffolding
41
42Scaffolding is very simple with Catalyst.
fc7ec1d9 43
4b8cb778 44The recommended way is to use Catalyst::Helper::Controller::Scaffold.
fc7ec1d9 45
4b8cb778 46Just install this module, and to scaffold a Class::DBI Model class, do the following:
fc7ec1d9 47
4b8cb778 48./script/myapp_create controller <name> Scaffold <CDBI::Class>Scaffolding
fc7ec1d9 49
fc7ec1d9 50
fc7ec1d9 51
fc7ec1d9 52
822fe954 53=head2 File uploads
54
55=head3 Single file upload with Catalyst
aba94964 56
26e73131 57To implement uploads in Catalyst, you need to have a HTML form similar to
aba94964 58this:
59
60 <form action="/upload" method="post" enctype="multipart/form-data">
61 <input type="hidden" name="form_submit" value="yes">
62 <input type="file" name="my_file">
63 <input type="submit" value="Send">
64 </form>
65
379ca371 66It's very important not to forget C<enctype="multipart/form-data"> in
67the form.
aba94964 68
69Catalyst Controller module 'upload' action:
70
5c0ff128 71 sub upload : Global {
72 my ($self, $c) = @_;
4d89569d 73
74 if ( $c->request->parameters->{form_submit} eq 'yes' ) {
75
76 if ( my $upload = $c->request->upload('my_file') ) {
47ae6960 77
5c0ff128 78 my $filename = $upload->filename;
47ae6960 79 my $target = "/tmp/upload/$filename";
80
3ffaf022 81 unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
47ae6960 82 die( "Failed to copy '$filename' to '$target': $!" );
5c0ff128 83 }
5c0ff128 84 }
85 }
4d89569d 86
5c0ff128 87 $c->stash->{template} = 'file_upload.html';
88 }
89
822fe954 90=head3 Multiple file upload with Catalyst
5c0ff128 91
379ca371 92Code for uploading multiple files from one form needs a few changes:
5c0ff128 93
379ca371 94The form should have this basic structure:
5c0ff128 95
96 <form action="/upload" method="post" enctype="multipart/form-data">
97 <input type="hidden" name="form_submit" value="yes">
98 <input type="file" name="file1" size="50"><br>
99 <input type="file" name="file2" size="50"><br>
100 <input type="file" name="file3" size="50"><br>
101 <input type="submit" value="Send">
102 </form>
103
379ca371 104And in the controller:
5c0ff128 105
106 sub upload : Local {
107 my ($self, $c) = @_;
4d89569d 108
109 if ( $c->request->parameters->{form_submit} eq 'yes' ) {
110
111 for my $field ( $c->req->upload ) {
112
02a53b81 113 my $upload = $c->req->upload($field);
4d89569d 114 my $filename = $upload->filename;
47ae6960 115 my $target = "/tmp/upload/$filename";
116
3ffaf022 117 unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
47ae6960 118 die( "Failed to copy '$filename' to '$target': $!" );
aba94964 119 }
120 }
61b1e958 121 }
4d89569d 122
5c0ff128 123 $c->stash->{template} = 'file_upload.html';
124 }
125
379ca371 126C<for my $field ($c-E<gt>req->upload)> loops automatically over all file
127input fields and gets input names. After that is basic file saving code,
128just like in single file upload.
aba94964 129
379ca371 130Notice: C<die>ing might not be what you want to do, when an error
131occurs, but it works as an example. A better idea would be to store
132error C<$!> in $c->stash->{error} and show a custom error template
133displaying this message.
aba94964 134
5c0ff128 135For more information about uploads and usable methods look at
379ca371 136L<Catalyst::Request::Upload> and L<Catalyst::Request>.
aba94964 137
deb90705 138=head2 Authentication with Catalyst::Plugin::Authentication::CDBI
139
140There are (at least) two ways to implement authentication with this plugin:
eff5f524 1411) only checking username and password;
379ca371 1422) checking username, password, and the roles the user has
deb90705 143
144For both variants you'll need the following code in your MyApp package:
145
146 use Catalyst qw/Session::FastMmap Static Authentication::CDBI/;
147
148 MyApp->config( authentication => { user_class => 'MyApp::M::MyApp::Users',
149 user_field => 'email',
150 password_field => 'password' });
151
152'user_class' is a Class::DBI class for your users table.
153'user_field' tells which field is used for username lookup (might be
51ef2818 154email, first name, surname etc.).
deb90705 155'password_field' is, well, password field in your table and by default
156password is stored in plain text. Authentication::CDBI looks for 'user'
157and 'password' fields in table, if they're not defined in the config.
158
51ef2818 159In PostgreSQL, the users table might be something like:
deb90705 160
51ef2818 161 CREATE TABLE users (
162 user_id serial,
163 name varchar(100),
164 surname varchar(100),
165 password varchar(100),
166 email varchar(100),
167 primary key(user_id)
168 );
deb90705 169
170We'll discuss the first variant for now:
51ef2818 1711. user:password login/auth without roles
deb90705 172
51ef2818 173To log in a user you might use an action like this:
deb90705 174
7c1078a4 175 sub login : Local {
deb90705 176 my ($self, $c) = @_;
177 if ($c->req->params->{username}) {
178 $c->session_login($c->req->params->{username},
61b1e958 179 $c->req->params->{password} );
deb90705 180 if ($c->req->{user}) {
9bee7d37 181 $c->forward('/restricted_area');
deb90705 182 }
183 }
61b1e958 184 }
deb90705 185
7c1078a4 186This action should not go in your MyApp class...if it does, it will
187conflict with the built-in method of the same name. Instead, put it
188in a Controller class.
189
deb90705 190$c->req->params->{username} and $c->req->params->{password} are html
61b1e958 191form parameters from a login form. If login succeeds, then
192$c->req->{user} contains the username of the authenticated user.
deb90705 193
51ef2818 194If you want to remember the user's login status in between further
195requests, then just use the C<$c-E<gt>session_login> method. Catalyst will
196create a session id and session cookie and automatically append session
197id to all urls. So all you have to do is just check $c->req->{user}
61b1e958 198where needed.
deb90705 199
51ef2818 200To log out a user, just call $c->session_logout.
deb90705 201
51ef2818 202Now let's take a look at the second variant:
2032. user:password login/auth with roles
deb90705 204
51ef2818 205To use roles you need to add the following parameters to MyApp->config in the 'authentication' section:
deb90705 206
207 role_class => 'MyApp::M::MyApp::Roles',
208 user_role_class => 'MyApp::M::MyApp::UserRoles',
209 user_role_user_field => 'user_id',
210 user_role_role_field => 'role_id',
211
212Corresponding tables in PostgreSQL could look like this:
213
51ef2818 214 CREATE TABLE roles (
215 role_id serial,
216 name varchar(100),
217 primary key(role_id)
218 );
219
220 CREATE TABLE user_roles (
221 user_role_id serial,
222 user_id int,
223 role_id int,
224 primary key(user_role_id),
225 foreign key(user_id) references users(user_id),
226 foreign key(role_id) references roles(role_id)
227 );
deb90705 228
61b1e958 229The 'roles' table is a list of role names and the 'user_role' table is
230used for the user -> role lookup.
deb90705 231
51ef2818 232Now if a logged-in user wants to see a location which is allowed only
233for people with an 'admin' role, in your controller you can check it
61b1e958 234with:
deb90705 235
61b1e958 236 sub add : Local {
deb90705 237 my ($self, $c) = @_;
238 if ($c->roles(qw/admin/)) {
016373e6 239 $c->res->output("Your account has the role 'admin.'");
deb90705 240 } else {
016373e6 241 $c->res->output("You're not allowed to be here.");
deb90705 242 }
61b1e958 243 }
deb90705 244
51ef2818 245One thing you might need is to forward non-authenticated users to a login
246form if they try to access restricted areas. If you want to do this
247controller-wide (if you have one controller for your admin section) then it's
9bee7d37 248best to add a user check to a 'begin' action:
deb90705 249
61b1e958 250 sub begin : Private {
deb90705 251 my ($self, $c) = @_;
252 unless ($c->req->{user}) {
253 $c->req->action(undef); ## notice this!!
9bee7d37 254 $c->forward('/user/login');
deb90705 255 }
61b1e958 256 }
deb90705 257
26e73131 258Pay attention to $c->req->action(undef). This is needed because of the
259way $c->forward works - C<forward> to C<login> gets called, but after
260that Catalyst will still execute the action defined in the URI (e.g. if
261you tried to go to C</add>, then first 'begin' will forward to 'login',
262but after that 'add' will nonetheless be executed). So
263$c->req->action(undef) undefines any actions that were to be called and
264forwards the user where we want him/her to be.
deb90705 265
51ef2818 266And this is all you need to do.
deb90705 267
afb208ae 268=head2 Pass-through login (and other actions)
269
eff5f524 270An easy way of having assorted actions that occur during the processing
271of a request that are orthogonal to its actual purpose - logins, silent
afb208ae 272commands etc. Provide actions for these, but when they're required for
eff5f524 273something else fill e.g. a form variable __login and have a sub begin
274like so:
afb208ae 275
eff5f524 276 sub begin : Private {
277 my ($self, $c) = @_;
278 foreach my $action (qw/login docommand foo bar whatever/) {
279 if ($c->req->params->{"__${action}"}) {
280 $c->forward($action);
281 }
282 }
afb208ae 283 }
145074c2 284
285=head2 How to use Catalyst without mod_perl
286
287Catalyst applications give optimum performance when run under mod_perl.
61b1e958 288However sometimes mod_perl is not an option, and running under CGI is
51ef2818 289just too slow. There's also an alternative to mod_perl that gives
dec2a2a9 290reasonable performance named FastCGI.
145074c2 291
822fe954 292=head3 Using FastCGI
145074c2 293
61b1e958 294To quote from L<http://www.fastcgi.com/>: "FastCGI is a language
295independent, scalable, extension to CGI that provides high performance
296without the limitations of specific server APIs." Web server support
297is provided for Apache in the form of C<mod_fastcgi> and there is Perl
298support in the C<FCGI> module. To convert a CGI Catalyst application
299to FastCGI one needs to initialize an C<FCGI::Request> object and loop
300while the C<Accept> method returns zero. The following code shows how
301it is done - and it also works as a normal, single-shot CGI script.
145074c2 302
303 #!/usr/bin/perl
304 use strict;
305 use FCGI;
306 use MyApp;
307
308 my $request = FCGI::Request();
309 while ($request->Accept() >= 0) {
1c61c726 310 MyApp->run;
145074c2 311 }
312
61b1e958 313Any initialization code should be included outside the request-accept
314loop.
145074c2 315
51ef2818 316There is one little complication, which is that C<MyApp-E<gt>run> outputs a
61b1e958 317complete HTTP response including the status line (e.g.:
318"C<HTTP/1.1 200>").
319FastCGI just wants a set of headers, so the sample code captures the
320output and drops the first line if it is an HTTP status line (note:
321this may change).
322
323The Apache C<mod_fastcgi> module is provided by a number of Linux
324distros and is straightforward to compile for most Unix-like systems.
325The module provides a FastCGI Process Manager, which manages FastCGI
326scripts. You configure your script as a FastCGI script with the
327following Apache configuration directives:
145074c2 328
329 <Location /fcgi-bin>
330 AddHandler fastcgi-script fcgi
331 </Location>
332
333or:
334
335 <Location /fcgi-bin>
336 SetHandler fastcgi-script
337 Action fastcgi-script /path/to/fcgi-bin/fcgi-script
338 </Location>
339
340C<mod_fastcgi> provides a number of options for controlling the FastCGI
341scripts spawned; it also allows scripts to be run to handle the
51ef2818 342authentication, authorization, and access check phases.
145074c2 343
61b1e958 344For more information see the FastCGI documentation, the C<FCGI> module
345and L<http://www.fastcgi.com/>.
eff5f524 346
379ca371 347=head2 Serving static content
348
349Serving static content in Catalyst can be somewhat tricky; this recipe
350shows one possible solution. Using this recipe will serve all static
351content through Catalyst when developing with the built-in HTTP::Daemon
352server, and will make it easy to use Apache to serve the content when
353your app goes into production.
354
355Static content is best served from a single directory within your root
356directory. Having many different directories such as C<root/css> and
357C<root/images> requires more code to manage, because you must separately
358identify each static directory--if you decide to add a C<root/js>
359directory, you'll need to change your code to account for it. In
360contrast, keeping all static directories as subdirectories of a main
361C<root/static> directory makes things much easier to manager. Here's an
362example of a typical root directory structure:
363
364 root/
365 root/content.tt
366 root/controller/stuff.tt
367 root/header.tt
368 root/static/
369 root/static/css/main.css
370 root/static/images/logo.jpg
371 root/static/js/code.js
372
373
374All static content lives under C<root/static> with everything else being
375Template Toolkit files. Now you can identify the static content by
376matching C<static> from within Catalyst.
377
378=head3 Serving with HTTP::Daemon (myapp_server.pl)
379
380To serve these files under the standalone server, we first must load the
381Static plugin. Install L<Catalyst::Plugin::Static> if it's not already
382installed.
383
384In your main application class (MyApp.pm), load the plugin:
385
386 use Catalyst qw/-Debug FormValidator Static OtherPlugin/;
387
388You will also need to make sure your end method does I<not> forward
389static content to the view, perhaps like this:
390
391 sub end : Private {
392 my ( $self, $c ) = @_;
393
394 $c->forward( 'MyApp::V::TT' )
395 unless ( $c->res->body || !$c->stash->{template} );
396 }
397
398This code will only forward to the view if a template has been
399previously defined by a controller and if there is not already data in
400C<$c-E<gt>res-E<gt>body>.
401
402Next, create a controller to handle requests for the /static path. Use
403the Helper to save time. This command will create a stub controller as
404C<lib/MyApp/C/Static.pm>.
405
406 $ script/myapp_create.pl controller Static
407
408Edit the file and add the following methods:
409
410 # serve all files under /static as static files
411 sub default : Path('/static') {
412 my ( $self, $c ) = @_;
413
414 # Optional, allow the browser to cache the content
415 $c->res->headers->header( 'Cache-Control' => 'max-age=86400' );
416
417 $c->serve_static; # from Catalyst::Plugin::Static
418 }
419
420 # also handle requests for /favicon.ico
421 sub favicon : Path('/favicon.ico') {
422 my ( $self, $c ) = @_;
423
424 $c->serve_static;
425 }
426
427You can also define a different icon for the browser to use instead of
428favicon.ico by using this in your HTML header:
429
430 <link rel="icon" href="/static/myapp.ico" type="image/x-icon" />
431
432=head3 Common problems
433
434The Static plugin makes use of the C<shared-mime-info> package to
435automatically determine MIME types. This package is notoriously
26e73131 436difficult to install, especially on win32 and OS X. For OS X the easiest
379ca371 437path might be to install Fink, then use C<apt-get install
438shared-mime-info>. Restart the server, and everything should be fine.
439
440Make sure you are using the latest version (>= 0.16) for best
441results. If you are having errors serving CSS files, or if they get
442served as text/plain instead of text/css, you may have an outdated
443shared-mime-info version. You may also wish to simply use the following
444code in your Static controller:
445
446 if ($c->req->path =~ /css$/i) {
447 $c->serve_static( "text/css" );
448 } else {
449 $c->serve_static;
450 }
451
452=head3 Serving with Apache
453
454When using Apache, you can completely bypass Catalyst and the Static
455controller by intercepting requests for the C<root/static> path at the
456server level. All that is required is to define a DocumentRoot and add a
457separate Location block for your static content. Here is a complete
6d23e1f4 458config for this application under mod_perl 1.x:
379ca371 459
460 <Perl>
461 use lib qw(/var/www/MyApp/lib);
462 </Perl>
463 PerlModule MyApp
464
465 <VirtualHost *>
466 ServerName myapp.example.com
467 DocumentRoot /var/www/MyApp/root
468 <Location />
469 SetHandler perl-script
470 PerlHandler MyApp
471 </Location>
472 <LocationMatch "/(static|favicon.ico)">
473 SetHandler default-handler
474 </LocationMatch>
475 </VirtualHost>
476
6d23e1f4 477And here's a simpler example that'll get you started:
478
479 Alias /static/ "/my/static/files/"
480 <Location "/static">
481 SetHandler none
482 </Location>
483
379ca371 484=head2 Forwarding with arguments
145074c2 485
eff5f524 486Sometimes you want to pass along arguments when forwarding to another
487action. As of version 5.30, arguments can be passed in the call to
488C<forward>; in earlier versions, you can manually set the arguments in
489the Catalyst Request object:
e6394847 490
eff5f524 491 # version 5.30 and later:
492 $c->forward('/wherever', [qw/arg1 arg2 arg3/]);
2343e117 493
eff5f524 494 # pre-5.30
2343e117 495 $c->req->args([qw/arg1 arg2 arg3/]);
496 $c->forward('/wherever');
497
eff5f524 498(See L<Catalyst::Manual::Intro#Flow_Control> for more information on
499passing arguments via C<forward>.)
500
822fe954 501=head2 Configure your application
502
503You configure your application with the C<config> method in your
504application class. This can be hard-coded, or brought in from a
505separate configuration file.
506
507=head3 Using YAML
508
509YAML is a method for creating flexible and readable configuration
510files. It's a great way to keep your Catalyst application configuration
511in one easy-to-understand location.
512
513In your application class (e.g. C<lib/MyApp.pm>):
514
515 use YAML;
516 # application setup
517 __PACKAGE__->config( YAML::LoadFile(__PACKAGE__->config->{'home'} . '/myapp.yml') );
518 __PACKAGE__->setup;
519
520Now create C<myapp.yml> in your application home:
521
522 --- #YAML:1.0
523 # DO NOT USE TABS FOR INDENTATION OR label/value SEPARATION!!!
524 name: MyApp
525
526 # authentication; perldoc Catalyst::Plugin::Authentication::CDBI
527 authentication:
528 user_class: 'MyApp::M::MyDB::Customer'
529 user_field: 'username'
530 password_field: 'password'
531 password_hash: 'md5'
532 role_class: 'MyApp::M::MyDB::Role'
533 user_role_class: 'MyApp::M::MyDB::PersonRole'
534 user_role_user_field: 'person'
535
536 # session; perldoc Catalyst::Plugin::Session::FastMmap
537 session:
538 expires: '3600'
539 rewrite: '0'
540 storage: '/tmp/myapp.session'
541
542 # emails; perldoc Catalyst::Plugin::Email
543 # this passes options as an array :(
544 email:
545 - SMTP
546 - localhost
547
548This is equivalent to:
549
550 # configure base package
551 __PACKAGE__->config( name => MyApp );
552 # configure authentication
553 __PACKAGE__->config->{authentication} = {
554 user_class => 'MyApp::M::MyDB::Customer',
555 ...
556 };
557 # configure sessions
558 __PACKAGE__->config->{session} = {
559 expires => 3600,
560 ...
561 };
562 # configure email sending
563 __PACKAGE__->config->{email} = [qw/SMTP localhost/];
564
565See also L<YAML>.
566
3912ee04 567=head2 Using existing CDBI (etc.) classes with Catalyst
568
569Many people have existing Model classes that they would like to use with
570Catalyst (or, conversely, they want to write Catalyst models that can be
571used outside of Catalyst, e.g. in a cron job). It's trivial to write a
572simple component in Catalyst that slurps in an outside Model:
573
574 package MyApp::M::Catalog;
575 use base qw/Catalyst::Base Some::Other::CDBI::Module::Catalog/;
576 1;
577
578and that's it! Now C<Some::Other::CDBI::Module::Catalog> is part of your
579Cat app as C<MyApp::M::Catalog>.
580
6d23e1f4 581=head2 Delivering a Custom Error Page
f25a3283 582
6d23e1f4 583By default, Catalyst will display its own error page whenever it
584encounters an error in your application. When running under C<-Debug>
585mode, the error page is a useful screen including the error message and
586a full Data::Dumper output of the C<$c> context object. When not in
587C<-Debug>, users see a simple "Please come back later" screen.
f25a3283 588
26e73131 589To use a custom error page, use a special C<end> method to short-circuit
6d23e1f4 590the error processing. The following is an example; you might want to
591adjust it further depending on the needs of your application (for
592example, any calls to C<fillform> will probably need to go into this
593C<end> method; see L<Catalyst::Plugin::FillInForm>).
f25a3283 594
6d23e1f4 595 sub end : Private {
596 my ( $self, $c ) = @_;
597
598 if ( scalar @{ $c->error } ) {
599 $c->stash->{errors} = $c->error;
600 $c->stash->{template} = 'errors.tt';
601 $c->forward('MyApp::View::TT');
121baf93 602 $c->error(0);
6d23e1f4 603 }
604
605 return 1 if $c->response->status =~ /^3\d\d$/;
606 return 1 if $c->response->body;
607
608 unless ( $c->response->content_type ) {
609 $c->response->content_type('text/html; charset=utf-8');
610 }
611
612 $c->forward('MyApp::View::TT');
613 }
f25a3283 614
6d23e1f4 615You can manually set errors in your code to trigger this page by calling
f25a3283 616
6d23e1f4 617 $c->error( 'You broke me!' );
f25a3283 618
d761e049 619=head2 Require user logins
620
621It's often useful to restrict access to your application to a set of
622registered users, forcing everyone else to the login page until they're
623signed in.
624
625To implement this in your application make sure you have a customer
626table with username and password fields and a corresponding Model class
627in your Catalyst application, then make the following changes:
628
629=head3 lib/MyApp.pm
630
631 use Catalyst qw/Session::FastMmap Authentication::CDBI/;
632
633 __PACKAGE__->config->{authentication} = {
634 'user_class' => 'ScratchPad::M::MyDB::Customer',
635 'user_field' => 'username',
636 'password_field' => 'password',
637 'password_hash' => '',
638 };
639
640 sub auto : Private {
641 my ($self, $c) = @_;
642 my $login_path = 'user/login';
643
644 # allow people to actually reach the login page!
645 if ($c->req->path eq $login_path) {
646 return 1;
647 }
648
649 # if we have a user ... we're OK
650 if ( $c->req->user ) {
651 $c->session->{'authed_user'} =
652 MyApp::M::MyDB::Customer->retrieve(
653 'username' => $c->req->user
654 );
655 }
656
657 # otherwise they're not logged in
658 else {
659 # force the login screen to be shown
660 $c->res->redirect($c->req->base . $login_path);
661 }
662
663 # continue with the processing chain
664 return 1;
665 }
666
667=head3 lib/MyApp/C/User.pm
668
669 sub login : Path('/user/login') {
670 my ($self, $c) = @_;
671
672 # default template
673 $c->stash->{'template'} = "user/login.tt";
674 # default form message
675 $c->stash->{'message'} = 'Please enter your username and password';
676
677 if ( $c->req->param('username') ) {
678 # try to log the user in
679 $c->session_login(
680 $c->req->param('username'),
681 $c->req->param('password'),
682 );
683
684 # if we have a user we're logged in
685 if ( $c->req->user ) {
686 $c->res->redirect('/some/page');
687 }
688
689 # otherwise we failed to login, try again!
690 else {
691 $c->stash->{'message'} =
692 'Unable to authenticate the login details supplied';
693 }
694 }
695 }
696
697 sub logout : Path('/user/logout') {
698 my ($self, $c) = @_;
699 # logout the session, and remove information we've stashed
700 $c->session_logout;
701 delete $c->session->{'authed_user'};
702
703 # do the 'default' action
704 $c->res->redirect($c->req->base);
705}
706
707
708=head3 root/base/user/login.tt
709
710 [% INCLUDE header.tt %]
711 <form action="/user/login" method="POST" name="login_form">
712 [% message %]<br />
713 <label for="username">username:</label><br />
714 <input type="text" id="username" name="username" /><br />
715
716 <label for="password">password:</label><br />
717 <input type="password" id="password" name="password" /><br />
718
719 <input type="submit" value="log in" name="form_submit" />
720 </form>
721 [% INCLUDE footer.tt %]
722
fc7ec1d9 723=head1 AUTHOR
724
725Sebastian Riedel, C<sri@oook.de>
379ca371 726Danijel Milicevic, C<me@danijel.de>
727Viljo Marrandi, C<vilts@yahoo.com>
822fe954 728Marcus Ramberg, C<mramberg@cpan.org>
729Jesse Sheidlower, C<jester@panix.com>
379ca371 730Andy Grundman, C<andy@hybridized.org>
3912ee04 731Chisel Wright, C<pause@herlpacker.co.uk>
fc7ec1d9 732
733=head1 COPYRIGHT
734
61b1e958 735This program is free software, you can redistribute it and/or modify it
736under the same terms as Perl itself.