updated log format
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Manual / Cookbook.pod
index 2cfdf77..db58529 100644 (file)
@@ -40,45 +40,15 @@ statistics in your debug messages.
 =head2 Scaffolding
 
 Scaffolding is very simple with Catalyst.
-Just use Catalyst::Model::CDBI::CRUD as your base class.
 
-    # lib/MyApp/Model/CDBI.pm
-    package MyApp::Model::CDBI;
+The recommended way is to use Catalyst::Helper::Controller::Scaffold.
 
-    use strict;
-    use base 'Catalyst::Model::CDBI::CRUD';
-
-    __PACKAGE__->config(
-        dsn           => 'dbi:SQLite:/tmp/myapp.db',
-        relationships => 1
-    );
+Just install this module, and to scaffold a Class::DBI Model class, do the following:
 
-    1;
+./script/myapp_create controller <name> Scaffold <CDBI::Class>Scaffolding
 
-    # lib/MyApp.pm
-    package MyApp;
 
-    use Catalyst 'FormValidator';
 
-    __PACKAGE__->config(
-        name => 'My Application',
-        root => '/home/joeuser/myapp/root'
-    );
-
-    sub my_table : Global {
-        my ( $self, $c ) = @_;
-        $c->form( optional => [ MyApp::Model::CDBI::Table->columns ] );
-        $c->forward('MyApp::Model::CDBI::Table');
-    }
-
-    1;
-
-Modify the C<$c-E<gt>form()> parameters to match your needs, and don't
-forget to copy the templates into the template root. Can't find the
-templates?  They were in the CRUD model distribution, so you can do
-B<look Catalyst::Model::CDBI::CRUD> from the CPAN shell to find them.
-
-Other Scaffolding modules are in development at the time of writing.
 
 =head2 File uploads
 
@@ -104,16 +74,16 @@ Catalyst Controller module 'upload' action:
         if ( $c->request->parameters->{form_submit} eq 'yes' ) {
 
             if ( my $upload = $c->request->upload('my_file') ) {
-            
+
                 my $filename = $upload->filename;
                 my $target   = "/tmp/upload/$filename";
-                
+
                 unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
                     die( "Failed to copy '$filename' to '$target': $!" );
                 }
             }
         }
-        
+
         $c->stash->{template} = 'file_upload.html';
     }
 
@@ -143,7 +113,7 @@ And in the controller:
                 my $upload   = $c->req->upload($field);
                 my $filename = $upload->filename;
                 my $target   = "/tmp/upload/$filename";
-                
+
                 unless ( $upload->link_to($target) || $upload->copy_to($target) ) {
                     die( "Failed to copy '$filename' to '$target': $!" );
                 }
@@ -165,135 +135,166 @@ displaying this message.
 For more information about uploads and usable methods look at
 L<Catalyst::Request::Upload> and L<Catalyst::Request>.
 
-=head2 Authentication with Catalyst::Plugin::Authentication::CDBI
-
-There are (at least) two ways to implement authentication with this plugin:
-1) only checking username and password;
-2) checking username, password, and the roles the user has
-
-For both variants you'll need the following code in your MyApp package:
-
-    use Catalyst qw/Session::FastMmap Static Authentication::CDBI/;
+=head2 Authentication with Catalyst::Plugin::Authentication
+
+In this example, we'll use the
+L<Catalyst::Plugin::Authentication::Store::DBIC> store and the
+L<Catalyst::Plugin::Authentication::Credential::Password> credentials.
+
+In the lib/MyApp.pm package, we'll need to change the C<use Catalyst;>
+line to include the following modules:
+
+    use Catalyst qw/
+        ConfigLoader 
+        Authentication
+        Authentication::Store::DBIC
+        Authentication::Credential::Password
+        Session
+        Session::Store::FastMmap
+        Session::State::Cookie
+        HTML::Widget
+        Static::Simple
+        /;
+
+The Session, Session::Store::* and Session::State::* modules listed above
+ensure that we stay logged-in across multiple page-views.
+
+In our MyApp.yml configuration file, we'll need to add:
+
+    authentication:
+      dbic:
+        user_class: MyApp::Model::DBIC::User
+        user_field: username
+        password_field: password
+        password_type: hashed
+        password_hash_type: SHA-1
+
+'user_class' is a DBIx::Class package for your users table.
+'user_field' tells which field (column) is used for username lookup.
+'password_field' is the password field in your table.
+The above settings for 'password_type' and 'password_hash_type' ensure that
+the password won't be stored in the database in clear text.
+
+In SQLite, the users table might be something like:
+
+    CREATE TABLE user (
+        id       INTEGER PRIMARY KEY,
+        username VARCHAR(100),
+        password VARCHAR(100)
+    );
 
