package CGI::Application::Framework; use strict; use warnings; =head1 NAME CGI::Application::Framework - Fully-featured MVC web application platform =head1 VERSION Version 0.26 =cut our $VERSION = '0.26'; use base qw / CGI::Application Exporter /; use vars qw / @EXPORT_OK $AUTOLOAD /; @EXPORT_OK = qw ( load_tmpl session redirect make_link AUTOLOAD make_self_url get_session_id ); use Carp; use Data::Dumper; use CGI::Application::Framework::Constants qw ( SESSION_FIRST_TIME SESSION_IN_COOKIE SESSION_IN_HIDDEN_FORM_FIELD SESSION_IN_URL SESSION_MISSING ); use CGI::Application::Plugin::ValidateRM qw / check_rm /; use CGI::Application::Plugin::Config::Context; use CGI::Application::Plugin::AnyTemplate; use CGI::Application::Plugin::LogDispatch; use Log::Dispatch::Config (); use Apache::SessionX; use CGI::Enurl (); use Digest::MD5; use Time::HiRes; use File::Basename qw ( fileparse ); use File::Spec; use Cwd 'abs_path'; use Module::Load; __PACKAGE__->add_callback('init', '_framework_init'); __PACKAGE__->add_callback('postrun', '_framework_postrun'); __PACKAGE__->add_callback('prerun', '_framework_prerun'); __PACKAGE__->add_callback('template_pre_process', '_framework_template_pre_process'); __PACKAGE__->add_callback('template_post_process', '_framework_template_post_process'); =head1 NOTE This is alpha-quality software which is being continually developed and refactored. Various APIs B change in future releases. There are bugs. There are missing features. Feedback and assistance welcome! =head1 SYNOPSIS =head2 Application Layout A C project has the following layout: /cgi-bin/ app.cgi # The CGI script /framework/ framework.conf # Global CAF config file projects/ MyProj/ framework.conf # MyProj config file common-templates/ # login templates go here common-modules/ CDBI/ MyProj.pm # The Class::DBI project module MyProj/ app.pm # The Class::DBI application module MyProj.pm # The project module applications/ framework.conf # "All applications" config file myapp1/ framework.conf # myapp1 config file myapp1.pm # An application module templates/ runmode_one.html # templates for myapp1 runmode_two.html myapp2/ framework.conf # myapp2 config file myapp2.pm # An application module templates/ runmode_one.html # templates for myapp2 runmode_two.html =head2 The CGI Script You call your application with an URL like: http://www.example.com/cgi-bin/app.cgi/MyProj/myapp By default, CAF applications are divided first into C and then into C. Based on the URL above, the Project is called C and the application within that project is called C. The actual CGI script (C) is tiny. It looks like this: #!/usr/bin/perl use strict; use CGI::Application::Framework; use CGI::Carp 'fatalsToBrowser'; CGI::Application::Framework->run_app( projects => '../caf/projects', ); =head2 An application module Your application module (C) looks like a standard C, with some extra features enabled. package myapp1; use strict; use warnings; use base qw ( MyProj ); sub setup { my $self = shift; $self->run_modes([qw( runmode_one )]); $self->start_mode('runmode_one'); } sub rumode_one { my $self = shift; $self->template->fill({ name => $self->session->{user}->fullname, }); } =head2 A template The template is named (by default) after the run mode that called it. In this case, it's C:

Welcome

