X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=catagits%2FCatalyst-Manual.git;a=blobdiff_plain;f=lib%2FCatalyst%2FManual%2FTutorial%2FAuthentication.pod;fp=lib%2FCatalyst%2FManual%2FTutorial%2FAuthentication.pod;h=0000000000000000000000000000000000000000;hp=0d7e7ab01c2a23ad7516e299b19501423620216a;hb=3ab6187c1a123983b6ae29e57f543328ce15755c;hpb=418ded01713d313bdc03308ced5b0cc408682e24 diff --git a/lib/Catalyst/Manual/Tutorial/Authentication.pod b/lib/Catalyst/Manual/Tutorial/Authentication.pod deleted file mode 100644 index 0d7e7ab..0000000 --- a/lib/Catalyst/Manual/Tutorial/Authentication.pod +++ /dev/null @@ -1,916 +0,0 @@ -=head1 NAME - -Catalyst::Manual::Tutorial::Authentication - Catalyst Tutorial - Chapter 5: Authentication - - -=head1 OVERVIEW - -This is B for the Catalyst tutorial. - -L - -=over 4 - -=item 1 - -L - -=item 2 - -L - -=item 3 - -L - -=item 4 - -L - -=item 5 - -B - -=item 6 - -L - -=item 7 - -L - -=item 8 - -L - -=item 9 - -L - -=item 10 - -L - -=back - - -=head1 DESCRIPTION - -Now that we finally have a simple yet functional application, we can -focus on providing authentication (with authorization coming next in -Chapter 6). - -This chapter of the tutorial is divided into two main sections: 1) basic, -cleartext authentication and 2) hash-based authentication. - -You can checkout the source code for this example from the catalyst -subversion repository as per the instructions in -L. - - -=head1 BASIC AUTHENTICATION - -This section explores how to add authentication logic to a Catalyst -application. - - -=head2 Add Users and Roles to the Database - -First, we add both user and role information to the database (we will -add the role information here although it will not be used until the -authorization section, Chapter 6). Create a new SQL script file by opening -C in your editor and insert: - - -- - -- Add user and role tables, along with a many-to-many join table - -- - CREATE TABLE user ( - id INTEGER PRIMARY KEY, - username TEXT, - password TEXT, - email_address TEXT, - first_name TEXT, - last_name TEXT, - active INTEGER - ); - CREATE TABLE role ( - id INTEGER PRIMARY KEY, - role TEXT - ); - CREATE TABLE user_role ( - user_id INTEGER, - role_id INTEGER, - PRIMARY KEY (user_id, role_id) - ); - -- - -- Load up some initial test data - -- - INSERT INTO user VALUES (1, 'test01', 'mypass', 't01@na.com', 'Joe', 'Blow', 1); - INSERT INTO user VALUES (2, 'test02', 'mypass', 't02@na.com', 'Jane', 'Doe', 1); - INSERT INTO user VALUES (3, 'test03', 'mypass', 't03@na.com', 'No', 'Go', 0); - INSERT INTO role VALUES (1, 'user'); - INSERT INTO role VALUES (2, 'admin'); - INSERT INTO user_role VALUES (1, 1); - INSERT INTO user_role VALUES (1, 2); - INSERT INTO user_role VALUES (2, 1); - INSERT INTO user_role VALUES (3, 1); - -Then load this into the C database with the following command: - - $ sqlite3 myapp.db < myapp02.sql - -=head2 Add User and Role Information to DBIC Schema - -Although we could manually edit the DBIC schema information to include -the new tables added in the previous step, let's use the C -option on the DBIC model helper to do most of the work for us: - - $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \ - create=static components=TimeStamp dbi:SQLite:myapp.db - exists "/root/dev/MyApp/script/../lib/MyApp/Model" - exists "/root/dev/MyApp/script/../t" - Dumping manual schema for MyApp::Schema to directory /root/dev/MyApp/script/../lib ... - Schema dump completed. - exists "/root/dev/MyApp/script/../lib/MyApp/Model/DB.pm" - $ - $ ls lib/MyApp/Schema/Result - Author.pm BookAuthor.pm Book.pm Role.pm User.pm UserRole.pm - -Notice how the helper has added three new table-specific result source -files to the C directory. And, more -importantly, even if there were changes to the existing result source -files, those changes would have only been written above the C<# DO NOT -MODIFY THIS OR ANYTHING ABOVE!> comment and your hand-edited -enhancements would have been preserved. - -Speaking of "hand-editted enhancements," we should now add -relationship information to the three new result source files. Edit -each of these files and add the following information between the C<# -DO NOT MODIFY THIS OR ANYTHING ABOVE!> comment and the closing C<1;>: - -C: - - # - # Set relationships: - # - - # has_many(): - # args: - # 1) Name of relationship, DBIC will create accessor with this name - # 2) Name of the model class referenced by this relationship - # 3) Column name in *foreign* table (aka, foreign key in peer table) - __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::Result::UserRole', 'user_id'); - - # many_to_many(): - # args: - # 1) Name of relationship, DBIC will create accessor with this name - # 2) Name of has_many() relationship this many_to_many() is shortcut for - # 3) Name of belongs_to() relationship in model class of has_many() above - # You must already have the has_many() defined to use a many_to_many(). - __PACKAGE__->many_to_many(roles => 'map_user_role', 'role'); - - -C: - - # - # Set relationships: - # - - # has_many(): - # args: - # 1) Name of relationship, DBIC will create accessor with this name - # 2) Name of the model class referenced by this relationship - # 3) Column name in *foreign* table (aka, foreign key in peer table) - __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::Result::UserRole', 'role_id'); - - -C: - - # - # Set relationships: - # - - # belongs_to(): - # args: - # 1) Name of relationship, DBIC will create accessor with this name - # 2) Name of the model class referenced by this relationship - # 3) Column name in *this* table - __PACKAGE__->belongs_to(user => 'MyApp::Schema::Result::User', 'user_id'); - - # belongs_to(): - # args: - # 1) Name of relationship, DBIC will create accessor with this name - # 2) Name of the model class referenced by this relationship - # 3) Column name in *this* table - __PACKAGE__->belongs_to(role => 'MyApp::Schema::Result::Role', 'role_id'); - -The code for these three sets of updates is obviously very similar to -the edits we made to the C, C, and C -classes created in Chapter 3. - -Note that we do not need to make any change to the -C schema file. It simply tells DBIC to load all -of the Result Class and ResultSet Class files it finds in below the -C directory, so it will automatically pick up our -new table information. - - -=head2 Sanity-Check Reload of Development Server - -We aren't ready to try out the authentication just yet; we only want -to do a quick check to be sure our model loads correctly. Press -C to kill the previous server instance (if it's still running) -and restart it: - - $ script/myapp_server.pl - -Look for the three new model objects in the startup debug output: - - ... - .-------------------------------------------------------------------+----------. - | Class | Type | - +-------------------------------------------------------------------+----------+ - | MyApp::Controller::Books | instance | - | MyApp::Controller::Root | instance | - | MyApp::Model::DB | instance | - | MyApp::Model::DB::Author | class | - | MyApp::Model::DB::Book | class | - | MyApp::Model::DB::BookAuthor | class | - | MyApp::Model::DB::Role | class | - | MyApp::Model::DB::User | class | - | MyApp::Model::DB::UserRole | class | - | MyApp::View::TT | instance | - '-------------------------------------------------------------------+----------' - ... - -Again, notice that your "Result Class" classes have been "re-loaded" -by Catalyst under C. - - -=head2 Include Authentication and Session Plugins - -Edit C and update it as follows (everything below -C is new): - - # Load plugins - use Catalyst qw/-Debug - ConfigLoader - Static::Simple - - StackTrace - - Authentication - - Session - Session::Store::FastMmap - Session::State::Cookie - /; - -B As discussed in MoreCatalystBasics, different versions of -C have used a variety of methods to load the plugins. -You can put the plugins in the C statement if you prefer. - -The C plugin supports Authentication while the -C plugins are required to maintain state across multiple HTTP -requests. - -Note that the only required Authentication class is the main one. This -is a change that occurred in version 0.09999_01 of the -C plugin. You B to specify a particular -Authentication::Store or Authentication::Credential plugin. Instead, -indicate the Store and Credential you want to use in your application -configuration (see below). - -Make sure you include the additional plugins as new dependencies in -the Makefile.PL file something like this: - - requires ( - 'Catalyst::Plugin::Authentication' => '0', - 'Catalyst::Plugin::Session' => '0', - 'Catalyst::Plugin::Session::Store::FastMmap' => '0', - 'Catalyst::Plugin::Session::State::Cookie' => '0', - ); - -Note that there are several options for -L -(L -is generally a good choice if you are on Unix; try -L if you -are on Win32) -- consult -L and its subclasses -for additional information and options (for example to use a database- -backed session store). - - -=head2 Configure Authentication - -There are a variety of ways to provide configuration information to -L. -Here we will use -L -because it automatically sets a reasonable set of defaults for us. Open -C and place the following text above the call to -C<__PACKAGE__-Esetup();>: - - # Configure SimpleDB Authentication - __PACKAGE__->config->{'Plugin::Authentication'} = { - default => { - class => 'SimpleDB', - user_model => 'DB::User', - password_type => 'clear', - }, - }; - -We could have placed this configuration in C, but placing -it in C is probably a better place since it's not likely -something that users of your application will want to change during -deployment (or you could use a mixture: leave C and -C defined in C as we show above, but place -C in C to allow the type of password to be -easily modified during deployment). We will stick with putting -all of the authentication-related configuration in C -for the tutorial, but if you wish to use C, just convert -to the following code: - - - use_session 1 - - password_type self_check - user_model DB::User - class SimpleDB - - - -B Here is a short script that will dump the contents of -Cconfig> to L format in -C: - - $ perl -Ilib -e 'use MyApp; use Config::General; - Config::General->new->save_file("myapp.conf", MyApp->config);' - -B Because we are using SimpleDB along with a database layout -that complies with its default assumptions, we don't need to specify -the names of the columns where our username and password information -is stored (hence, the "Simple" part of "SimpleDB"). That being said, -SimpleDB lets you specify that type of information if you need to. -Take a look at -C -for details. - - -=head2 Add Login and Logout Controllers - -Use the Catalyst create script to create two stub controller files: - - $ script/myapp_create.pl controller Login - $ script/myapp_create.pl controller Logout - -You could easily use a single controller here. For example, you could -have a C controller with both C and C actions. -Remember, Catalyst is designed to be very flexible, and leaves such -matters up to you, the designer and programmer. - -Then open C, locate the -C method (or C if you -are using an older version of Catalyst) that was automatically -inserted by the helpers when we created the Login controller above, -and update the definition of C to match: - - =head2 index - - Login logic - - =cut - - sub index :Path :Args(0) { - my ($self, $c) = @_; - - # Get the username and password from form - my $username = $c->request->params->{username} || ""; - my $password = $c->request->params->{password} || ""; - - # If the username and password values were found in form - if ($username && $password) { - # Attempt to log the user in - if ($c->authenticate({ username => $username, - password => $password } )) { - # If successful, then let them use the application - $c->response->redirect($c->uri_for( - $c->controller('Books')->action_for('list'))); - return; - } else { - # Set an error message - $c->stash->{error_msg} = "Bad username or password."; - } - } - - # If either of above don't work out, send to the login page - $c->stash->{template} = 'login.tt2'; - } - -Be sure to remove the C<$c-Eresponse-Ebody('Matched MyApp::Controller::Login in Login.');> -line of the C. - -This controller fetches the C and C values from the -login form and attempts to authenticate the user. If successful, it -redirects the user to the book list page. If the login fails, the user -will stay at the login page and receive an error message. If the -C and C values are not present in the form, the -user will be taken to the empty login form. - -Note that we could have used something like "C", -however, it is generally recommended (partly for historical reasons, -and partly for code clarity) only to use C in -C, and then mainly to generate the 404 not -found page for the application. - -Instead, we are using "C" here to -specifically match the URL C. C actions (aka, "literal -actions") create URI matches relative to the namespace of the -controller where they are defined. Although C supports -arguments that allow relative and absolute paths to be defined, here -we use an empty C definition to match on just the name of the -controller itself. The method name, C, is arbitrary. We make -the match even more specific with the C<:Args(0)> action modifier -- -this forces the match on I C, not -C. - -Next, update the corresponding method in -C to match: - - =head2 index - - Logout logic - - =cut - - sub index :Path :Args(0) { - my ($self, $c) = @_; - - # Clear the user's state - $c->logout; - - # Send the user to the starting point - $c->response->redirect($c->uri_for('/')); - } - -As with the login controller, be sure to delete the -C<$c-Eresponse-Ebody('Matched MyApp::Controller::Logout in Logout.');> -line of the C. - - -=head2 Add a Login Form TT Template Page - -Create a login form by opening C and inserting: - - [% META title = 'Login' %] - - -
- - - - - - - - - - - - -
Username:
Password:
-
- - -=head2 Add Valid User Check - -We need something that provides enforcement for the authentication -mechanism -- a I mechanism that prevents users who have not -passed authentication from reaching any pages except the login page. -This is generally done via an C action/method (prior to Catalyst -v5.66, this sort of thing would go in C, but starting in -v5.66, the preferred location is C). - -Edit the existing C class file and insert -the following method: - - =head2 auto - - Check if there is a user and, if not, forward to login page - - =cut - - # Note that 'auto' runs after 'begin' but before your actions and that - # 'auto's "chain" (all from application path to most specific class are run) - # See the 'Actions' section of 'Catalyst::Manual::Intro' for more info. - sub auto : Private { - my ($self, $c) = @_; - - # Allow unauthenticated users to reach the login page. This - # allows unauthenticated users to reach any action in the Login - # controller. To lock it down to a single action, we could use: - # if ($c->action eq $c->controller('Login')->action_for('index')) - # to only allow unauthenticated access to the 'index' action we - # added above. - if ($c->controller eq $c->controller('Login')) { - return 1; - } - - # If a user doesn't exist, force login - if (!$c->user_exists) { - # Dump a log message to the development server debug output - $c->log->debug('***Root::auto User not found, forwarding to /login'); - # Redirect the user to the login page - $c->response->redirect($c->uri_for('/login')); - # Return 0 to cancel 'post-auto' processing and prevent use of application - return 0; - } - - # User found, so return 1 to continue with processing after this 'auto' - return 1; - } - -As discussed in -L, -every C method from the application/root controller down to the -most specific controller will be called. By placing the -authentication enforcement code inside the C method of -C (or C), it will be -called for I request that is received by the entire -application. - - -=head2 Displaying Content Only to Authenticated Users - -Let's say you want to provide some information on the login page that -changes depending on whether the user has authenticated yet. To do -this, open C in your editor and add the following -lines to the bottom of the file: - - ... -