-    MyApp->config( authentication => { user_class => 'MyApp::M::MyApp::Users',
-                                       user_field => 'email',
-                                       password_field => 'password' });
+Now we need to create a DBIC::SchemaLoader component for this database
+(changing "myapp.db" to wherever your SQLite database is).
 
-'user_class' is a Class::DBI class for your users table.
-'user_field' tells which field is used for username lookup (might be 
-email, first name, surname etc.).
-'password_field' is, well, password field in your table and by default 
-password is stored in plain text. Authentication::CDBI looks for 'user' 
-and 'password' fields in table, if they're not defined in the config.
+    script/myapp_create.pl model DBIC DBIC::SchemaLoader 'dbi:SQLite:myapp.db'
 
-In PostgreSQL, the users table might be something like:
+Now we can start creating our page controllers and templates.
+For our homepage, we create the file "root/index.tt" containing:
 
- CREATE TABLE users (
-   user_id   serial,
-   name      varchar(100),
-   surname   varchar(100),
-   password  varchar(100),
-   email     varchar(100),
-   primary key(user_id)
- );
+    <html>
+    <body>
+    [% IF c.user %]
+        <p>hello [% c.user.username %]</p>
+        <p><a href="[% c.uri_for( '/logout' ) %]">logout</a></p>
+    [% ELSE %]
+        <p><a href="[% c.uri_for( '/login' ) %]">login</a></p>
+    [% END %]
+    </body>
+    </html>
 
-We'll discuss the first variant for now:
-1. user:password login/auth without roles
+If the user is logged in, they will be shown their name, and a logout link.
+Otherwise, they will be shown a login link.
 
-To log in a user you might use an action like this:
+To display the homepage, we can uncomment the C<default> and C<end>
+subroutines in lib/MyApp/Controller/Root.pm and populate them as so:
 
-    sub login : Local {
-        my ($self, $c) = @_;
-        if ($c->req->params->{username}) {
-            $c->session_login($c->req->params->{username}, 
-                              $c->req->params->{password} );
-            if ($c->req->{user}) {
-                $c->forward('/restricted_area');
-            }
-        }
+    sub default : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->stash->{template} = 'index.tt';
     }
 
-This action should not go in your MyApp class...if it does, it will
-conflict with the built-in method of the same name.  Instead, put it
-in a Controller class.
-
-$c->req->params->{username} and $c->req->params->{password} are html 
-form parameters from a login form. If login succeeds, then 
-$c->req->{user} contains the username of the authenticated user.
-
-If you want to remember the user's login status in between further 
-requests, then just use the C<$c-E<gt>session_login> method. Catalyst will 
-create a session id and session cookie and automatically append session 
-id to all urls. So all you have to do is just check $c->req->{user} 
-where needed.
-
-To log out a user, just call $c->session_logout.
+    sub end : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->forward( $c->view('TT') )
+            unless $c->response->body || $c->response->redirect;
+    }
 
-Now let's take a look at the second variant:
-2. user:password login/auth with roles
+The login template is very simple, as L<HTML::Widget> will handle the
+HTML form creation for use. This is saved as "root/login.tt".
 
-To use roles you need to add the following parameters to  MyApp->config in the 'authentication' section:
+    <html>
+    <head>
+    <link href="[% c.uri_for('/static/simple.css') %]" rel="stylesheet" type="text/css">
+    </head>
+    <body>
+    [% result %]
+    </body>
+    </html>
 
-    role_class      => 'MyApp::M::MyApp::Roles',
-    user_role_class => 'MyApp::M::MyApp::UserRoles',
-    user_role_user_field => 'user_id',
-    user_role_role_field => 'role_id',
+For the HTML form to look correct, we also copy the C<simple.css> file
+from the L<HTML::Widget> distribution into our "root/static" folder.
+This file is automatically server by the L<Catalyst::Plugin::Static::Simple>
+module which we loaded in our lib/MyApp.pm package.
 
-Corresponding tables in PostgreSQL could look like this:
+To handle login requests, we first create a controller, like so:
 
- CREATE TABLE roles (
-   role_id  serial,
-   name     varchar(100),
-   primary key(role_id)
- );
+    script/myapp_create.pl controller Login
 
- CREATE TABLE user_roles (
-   user_role_id  serial,
-   user_id       int,
-   role_id       int,
-   primary key(user_role_id),
-   foreign key(user_id) references users(user_id),
-   foreign key(role_id) references roles(role_id)
- );
+In the lib/MyApp/Controller/Login.pm package, we can then uncomment the
+C<default> subroutine, and populate it, as below.
 