Hello there, =head2 The project module Your common project module (MyProj.pm) contains a lot of code, but most of it can be copied directly from the examples: package MyProj; use warnings; use strict; use base 'CGI::Application::Framework'; # Called to determine if the user filled in the login form correctly sub _login_authenticate { my $self = shift; my $username = $self->query->param('username'); my $password = $self->query->param('password'); my ($user) = CDBI::Project::app::Users->search( username => $username ); if ($password eq $user->password) { return(1, $user); } return; } # Called to determine if the user filled in the re-login form correctly sub _relogin_authenticate { my $self = shift; my $password = $self->query->param('password'); my $user = CDBI::Project::app::Users->retrieve( $self->session->{uid} ); if ($password eq $user->password) { return(1, $user); } return; } # _login_profile and _relogin_profile are # definitions for Data::FormValidate, as needed by # CGI::Application::Plugin::ValidateRM sub _login_profile { return { required => [ qw ( username password ) ], msgs => { any_errors => 'some_errors', # just want to set a true value here prefix => 'err_', }, }; } sub _relogin_profile { return { required => [ qw ( password ) ], msgs => { any_errors => 'some_errors', # just want to set a true value here prefix => 'err_', }, }; } # Return extra values for the login template for when sub _login_failed_errors { my $self = shift; my $is_login_authenticated = shift; my $user = shift; my $errs = undef; if ( $user && (!$is_login_authenticated) ) { $errs->{'err_password'} = 'Incorrect password for this user'; } elsif ( ! $user ) { $errs->{'err_username'} = 'Unknown user'; } else { die "Can't happen! "; } $errs->{some_errors} = '1'; return $errs; } # Return error values for the relogin template sub _relogin_failed_errors { my $self = shift; my $is_login_authenticated = shift; my $user = shift; my $errs = undef; if ( $user && (!$is_login_authenticated) ) { $errs->{err_password} = 'Incorrect password for this user'; } elsif ( ! $user ) { $errs->{err_username} = 'Unknown username'; $self->log_confess("Can't happen! "); } $errs->{some_errors} = '1'; return $errs; } # Here we handle the logic for sessions timing out or otherwise becoming invalid sub _relogin_test { my $self = shift; if ($self->session->{_timestamp} < time - 600) { return 1; } return; } # Whenever a session is created, we have the opportunity to # fill it with any values we like sub _initialize_session { my $self = shift; my $user = shift; $self->session->{user} = $user; } # Provide values to the relogin template sub _relogin_tmpl_params { my $self = shift; return { username => $self->session->{'user'}->username; }; } # Provide values to the login template sub _login_tmpl_params { my $self = shift; } =head2 The database classes By convention, the database classes are also split into Project and application. First the project level C: package CDBI::MyProj; use base qw( CGI::Application::Framework::CDBI ); use strict; use warnings; 1; Next, the application-specific C package CDBI::Project::app; use Class::DBI::Loader; use base qw ( CDBI::MyProj ); use strict; use warnings; sub db_config_section { 'db_myproj'; } sub import { my $caller = caller; $caller->new_hook('database_init'); $caller->add_callback('database_init', \&setup_tables); } my $Already_Setup_Tables; sub setup_tables { return if $Already_Setup_Tables; my $config = CGI::Application::Plugin::Config::Context->get_current_context; my $db_config = $config->{__PACKAGE__->db_config_section}; my $loader = Class::DBI::Loader->new( dsn => $db_config->{'dsn'}, user => $db_config->{'username'}, password => $db_config->{'password'}, namespace => __PACKAGE__, relationships => 0, ); $Already_Setup_Tables = 1; } 1; =head2 Configuration By default, there are four levels of configuration files in a CAF application: I, I, I and I: /caf/ framework.conf # Global CAF config file projects/ MyProj/ framework.conf # MyProj config file applications/ framework.conf # "All applications" config file myapp1/ framework.conf # myapp1 config file myapp2/ framework.conf # myapp2 config file When an application starts, the application-level framework.conf is loaded. This config file typically contains the line: <> Which includes the "all apps" configuration file. Similarly this configuration file includes the project-level configuration file, and so on up the chain until we reach the top-level C. CAF uses the C configuration system, which is compatible with multiple configuration file formats. The default configuration format is C, which means apache-style config files: md5_salt = bsdjfgNx/INgjlnVlE%K6N1BvUq9%#rjkfldBh session_timeout = 300 object_store = Apache::Session::DB_File LockDirectory = ../caf/sessions/locks FileName = ../caf/sessions/database include_path_common common-templates # template types include: HTMLTemplate, TemplateToolkit and Petal default_type HTMLTemplate include_path_common common-templates default_type HTMLTemplate cache 1 global_vars 1 die_on_bad_params 0 module = Log::Dispatch::File filename = ../caf/logs/webapp.log min_level = debug mode = append append_newline = 1 format = [%P][%d] %F %L %p - %m%n dsn = DBI:mysql:dbname=project username = dbuser password = seekrit =head1 DESCRIPTION C is a web development plaform built upon C. It incorporates many modules from CPAN in order to provide a feature-rich environment, and makes it easy to write robust, secure, scalable web applications. It has the following features: =over 4 =item * Model-View-Controller (MVC) development with L =item * Choice of templating system (via L) =over 4 =item * L =item * L =item * L =item * L =back =item * Form Validatation and Sticky Forms (via L) =item * Easy (optional) L integration =item * Session Management (L) =item * Authentication =item * Login Managment =over 4 =item * login form =item * relogin after session timeout =item * form state is saved after relogin =back =item * Powerful configuration system (via L) =item * Link Integrity system =item * Logging (via L) =back =head1 STARTUP (app.cgi and the run_app method) You call your application with an URL like: http://www.example.com/cgi-bin/app.cgi/MyProj/myapp?rm=some_runmode This instructs CAF to run the application called C which can be found in the project called C. When CAF finds this module, it sets the value of the C param to C and runs the application. All of your applications are run through the single CGI script. For instance, here are some examples: http://www.example.com/cgi-bin/app.cgi/Admin/users?rm=add http://www.example.com/cgi-bin/app.cgi/Admin/users?rm=edit http://www.example.com/cgi-bin/app.cgi/Admin/documents?rm=publish http://www.example.com/cgi-bin/app.cgi/Library/search?rm=results The actual CGI script (C) is tiny. It looks like this: #!/usr/bin/perl use strict; use CGI::Application::Framework; use CGI::Carp 'fatalsToBrowser'; CGI::Application::Framework->run_app( projects => '../caf/projects', ); All the magic happens in the C method. This method does a lot of magic in one go: * examines the value of the URL's C * determines the correct application * finds the application's config file * finds the application's module file * adds paths to @INC, as appropriate * adds paths to the application's TMPL_PATH * passes on any PARAMS or QUERY to the application's new() method * runs the application The only required option is the location of the CAF C directory. The full list of options are: =over 4 =item projects Location of the CAF top-level projects directory. Required. =item app_params Any extra parameters to pass as the C option to the application's C method. I =item query A L query object to pass as the C option to the application's C method. I =item common_lib_dir Where the Perl modules for this project are stored. Defaults to: $projects/$project_name/common-modules The value of this parameter will be added to the application's @INC. =item common_template_dir Where the templates common to all apps in this project are stored. Defaults to: $projects/$project_name/common-templates =item app_dir Where the application Perl modules are stored. Defaults to: $projects/$project_name/applications/$app_name/ The value of this parameter will be added to the application's @INC. =item app_template_dir Where the application-specific template files are. Defaults to: $app_dir/templates =item module The filename of the application module. Defaults to: $app_name.pm =back The C method in CAF was inspired by Michael Peter's L module, and implements a similar concept. =cut sub run_app { my $class = shift; my %params = @_; my $projects = $params{'projects'} or croak "CAF->run_app error: run_app needs a projects dir\n"; -d $projects or croak "CAF->run_app error: projects dir does not exist \n"; my ($project_name, $app_name); if ($ENV{'PATH_INFO'} =~ m{^/?(\w*)/(\w*).*$}) { $project_name = $1; $app_name = $2; } # TODO: add option to chdir to appdir $project_name or croak "CAF->run_app error: can't find project name from PATH_INFO\n"; $app_name or croak "CAF->run_app error: can't find app name from PATH_INFO\n"; my $app_params = $params{'app_params'} || {}; my $query = $params{'query'} || undef; my $common_lib_dir = $params{'common_lib_dir'} || "$projects/$project_name/common-modules"; my $common_template_dir = $params{'common_template_dir'} || "$projects/$project_name/common-templates"; my $app_dir = $params{'app_dir'} || "$projects/$project_name/applications/$app_name/"; my $app_template_dir = $params{'app_template_dir'} || "$app_dir/templates"; my $module = $params{'module'} || "$app_name.pm"; $class->_add_to_inc($common_lib_dir); $class->_add_to_inc($app_dir); my @template_dirs = map { abs_path($_) } ( $app_template_dir, $common_template_dir, ); require $module; my %args = ( TMPL_PATH => \@template_dirs, PARAMS => { framework_app_dir => abs_path($app_dir), %$app_params } ); $args{'QUERY'} = $query if $query; my $app = $app_name->new(%args); $app->run; return $app; } my %Added_To_INC; sub _add_to_inc { my ($self, $path) = @_; $path = abs_path($path); unless ($Added_To_INC{$path}) { unshift @INC, $path; $Added_To_INC{$path} = 1; } } =head1 TEMPLATES CAF uses the L system. C allows you to use any supported Perl templating system, and switch between them while using the same API. Currently supported templating systems include L, L, L and L. =head2 Syntax The syntax is pretty flexible. Pick a style that's most comfortable for you. =over 4 =item CGI::Application::Plugin::TT style syntax $self->template->process('edit_user', \%params); or (with slightly less typing): $self->template->fill('edit_user', \%params); =item CGI::Application load_tmpl style syntax my $template = $self->template->load('edit_user'); $template->param('foo' => 'bar'); $template->output; =back =head2 Defaults If you don't specify a filename, the system loads a template named after the current run mode. If you do specify a filename, you typically omit the filanme's extension. The correct extension is added according to the template's type. sub add_user { my $self = shift; $self->template->fill; # shows add_user.html # or add_user.tmpl # or add_user.xhtml # (depending on template type) } sub del_user { my $self = shift; $self->template('are_you_sure')->fill; # shows are_you_sure.html # or are_you_sure.tmpl # or are_you_sure.xhtml # (depending on template type) } The default template type is specified in the CAF configuration file. along with the other template options. =head2 Template Configuration Here are the template options from the default top-level C: include_path_common common-templates # template types include: HTMLTemplate, TemplateToolkit and Petal default_type HTMLTemplate # Default options for each template type cache 1 global_vars 1 die_on_bad_params 0 POST_CHOMP 1 POST_CHOMP 1 =head2 System Templates In addition to regular templates there are also I. These are used to display the templates for the various runmodes that are called automatically: * invalid_checksum.html * invalid_session.html * login.html * login_form.html * relogin.html * relogin_form.html You can use a different set of options for the system templates than you use for your ordinary templates. For instance you can use L for your run mode templates, but use L for the login and relogin forms. The options for the I are defined in the C section in the top level C: include_path_common common-templates default_type HTMLTemplate cache 1 global_vars 1 die_on_bad_params 0 With both C and C the configuration structure maps very closely to the data structure expected by L. See the docs for that module for further configuration details. =head2 Where Templates are Stored =head3 Application Templates By default, your application templates are stored in the C subdirectory of your application directory: /framework/ projects/ MyProj/ applications/ myapp1/ templates/ runmode_one.html # templates for myapp1 runmode_two.html myapp2/ templates/ runmode_one.html # templates for myapp2 runmode_two.html =head3 Project Templates By default, project templates are stored in the C subdirectory of your project directory: projects/ MyProj/ common-templates/ # login and other common # templates go here =head3 Pre- and Post- process You can hook into the template generation process so that you can modify every template created. Details for how to do this can be found in the docs for to L. =head1 EMBEDDED COMPONENTS Embedded Components allow you to include application components within your templates. For instance, you might include a I
component a the top of every page and a I