root/trunk/Dispatcher/Dispatcher.pm @ 63

Revision 63, 18.4 KB (checked in by bradley, 4 years ago)

Allow use of confess()

Line 
1
2package Apache::Request::Dispatcher;
3
4use strict;
5use DBI;
6use Template;
7use AppConfig;
8#use Exception qw(:all);
9
10use Apache::Const qw(:common :methods :http);
11use POSIX qw(strftime);
12use Carp qw(cluck confess);
13use Apache::Session::Postgres;
14
15use Data::Dumper;
16
17# TODO: Need to change these to Apache::Request (libapreq)
18use CGI;
19use CGI::Cookie;
20
21our @ISA;
22
23=pod
24=head1 NAME
25
26Apache::Request::Dispatcher - dispatches requests to a sub-class of Apache::Request::Controller
27
28=head1 SYNOPSIS
29
30    <Location /myApplication>
31      SetHandler perl-script
32      Perlhander Apache::Request::Dispatcher
33      SetEnv DispatcherConf /path/to/file.cfg
34      setEnv APP_NAME myApplication
35    </Location>
36
37=head1 DESCRIPTION
38
39Apache::Request::Dispatcher is a mod_perl handler which handles
40Apache HTTP requests under mod_perl, and dispatches them to a
41sub-class of Apache::Request::Controller (after some initial
42request setup has been performed).
43
44If required, it will establish a connection to a database
45using the DBI, retrieve (or create) session data for this
46request (using Apache::Session), initialise a template
47processor (using the Template-Toolkit).
48
49The Dispatcher parses  the URI of the request to determine
50which subclass of Apache::Request::Controller to then pass
51control to.
52
53Parsing of the URI occurs as follows:
54
55APP_NAME (environment set in Apache Config) is removed
56from the begining of the URI, so that:
57
58    '/myApplication/SubClass/action'
59
60becomes: 'SubClass/action'
61
62or
63
64    '/myApplication/Sub/Class/action'
65
66becomes: 'Sub/Class/action'
67
68This is then converted to a module name, and a method name,
69such as:
70
71    Apache::Request::Controller::SubClass or
72    Apache::Request::Controller::Sub::Class
73
74with action() being the method name.
75
76It should be noted that if the SubClass or Action name
77contain any thing other than [A-Za-z0-9_] then the request
78is declined.
79
80The dispatcher then dynamically inherits from the module name,
81and then calls $self->action().
82
83The action() method of the controller is then called in an object-oriented
84fashion, with a dispatcher object passed in as its first parameter.
85
86This object contains the following hash elements:
87
88    request  => $r,        # The Apache Request Object
89    dbh      => $dbh,      # The Database Connection Object
90    cfg      => $cfg,      # The AppConfig object
91    template => $template, # The Template Processor
92    apr      => $q,        # The CGI/libapreq object
93    session  => \%session  # Any session data for this user
94
95Depending on the configuration file, 'dbh' or 'session' may be
96undefined if they've been turned off.
97
98an example controller method might be written as follows:
99
100    package Apache::Request::Controller::SubClass;
101    our @ISA = qw(Apache::Request::Controller);
102    use strict;
103    use Apache::Const qw(:common :methods :http);
104
105    sub action
106    {
107        my $self = shift;
108
109        my $thing = getThingByID($self->{'dbh'}, $self->{'apr'}->param('thingId'));
110
111        $self->{'request'}->status(HTTP_OK);
112        $self->{'request'}->content_type('text/html');
113
114        $self->{'template'}->process('myTemplate', {thing => $thing});
115        return OK;
116    }
117    1;
118
119=head2 Special Actions
120
121=over 4
122
123=item __cache()
124
125Generally, the controllers that are dispatched to will generate dynamic
126content, and as such the dispatcher automatically sets the browsers
127caching policy to not cache any content. However, if your Controller
128sub-class has a method called __cache() then will be used to define
129the caching policy. The action name is given as a parameter to the
130__cache() method, and based on this, the __cache() method should
131return 1 to allow caching, or zero to prevent it.
132
133For exmaple, if your Controller sub-class provides 2 actions, staticContent()
134and dynamicContent(), then your __cache() method can control the caching
135policy as follows:
136
137    sub __cache
138    {
139        my $action = shift;
140
141        my $policy = { staticContent  => 1,
142                       dynamicContent => 0};
143        return $policy->{$action} || 0; # Default to no caching.
144    }
145
146If your Controller sub-class wants to turn off caching globally,
147then you can just return zero regardless of what the action name is.
148
149=item __index()
150
151If the dispatcher cannot work out which action the request is for
152(this happens on a URI such as '/myApplication/SubClass') then the
153dispatcher checkes to see if the Controller SubClass has a 'default'
154action by calling __index(). If this method does not exist, then
155the request is declined. The __index() method should return the name
156of the default action, such as:
157
158    sub __index
159    {
160        return 'listAllItems';
161    }
162
163=back
164
165=head2 Template Defaults
166
167The template processor has the following defaults defined, and may
168be used by all templates:
169
170=over 4
171
172=item DisplayNumber()
173
174This template variable is a reference to a subroutine which will add
175comma's in the right place in numbers, for exmaple:
176
177    [% DisplayNumber(1000) %] becomes 1,000
178
179=item DisplayDate()
180
181This template variable is a reference to a subroutine which will take
182a time value (in the form of seconds since the epoch) and display
183the actual date. You may optionally specify an strftime() format:
184
185    [% DisplayDate( CURRENT_TIME, '%a %d %B %Y' ) %]
186
187=item DisplayTime()
188
189This template variable is a reference to a subroutine which will
190take a time value (in the form of seconds since the epoch) and
191displays the time-of-day. You may optionally specify an strftime() format:
192
193    [% DisplayTime( CURRENT_TIME, '%a %d %B %Y' ) %]
194
195=item DisplayDateTime()
196
197This template variable is a reference to a subroutine which will
198take a time value (in the form of seconds since the epoch) and
199displays the date and time of day
200
201    [% DisplayDateTime( CURRENT_TIME ) %]
202
203=item DisplayDuration()
204
205This template variable is a reference to a subroutine which
206will take a number of seconds as input, and output a string
207of the form 'H hours, M minutes and S seconds', eg:
208
209    Last Modified: [% DisplayDuration( CURRENT_TIME - LAST_MODIFIED %]
210
211=item APP_NAME
212
213This template variable is a string which represents the application
214name, as defined by the environment variable APP_NAME.
215
216=item REQUEST
217
218This template variable is an Apache2::RequestRec object, so that the
219template can have access to the current URI etc. Its not really meant
220to be used to set any outgoing headers or any thing tho, as setting
221up the response should really be done in the Controller.
222
223=back
224
225=head2 The Configuration File
226
227The dispatcher can be used to dispatch to multiple controllers
228that dont even need to belong to the same application, and each
229can application can have its own database connection and set of
230templates. This is achieved by having Apache specify which
231configuration file to use based on the Location of the request
232URI. For example:
233
234    <Location /myApplication>
235      SetHandler perl-script
236      Perlhander Apache::Request::Dispatcher
237      SetEnv DispatcherConf /path/to/file.cfg
238      setEnv APP_NAME myApplication
239    </Location>
240
241will specify that all requests with a root-URI of /myApplication
242will use the configuration file as specified in the DispatcherConf
243environment variable, which is /path/to/file.cfg in the above
244example.
245
246The contents of this configuration file are as follows:
247
248    db_dsn="DBI:Pg:database=myApplication;host=127.0.0.1"
249    db_username=apache
250    db_password=apache
251
252    templatePath="/data/myApplication/templates"
253
254    defaultController=WelcomePage
255
256    # If you wish to use sessions, uncomment this and make sure you have
257    # created the sessions table within the database db_dsn.
258    useSessions="Apache::Session::Postgres"
259    sessionTable="sessions"
260
261where db_dsn, db_username, and db_password specifies the database connection
262options, templatePath specifies where the templates are all stored,
263and defaultController specifies which controller is the default, so
264that if a request for '/myApplication' is recieved, it will be converted
265to '/myApplication/WelcomePage', which will in turn get converted to
266Apache::Request::Controller::WelcomePage.
267
268At a minimum, you have to have templatePath defined. If you dont specify a
269default Controller, then the top-level URI will not work.
270
271=head1 AUTHOR
272
273Bradley Kite <bradley-cpan@kitefamily.co.uk>
274
275If you wish to email me, then please remove the '-cpan' part
276of my email address as anything addressed to 'bradley-cpan'
277is assumed to be spam and is not read.
278
279=head1 SEE ALSO
280
281L<Apache::Request::Controller>, L<DB::Table>, L<DB::Table::Row>, L<DBI>, L<perl>
282
283=cut
284
285# /* handler */ {{{
286sub handler
287{
288    my $r = shift;
289
290    # Validate the URI
291    my $uri     = $r->uri;
292
293    # The app-name is allowed to contain funny characters, so we strip it off
294    # before we check its validity.
295    my $relativePath = substr($uri, length($r->location) + 1); # +1 for the training slash
296    if (($relativePath ne '') && ($relativePath !~ /^[A-Za-z0-9_\/]+$/))
297    {
298        return DECLINED;
299    }
300
301    my $self = init($r);
302
303    # Auth Levels: none (default), mixed (anon users allowed), login (must login)
304    if ($self->{'cfg'}->authLevel eq 'login')
305    {
306        # Force the user to login
307        unless ($self->{'session'}->{'auth'}->{'username'})
308        {
309            my $authController = join('/', $self->{'request'}->location, $self->{'cfg'}->authController);
310            $self->{'request'}->headers_out->set(Location => $authController);
311            return HTTP_MOVED_TEMPORARILY;
312        }
313    }
314
315    # Start parsing the URI
316    my @bits = split(/\/+/, $relativePath);
317
318    my $webPrefix = 'Apache::Request::Controller';
319    my $method    = pop @bits;
320    my $pkgName   = join('::', $webPrefix, @bits);
321
322    # Prevent access to the session table.
323    if ($self->{'cfg'}->useSessions && $pkgName eq "Apache::Request::Controller::" . ucfirst($self->{'cfg'}->sessionTable))
324    {
325        return DECLINED;
326    }
327
328    # Show the "Index" page of the given component.
329    if ($pkgName eq $webPrefix)
330    {
331        $pkgName .= '::' . $method;
332        $method   = '';
333    }
334
335    if ($pkgName eq 'Apache::Request::Controller::' || $pkgName eq 'Apache::Request::Controller')
336    {
337        if ($self->{'cfg'}->defaultController)
338        {
339            unless ($uri =~ /\/$/)
340            {
341                $uri .= '/';
342            }
343            $uri .= $self->{'cfg'}->defaultController;
344            #$r->uri($uri);
345            $r->headers_out->set(Location => $uri);
346            return HTTP_MOVED_TEMPORARILY;
347        }
348    }
349
350    if ($method =~ /^\_/)
351    {
352        warn("Cannot dispatch to $pkgName\-\>$method (actions cannot start with a leading underscore)");
353        return DECLINED;
354    }
355
356#    my $ret = try {
357        unshift @ISA, ($pkgName);
358        my $return;
359        eval {
360            if ($method eq '')
361            {
362                unless ($uri =~ /\/$/)
363                {
364                    $uri .= '/';
365                }
366                if ($self->can('__index'))
367                {
368                    $uri .= $self->__index();
369                    $r->uri($uri);
370                    $r->headers_out->set(Location => $uri);
371                    $return  = HTTP_MOVED_TEMPORARILY;
372                }
373                else
374                {
375                    warn("Cannot figure out the default method of $pkgName. Please write an __index() method which returns it");
376                    $return = DECLINED;
377                }
378            }
379            elsif ($self->can($method))
380            {
381                if ($self->can('__cache'))
382                {
383                    eval "\$r->no_cache((${pkgName}::__cache}(\$method) ? 0 : 1));";
384                    confess($@) if ($@);
385                }
386                else
387                {
388                    $r->no_cache(1);
389                }
390                $return = $self->$method();
391            }
392            else
393            {
394                warn("Cannot find $pkgName->$method, declining request");
395                $return = DECLINED;
396            }
397        };
398        if ($@)
399        {
400            my $err = sprintf('Cannot dispatch to %s->%s (%s)',
401                               $pkgName, $method, $@);
402            warn($err);
403            # Exception->new('other')->raise($err);
404            $self->{'dbh'}->rollback;
405            return HTTP_INTERNAL_SERVER_ERROR;
406        }
407        if (($return == OK || $return == HTTP_MOVED_TEMPORARILY) && $self->{'request'}->is_initial_req())
408        {
409            # We only commit if we're the "main" (initial) request  (and if every thing is OK, of course)
410            $self->{'session'}->{'_mtime'} = time();
411            $self->{'dbh'}->commit();
412        }
413        elsif ($return != OK && $return != HTTP_MOVED_TEMPORARILY)
414        {
415            # We roll-back any time. This is so that if the main request works but then
416            # makes a sub-request which fails, then the subrequest will roll-back the main
417            # requests actions too. This is because the sub-request will present some sort
418            # of error to the user, leading them to think that what they just did failed.
419            $self->{'dbh'}->rollback;
420        }
421        return $return;
422#    }
423#    when 'template',
424#    except
425#    {
426#        # If a template error occurs, then the http header etc. would
427#        # have already have been sent.
428#        my $err = shift;
429#        $err->confess;
430#        $self->{'template'}->process('error.tt2'. {ERROR => $err->stringify})
431#          or do { print $err->stringify };
432#        return OK;
433#    }
434#    when 'not_found',
435#    except
436#    {
437#        my $err = shift;
438#        $err->confess;
439#        return DECLINED;
440#    }
441#    when 'other',
442#    except
443#    {
444#        my $err = shift;
445#        $err->confess;
446#        return HTTP_INTERNAL_SERVER_ERROR;
447#    }
448#    finally
449#    {
450#        my $err = shift;
451#        my $retCode = shift;
452
453#        if ($err)
454#        {
455#            $self->{'dbh'}->rollback;
456#        }
457#        else
458#        {
459#            $self->{'dbh'}->commit;
460#        }
461#        return $retCode;
462#    };
463#
464#    return $ret;
465}
466# /* handler */ }}}
467
468# /* init */ {{{
469sub init
470{
471    my $r = shift;
472
473    # Do some initial setup config.
474
475    @ISA = ();
476
477    my $q = new CGI;
478
479    # TODO: Perhaps cache the config files using the key ($ENV{'DispatcherConf'}) into a
480    #       global hash
481    my ($cfg, $dbh, $template);
482    unless ($cfg)
483    {
484        $cfg = AppConfig->new(qw(db_dsn=s db_username=s db_password=s
485                                 templatePath=s defaultController=s
486                                 useSessions=s sessionTable=s
487                                 session_dsn=s session_username=s session_password=s
488                                 authLevel=s authController=s));
489        $cfg->file($ENV{'DispatcherConf'}) || die "Could not open config: $ENV{DispatcherConf}: $!";
490    }
491
492    if ($cfg->db_dsn)
493    {
494        $dbh = DBI->connect($cfg->db_dsn, $cfg->db_username, $cfg->db_password,
495                            {AutoCommit => 0,
496                             RaiseError => 0});
497        unless ($dbh)
498        {
499            confess(sprintf("Could not connect to database %s: %s",
500                            $cfg->db_dsn,
501                            $DBI::errstr));
502        }
503    }
504
505    my %session;
506    if ($cfg->useSessions)
507    {
508        my $cookies = CGI::Cookie->fetch($r) || {};
509        my $cookie  = $cookies->{'session_id'};
510
511        $cfg->sessionTable('sessions') unless ($cfg->sessionTable);
512        my $opts = {Handle => $dbh, Commit => 1, TableName => $cfg->sessionTable};
513
514        if ($cookie)
515        {
516            my $cookieValue = $cookie->value;
517            tie %session, $cfg->useSessions, $cookieValue, $opts;
518        }
519        else
520        {
521            tie %session, $cfg->useSessions, undef, $opts;
522        }
523        # TODO: Add some extra stuff to the config file to determine cookie
524        #       options, such as expire time.
525        $cookie = $q->cookie('-name'    => 'session_id',
526                             '-value'   => $session{'_session_id'},
527                             #'-expires' => '+1d', Per-Session is all thats needed.
528                             '-path'    => $r->location . '/',
529                             '-domain'  => $r->hostname,
530                             '-secure'  => 0);
531        $r->headers_out->set('Set-Cookie' => $cookie->as_string);
532    }
533
534    my $templatePath = $cfg->templatePath || '/data/templates';
535    $template ||= new Template(INCLUDE_PATH => $templatePath,
536                               RECURSION    => 1,
537                               PRE_DEFINE   => { DisplayNumber   => \&T_DisplayNumber,
538                                                 DisplayDate     => \&T_DisplayDate,
539                                                 DisplayTime     => \&T_DisplayTime,
540                                                 DisplayDateTime => \&T_DisplayDateTime,
541                                                 DisplayDuration => \&T_DisplayDuration,
542                                                 REQUEST         => $r,
543                                                 SESSION         => \%session,
544                                                 CFG             => $cfg});
545    my $self = {request  => $r,
546                dbh      => $dbh,
547                cfg      => $cfg,
548                template => $template,
549                apr      => $q,
550                session  => \%session};
551    bless ($self, __PACKAGE__);
552
553    return $self;
554}
555# /* init */ }}}
556
557# /* Template Display Functions */ {{{
558sub T_DisplayNumber
559{
560    my $number = reverse(shift);
561    $number =~ s/(\d\d\d)(?!$)/$1\,/g;
562    return scalar(reverse($number));
563}
564
565sub T_DisplayDateTime
566{
567    my $time = shift;
568
569    return strftime('%a %d %B %T %Y', gmtime($time));
570}
571
572sub T_DisplayTime
573{
574    my $time   = shift;
575    my $format = shift || '%a %d %B %Y';
576
577    return strftime($format, gmtime($time));
578}
579
580sub T_DisplayDate
581{
582    my $time   = shift;
583    my $format = shift || '%a %d %B %Y';
584
585    return strftime($format, gmtime($time));
586}
587
588sub T_DisplayDuration
589{
590    my $duration = shift; # number of seconds
591
592    my @bits = ({ NAME => 'Seconds',
593                  NEXT => 60},
594                { NAME => 'Minutes',
595                  NEXT => 60 },
596                { NAME => 'Hours',
597                  NEXT => '24' },
598                { NAME => 'Days',
599                  NEXT => 7 },
600                { NAME => 'Weeks' });
601    my @results;
602    do
603    {
604        my $remainder = ($duration % $bits[0]->{'NEXT'});
605        unshift @results, sprintf("%d %s", $remainder,
606                                        $bits[0]->{'NAME'}) if ($remainder > 0);
607        $duration -= $remainder;
608        $duration /= $bits[0]->{'NEXT'};
609        shift @bits;
610    }
611    while (defined ($bits[0]) && defined ($bits[0]->{'NEXT'}));
612
613    return join(' ', @results);
614}
615# /* Template Display Functions */ }}}
616
6171;
618
Note: See TracBrowser for help on using the browser.