-The 'roles' table is a list of role names and the 'user_role' table is 
-used for the user -> role lookup.
+First the widget is created, it needs the 'action' set, and 'username' and
+'password' fields and a submit button added.
 
-Now if a logged-in user wants to see a location which is allowed only 
-for people with an 'admin' role, in your controller you can check it 
-with:
+Then, if we've received a username and password in the request, we attempt
+to login. If successful, we redirect to the homepage; if not the login form
+will be displayed again.
 
-    sub add : Local {
-        my ($self, $c) = @_;
-        if ($c->roles(qw/admin/)) {
-            $c->res->output("Your account has the role 'admin.'");
-        } else {
-            $c->res->output("You're not allowed to be here.");
+    sub default : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->widget->method('POST')->action( $c->uri_for('/login') );
+        $c->widget->element( 'Textfield', 'username' )->label( 'Username' );
+        $c->widget->element( 'Password', 'password' )->label( 'Password' );
+        $c->widget->element( 'Submit' )->value( 'Login' );
+        
+        my $result = $c->widget->process( $c->req );
+        
+        if ( my $user = $result->param('username')
+            and my $pass = $result->param('password') )
+        {    
+            if ( $c->login( $user, $pass ) ) {
+                $c->response->redirect( $c->uri_for( "/" ) );
+                return;
+            }
         }
+        
+        $c->stash->{template} = 'login.tt';
+        $c->stash->{result}   = $result;
     }
 
-One thing you might need is to forward non-authenticated users to a login 
-form if they try to access restricted areas. If you want to do this 
-controller-wide (if you have one controller for your admin section) then it's 
-best to add a user check to a 'begin' action:
+To handle logout's, we create a new controller:
 
-    sub begin : Private {
-        my ($self, $c) = @_;
-        unless ($c->req->{user}) {
-            $c->req->action(undef);  ## notice this!!
-            $c->forward('/user/login');
-        }
+    script/myapp_create.pl controller Logout
+
+Then in the lib/MyApp/Controller/Logout.pm package, we change the
+C<default> subroutine, to logout and then redirect back to the
+homepage.
+
+    sub default : Private {
+        my ( $self, $c ) = @_;
+    
+        $c->logout;
+        
+        $c->response->redirect( $c->uri_for( "/" ) );
     }
 
-Pay attention to $c->req->action(undef). This is needed because of the
-way $c->forward works - C<forward> to C<login> gets called, but after
-that Catalyst will still execute the action defined in the URI (e.g. if
-you tried to go to C</add>, then first 'begin' will forward to 'login',
-but after that 'add' will nonetheless be executed). So
-$c->req->action(undef) undefines any actions that were to be called and
-forwards the user where we want him/her to be.
+Remember that to test this, we would first need to add a user to the
+database, ensuring that the password field is saved as the SHA1 hash
+of our desired password.
 
-And this is all you need to do. 
 
 =head2 Pass-through login (and other actions)
 
@@ -351,7 +352,7 @@ output and  drops the first line if it is an HTTP status line (note:
 this may change).
 
 The Apache C<mod_fastcgi> module is provided by a number of Linux 
-distros and is straightforward to compile for most Unix-like systems.  
+distro's and is straightforward to compile for most Unix-like systems.  
 The module provides a FastCGI Process Manager, which manages FastCGI 
 scripts.  You configure your script as a FastCGI script with the 
 following Apache configuration directives:
@@ -373,7 +374,7 @@ authentication, authorization, and access check phases.
 
 For more information see the FastCGI documentation, the C<FCGI> module 
 and L<http://www.fastcgi.com/>.
+
 =head2 Serving static content
 
 Serving static content in Catalyst can be somewhat tricky; this recipe
@@ -440,7 +441,7 @@ Edit the file and add the following methods:
     # serve all files under /static as static files
     sub default : Path('/static') {
         my ( $self, $c ) = @_;
-    
+
         # Optional, allow the browser to cache the content
         $c->res->headers->header( 'Cache-Control' => 'max-age=86400' );
 
@@ -450,7 +451,7 @@ Edit the file and add the following methods:
     # also handle requests for /favicon.ico
     sub favicon : Path('/favicon.ico') {
         my ( $self, $c ) = @_;
-    
+
         $c->serve_static;
     }
 
@@ -491,7 +492,7 @@ config for this application under mod_perl 1.x:
         use lib qw(/var/www/MyApp/lib);
     </Perl>
     PerlModule MyApp
-    
+
     <VirtualHost *>
         ServerName myapp.example.com
         DocumentRoot /var/www/MyApp/root
@@ -525,8 +526,8 @@ the Catalyst Request object:
   $c->req->args([qw/arg1 arg2 arg3/]);
   $c->forward('/wherever');
 
-(See L<Catalyst::Manual::Intro#Flow_Control> for more information on
-passing arguments via C<forward>.)
+(See the L<Catalyst::Manual::Intro> Flow_Control section for more 
+information on passing arguments via C<forward>.)
 
 =head2 Configure your application
 
@@ -553,16 +554,6 @@ Now create C<myapp.yml> in your application home:
   # DO NOT USE TABS FOR INDENTATION OR label/value SEPARATION!!!
   name:     MyApp
 
-  # authentication; perldoc Catalyst::Plugin::Authentication::CDBI
-  authentication:
-    user_class:           'MyApp::M::MyDB::Customer'
-    user_field:           'username'
-    password_field:       'password'
-    password_hash:        'md5'
-    role_class:           'MyApp::M::MyDB::Role'
-    user_role_class:      'MyApp::M::MyDB::PersonRole'
-    user_role_user_field: 'person'
-
   # session; perldoc Catalyst::Plugin::Session::FastMmap
   session:
     expires:        '3600'
@@ -594,27 +585,31 @@ This is equivalent to:
 
 See also L<YAML>.
 
-=head2 Using existing CDBI (etc.) classes with Catalyst
+=head2 Using existing DBIC (etc.) classes with Catalyst
 
 Many people have existing Model classes that they would like to use with
 Catalyst (or, conversely, they want to write Catalyst models that can be
 used outside of Catalyst, e.g.  in a cron job). It's trivial to write a
 simple component in Catalyst that slurps in an outside Model:
 
-    package MyApp::M::Catalog;
-    use base qw/Catalyst::Base Some::Other::CDBI::Module::Catalog/;
+    package MyApp::Model::DB;
+    use base qw/Catalyst::Model::DBIC::Schema/;
+    __PACKAGE__->config(
+        schema_class => 'Some::DBIC::Schema',
+        connect_info => ['dbi:SQLite:foo.db', '', '', {AutoCommit=>1}];
+    );
     1;
 
-and that's it! Now C<Some::Other::CDBI::Module::Catalog> is part of your
-Cat app as C<MyApp::M::Catalog>.
+and that's it! Now C<Some::DBIC::Schema> is part of your
+Cat app as C<MyApp::Model::DB>.
 
 =head2 Delivering a Custom Error Page
 
 By default, Catalyst will display its own error page whenever it
 encounters an error in your application. When running under C<-Debug>
 mode, the error page is a useful screen including the error message and
-a full Data::Dumper output of the C<$c> context object. When not in
-C<-Debug>, users see a simple "Please come back later" screen.
+L<Data::Dump> output of the relevant parts of the C<$c> context object. 
+When not in C<-Debug>, users see a simple "Please come back later" screen.
 
 To use a custom error page, use a special C<end> method to short-circuit
 the error processing. The following is an example; you might want to
@@ -624,21 +619,21 @@ C<end> method; see L<Catalyst::Plugin::FillInForm>).
 
     sub end : Private {
         my ( $self, $c ) = @_;
-    
+
         if ( scalar @{ $c->error } ) {
             $c->stash->{errors}   = $c->error;
             $c->stash->{template} = 'errors.tt';
             $c->forward('MyApp::View::TT');
-            $c->{error} = [];
+            $c->error(0);
         }
-    
+
         return 1 if $c->response->status =~ /^3\d\d$/;
         return 1 if $c->response->body;
-    
+
         unless ( $c->response->content_type ) {
             $c->response->content_type('text/html; charset=utf-8');
         }
-    
+
         $c->forward('MyApp::View::TT');
     }
 
@@ -646,6 +641,164 @@ You can manually set errors in your code to trigger this page by calling
 
     $c->error( 'You broke me!' );
 
+=head2 Require user logins
+
+It's often useful to restrict access to your application to a set of
+registered users, forcing everyone else to the login page until they're
+signed in.
+
+To implement this in your application make sure you have a customer
+table with username and password fields and a corresponding Model class
+in your Catalyst application, then make the following changes:
+
+=head3 lib/MyApp.pm
+
+  use Catalyst qw/
+      Authentication
+      Authentication::Store::DBIC
+      Authentication::Credential::Password
+  /;
+
+  __PACKAGE__->config->{authentication}->{dbic} = {
+    'user_class'        => 'My::Model::DBIC::User',
+    'user_field'        => 'username',
+    'password_field'    => 'password'
+    'password_type'     => 'hashed',
+    'password_hash_type'=> 'SHA-1'
+  };
+
+  sub auto : Private {
+    my ($self, $c) = @_;
+    my $login_path = 'user/login';
+
+    # allow people to actually reach the login page!
+    if ($c->request->path eq $login_path) {
+      return 1;
+    }
+
+    # if a user doesn't exist, force login
+    if ( !$c->user_exists ) {
+      # force the login screen to be shown
+      $c->response->redirect($c->request->base . $login_path);
+    }
+
+    # otherwise, we have a user - continue with the processing chain
+    return 1;
+  }
+
+=head3 lib/MyApp/C/User.pm
+
+  sub login : Path('/user/login') {
+    my ($self, $c) = @_;
+
+    # default template
+    $c->stash->{'template'} = "user/login.tt";
+    # default form message
+    $c->stash->{'message'} = 'Please enter your username and password';
+
+    if ( $c->request->param('username') ) {
+      # try to log the user in
+      # login() is provided by ::Authentication::Credential::Password
+      if( $c->login(
+        $c->request->param('username'),
+        $c->request->param('password'),
+        );
+
+        # if login() returns 1, user is now logged in
+        $c->response->redirect('/some/page');
+      }
+
+      # otherwise we failed to login, try again!
+      $c->stash->{'message'} = 
+         'Unable to authenticate the login details supplied';
+    }
+  }
+
+  sub logout : Path('/user/logout') {
+    my ($self, $c) = @_;
+    # log the user out
+    $c->logout;
+
+    # do the 'default' action
+    $c->response->redirect($c->request->base);
+  }
+
+
+=head3 root/base/user/login.tt
+
+ [% INCLUDE header.tt %]
+ <form action="/user/login" method="POST" name="login_form">
+    [% message %]<br />
+    <label for="username">username:</label><br />
+    <input type="text" id="username" name="username" /><br />
+
+    <label for="password">password:</label><br />
+    <input type="password" id="password" name="password" /><br />
+
+    <input type="submit" value="log in" name="form_submit" />
+  </form>
+  [% INCLUDE footer.tt %]
+
+=head2 Role-based Authorization
+
+For more advanced access control, you may want to consider using role-based
+authorization. This means you can assign different roles to each user, e.g.
+"user", "admin", etc.
+
+The C<login> and C<logout> methods and view template are exactly the same as
+in the previous example.
+
+The L<Catalyst::Plugin::Authorization::Roles> plugin is required when
+implementing roles:
+
+ use Catalyst qw/
+    Authentication
+    Authentication::Credential::Password
+    Authentication::Store::Htpasswd
+    Authorization::Roles
+  /;
+
+Roles are implemented automatically when using
+L<Catalyst::Authentication::Store::Htpasswd>:
+
+  # no additional role configuration required
+  __PACKAGE__->config->{authentication}{htpasswd} = "passwdfile";
+
+Or can be set up manually when using L<Catalyst::Authentication::Store::DBIC>:
+
+  # Authorization using a many-to-many role relationship
+  __PACKAGE__->config->{authorization}{dbic} = {
+    'role_class'           => 'My::Model::DBIC::Role',
+    'role_field'           => 'name',
+    'user_role_user_field' => 'user',
+
+    # DBIx::Class only (omit if using Class::DBI)
+    'role_rel'             => 'user_role',
+
+    # Class::DBI only, (omit if using DBIx::Class)
+    'user_role_class'      => 'My::Model::CDBI::UserRole'
+    'user_role_role_field' => 'role',
+  };
+
+To restrict access to any action, you can use the C<check_user_roles> method:
+
+  sub restricted : Local {
+     my ( $self, $c ) = @_;
+
+     $c->detach("unauthorized")
+       unless $c->check_user_roles( "admin" );
+
+     # do something restricted here
+  }
+
+You can also use the C<assert_user_roles> method. This just gives an error if
+the current user does not have one of the required roles:
+
+  sub also_restricted : Global {
+    my ( $self, $c ) = @_;
+    $c->assert_user_roles( qw/ user admin / );
+  }
+
 =head1 AUTHOR
 
 Sebastian Riedel, C<sri@oook.de>
@@ -655,6 +808,9 @@ Marcus Ramberg, C<mramberg@cpan.org>
 Jesse Sheidlower, C<jester@panix.com>
 Andy Grundman, C<andy@hybridized.org> 
 Chisel Wright, C<pause@herlpacker.co.uk>
+Will Hawes, C<info@whawes.co.uk>
+Gavin Henry, C<ghenry@cpan.org> (Spell checking)
+
 
 =head1 COPYRIGHT