- [% - # This code illustrates how certain parts of the TT - # template will only be shown to users who have logged in - %] - [% IF c.user_exists %] - Please Note: You are already logged in as '[% c.user.username %]'. - You can logout here. - [% ELSE %] - You need to log in to use this application. - [% END %] - [%# - Note that this whole block is a comment because the "#" appears - immediate after the "[%" (with no spaces in between). Although it - can be a handy way to temporarily "comment out" a whole block of - TT code, it's probably a little too subtle for use in "normal" - comments. - %] -

- -Although most of the code is comments, the middle few lines provide a -"you are already logged in" reminder if the user returns to the login -page after they have already authenticated. For users who have not yet -authenticated, a "You need to log in..." message is displayed (note the -use of an IF-THEN-ELSE construct in TT). - - -=head2 Try Out Authentication - -Press C to kill the previous server instance (if it's still -running) and restart it: - - $ script/myapp_server.pl - -B If you are having issues with authentication on -Internet Explorer, be sure to check the system clocks on both your -server and client machines. Internet Explorer is very picky about -timestamps for cookies. You can quickly sync a Debian system by -installing the "ntpdate" package: - - sudo aptitude -y install ntpdate - -And then run the following command: - - sudo ntpdate-debian - -Or, depending on your firewall configuration: - - sudo ntpdate-debian -u - -Note: NTP can be a little more finicky about firewalls because it uses -UDP vs. the more common TCP that you see with most Internet protocols. -Worse case, you might have to manually set the time on your development -box instead of using NTP. - -Now trying going to L and you should -be redirected to the login page, hitting Shift+Reload or Ctrl+Reload -if necessary (the "You are already logged in" message should I -appear -- if it does, click the C button and try again). Note -the C<***Root::auto User not found...> debug message in the -development server output. Enter username C and password -C, and you should be taken to the Book List page. - -Open C and add the following lines to the -bottom (below the closing tag): - -

- Login - Create -

- -Reload your browser and you should now see a "Login" and "Create" links -at the bottom of the page (as mentioned earlier, you can update template -files without reloading the development server). Click the first link -to return to the login page. This time you I see the "You are -already logged in" message. - -Finally, click the C link on the C page. -You should stay at the login page, but the message should change to "You -need to log in to use this application." - - -=head1 USING PASSWORD HASHES - -In this section we increase the security of our system by converting -from cleartext passwords to SHA-1 password hashes that include a -random "salt" value to make them extremely difficult to crack with -dictionary and "rainbow table" attacks. - -B This section is optional. You can skip it and the rest of the -tutorial will function normally. - -Be aware that even with the techniques shown in this section, the browser -still transmits the passwords in cleartext to your application. We are -just avoiding the I of cleartext passwords in the database by -using a salted SHA-1 hash. If you are concerned about cleartext passwords -between the browser and your application, consider using SSL/TLS, made -easy with the Catalyst plugin Catalyst::Plugin:RequireSSL. - - -=head2 Install DBIx::Class::EncodedColumn - -L provides features -that can greatly simplify the maintenance of passwords. It's currently -not available as a .deb package in the normal Debian repositories, so let's -install it directly from CPAN: - - $ sudo cpan DBIx::Class::EncodedColumn - - -=head2 Re-Run the DBIC::Schema Model Helper to Include DBIx::Class::EncodedColumn - -Next, we can re-run the model helper to have it include -L in all of the -Result Classes it generates for us. Simply use the same command we -saw in Chapters 3 and 4, but add C<,EncodedColumn> to the C -argument: - - $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \ - create=static components=TimeStamp,EncodedColumn dbi:SQLite:myapp.db - -If you then open one of the Result Classes, you will see that it -includes EncodedColumn in the C line. Take a look at -C since that's the main class where we -want to use hashed and salted passwords: - - __PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp", "EncodedColumn", "Core"); - - -=head2 Modify the "password" Column to Use EncodedColumn - -Open the file C and enter the following -text below the "# DO NOT MODIFY THIS OR ANYTHING ABOVE!" line but above -the closing "1;": - - # Have the 'password' column use a SHA-1 hash and 10-character salt - # with hex encoding; Generate the 'check_password" method - __PACKAGE__->add_columns( - 'password' => { - data_type => "TEXT", - size => undef, - encode_column => 1, - encode_class => 'Digest', - encode_args => {salt_length => 10}, - encode_check_method => 'check_password', - }, - ); - -This redefines the automatically generated definition for the password -fields at the top of the Result Class file to now use EncodedColumn -logic (C is set to 1). C can be set to -either C to use -L, -or C for -L. -C is then used to customize the type of Digest you -selected. Here we only specified the size of the salt to use, but -we could have also modified the hashing algorithm ('SHA-256' is -the default) and the format to use ('base64' is the default, but -'hex' and 'binary' are other options). To use these, you could -change the C to something like: - - encode_args => {algorithm => 'SHA-1', - format => 'hex', - salt_length => 10}, - - -=head2 Load Hashed Passwords in the Database - -Next, let's create a quick script to load some hashed and salted passwords -into the C column of our C table. Open the file -C in your editor and enter the following text: - - #!/usr/bin/perl - - use strict; - use warnings; - - use MyApp::Schema; - - my $schema = MyApp::Schema->connect('dbi:SQLite:myapp.db'); - - my @users = $schema->resultset('User')->all; - - foreach my $user (@users) { - $user->password('mypass'); - $user->update; - } - -EncodedColumn lets us simple call C<$user->check_password($password)> -to see if the user has supplied the correct password, or, as we show -above, call C<$user->update($new_password)> to update the hashed -password stored for this user. - -Then run the following command: - - $ perl -Ilib set_hashed_passwords.pl - -We had to use the C<-Ilib> arguement to tell perl to look under the -C directory for our C model. - -Then dump the users table to verify that it worked: - - $ sqlite3 myapp.db "select * from user" - 1|test01|38d3974fa9e9263099f7bc2574284b2f55473a9bM=fwpX2NR8|t01@na.com|Joe|Blow|1 - 2|test02|6ed8586587e53e0d7509b1cfed5df08feadc68cbMJlnPyPt0I|t02@na.com|Jane|Doe|1 - 3|test03|af929a151340c6aed4d54d7e2651795d1ad2e2f7UW8dHoGv9z|t03@na.com|No|Go|0 - -As you can see, the passwords are much harder to steal from the -database. Also note that this demonstrates how to use a DBIx::Class -model outside of your web application -- a very useful feature in many -situations. - - -=head2 Enable Hashed and Salted Passwords - -Edit C and update it to match the following text (the only change -is to the C field): - - # Configure SimpleDB Authentication - __PACKAGE__->config->{'Plugin::Authentication'} = { - default => { - class => 'SimpleDB', - user_model => 'DB::User', - password_type => 'self_check', - }, - }; - -The use of C will cause -Catalyst::Plugin::Authentication::Store::DBIC to call the -C method we enabled on our C columns. - - -=head2 Try Out the Hashed Passwords - -Press C to kill the previous server instance (if it's still -running) and restart it: - - $ script/myapp_server.pl - -You should now be able to go to L and -login as before. When done, click the "logout" link on the login page -(or point your browser at L). - - -=head1 USING THE SESSION FOR FLASH - -As discussed in the previous chapter of the tutorial, C allows -you to set variables in a way that is very similar to C, but it -will remain set across multiple requests. Once the value is read, it -is cleared (unless reset). Although C has nothing to do with -authentication, it does leverage the same session plugins. Now that -those plugins are enabled, let's go back and update the "delete and -redirect with query parameters" code seen at the end of the L chapter of the tutorial to -take advantage of C. - -First, open C and modify C -to match the following (everything after the model search line of code -has changed): - - =head2 delete - - Delete a book - - =cut - - sub delete :Chained('object') :PathPart('delete') :Args(0) { - my ($self, $c) = @_; - - # Use the book object saved by 'object' and delete it along - # with related 'book_authors' entries - $c->stash->{object}->delete; - - # Use 'flash' to save information across requests until it's read - $c->flash->{status_msg} = "Book deleted"; - - # Redirect the user back to the list page - $c->response->redirect($c->uri_for($self->action_for('list'))); - } - -Next, open C and update the TT code to pull from -flash vs. the C query parameter: - - ... -
- [%# Status and error messages %] - [% status_msg || c.flash.status_msg %] - [% error_msg %] - [%# This is where TT will stick all of your template's contents. -%] - [% content %] -
- ... - -Although the sample above only shows the C div, leave the -rest of the file intact -- the only change we made to the C -was to add "C<|| c.request.params.status_msg>" to the -Cspan class="message"E> line. - - -=head2 Try Out Flash - -Restart the development server, log in, and then point your browser to -L to create an extra -several books. Click the "Return to list" link and delete one of the -"Test" books you just added. The C mechanism should retain our -"Book deleted" status message across the redirect. - -B While C will save information across multiple requests, -I. In general, this is -exactly what you want -- the C message will get displayed on -the next screen where it's appropriate, but it won't "keep showing up" -after that first time (unless you reset it). Please refer to -L for additional -information. - - -=head2 Switch To Flash-To-Stash - -Although the a use of flash above works well, the -C statement is a little ugly. A nice -alternative is to use the C feature that automatically -copies the content of flash to stash. This makes your controller -and template code work regardless of where it was directly access, a -forward, or a redirect. To enable C, you can either -set the value in C by changing the default -C<__PACKAGE__-Econfig> setting to something like: - - __PACKAGE__->config( - name => 'MyApp', - session => {flash_to_stash => 1} - ); - -B add the following to C: - - - flash_to_stash 1 - - -The C<__PACKAGE__-Econfig> option is probably preferable here -since it's not something you will want to change at runtime without it -possibly breaking some of your code. - -Then edit C and change the C line -to match the following: - - [% status_msg %] - -Restart the development server and go to -L in your browser. Delete another -of the "Test" books you added in the previous step. Flash should still -maintain the status message across the redirect even though you are no -longer explicitly accessing C. - - -=head1 AUTHOR - -Kennedy Clark, C - -Please report any errors, issues or suggestions to the author. The -most recent version of the Catalyst Tutorial can be found at -L. - -Copyright 2006-2008, Kennedy Clark, under Creative Commons License -(L).