]> git.decadent.org.uk Git - maypole.git/blobdiff - lib/Maypole/Manual/Request.pod
+ Updated manual, thanks to Dave Howorth
[maypole.git] / lib / Maypole / Manual / Request.pod
index f9186563e440532604a84a0e91f62806fcf95585..d3d66f299c66d6116d27c2ba1dba747b23226471 100644 (file)
@@ -32,8 +32,8 @@ And in F<BeerDB.pm>, you put:
 
 It doesn't work.
 
-B<Solution>: It doesn't work because of the timing of the module
-loading. C<use Beer::Beer> will try to set up the C<has_a> relationships
+B<Solution>: It doesn't work because of the timing of the module loading.
+C<use BeerDB::Beer> will try to set up the C<has_a> relationships
 at compile time, when the database tables haven't even been set up,
 since they're set up by
 
@@ -54,10 +54,10 @@ or move the module loading to run-time (my preferred solution):
 You're seeing bizarre problems with Maypole output, and you want to test it in
 some place outside of the whole Apache/mod_perl/HTTP/Internet/browser circus.
 
-B<Solution>: Use the C<Maypole::CLI> module to go directly from a URL to
+B<Solution>: Use the L<Maypole::CLI> module to go directly from a URL to
 standard output, bypassing Apache and the network altogether.
 
-C<Maypole::CLI> is not a standalone front-end, but to allow you to debug your
+L<Maypole::CLI> is not a standalone front-end, but to allow you to debug your
 applications without having to change the front-end they use, it temporarily
 "borgs" an application. If you run it from the command line, you're expected
 to use it like so:
@@ -72,23 +72,30 @@ You can also use the C<Maypole::CLI> module programatically to create
 test suites for your application. See the Maypole tests themselves or
 the documentation to C<Maypole::CLI> for examples of this.
 
+Don't forget also to turn on debugging output in your application:
+
+    package BeerDB;
+    use strict;
+    use warnings;
+    use Maypole::Application qw(-Debug);
+
 =head3 Changing how URLs are parsed
 
 You don't like the way Maypole URLs look, and want something that either
 fits in with the rest of your site or hides the internal workings of the
 system.
 
-C<Solution>: So far we've been using the C</table/action/id/args> form
+B<Solution>: So far we've been using the C</table/action/id/args> form
 of a URL as though it was "the Maypole way"; well, there is no Maypole
 way. Maypole is just a framework and absolutely everything about it is 
 overridable. 
 
 If we want to provide our own URL handling, the method to override in
 the driver class is C<parse_path>. This is responsible for taking
-C<$r-E<gt>{path}> and filling the C<table>, C<action> and C<args> slots
+C<$r-E<gt>path> and filling the C<table>, C<action> and C<args> slots
 of the request object. Normally it does this just by splitting the path
-on C</>s, but you can do it any way you want, including getting the
-information from C<POST> form parameters or session variables. 
+on 'C</>' characters, but you can do it any way you want, including
+getting the information from C<POST> form parameters or session variables. 
 
 For instance, suppose we want our URLs to be of the form
 C<ProductDisplay.html?id=123>, we could provide a C<parse_path> method
@@ -96,13 +103,12 @@ like so:
 
     sub parse_path {
         my $r = shift;
-        $r->{path} ||= "ProductList.html";
-        ($r->{table}, $r->{action}) = 
-            ($r->{path} =~ /^(.*?)([A-Z]\w+)\.html/);
-        $r->{table}  = lc $r->{table};
-        $r->{action} = lc $r->{action};
-        my %query = $r->{ar}->args;
-        $self->{args} = [ $query{id} ];
+        $r->path("ProductList.html") unless $r->path;
+        ($r->path =~ /^(.*?)([A-Z]\w+)\.html/);
+        $r->table(lc $1);
+        $r->action(lc $2);
+        my %query = $r->ar->args;
+        $self->args([ $query{id} ]);
     }
 
 This takes the path, which already has the query parameters stripped off
@@ -110,7 +116,8 @@ and parsed, and finds the table and action portions of the filename,
 lower-cases them, and then grabs the C<id> from the query. Later methods
 will confirm whether or not these tables and actions exist.
 
-See L<BuySpy.pod> for another example of custom URL processing.
+See the L<iBuySpy Portal|Maypole::Manual::BuySpy> for another
+example of custom URL processing.
 
 =head3 Maypole for mobile devices
 
@@ -126,7 +133,7 @@ putting this method in our driver class:
 
     sub get_template_root {
         my $r = shift;
-        my $browser = $r->{ar}->headers_in->get('User-Agent');
+        my $browser = $r->headers_in->get('User-Agent');
         if ($browser =~ /mobile|palm|nokia/i) {
             "/home/myapp/templates/mobile";
         } else {
@@ -140,7 +147,7 @@ idea.)
 =head2 Content display hacks
 
 These hacks deal primarily with the presentation of data to the user,
-modifying the C<view> template or changing the way that the results of
+modifying the F<view> template or changing the way that the results of
 particular actions are displayed.
 
 =head3 Null Action
@@ -156,8 +163,9 @@ way.
 
 If, on the other hand, you want to display some data, and what you're
 essentially doing is a variant of the C<view> action, then you need to
-ensure that you have an exported action, as described in
-L<StandardTemplates.pod>:
+ensure that you have an exported action, as described in the
+L<templates and actions|Maypole::Manual::StandardTemplates/"C<view> and C<edit>">
+chapter:
 
     sub my_view :Exported { }
 
@@ -176,17 +184,17 @@ going to C</beer/view/I<id>> all along. We do this by setting the
 objects in the C<objects> slot and changing the C<template> to the
 one we wanted to go to.
 
-In this example from L<Flox.pod>, we've just performed an C<accept>
-method on a C<Flox::Invitation> object and we want to go back to viewing
-a user's page.
+In this example from L<Flox|Maypole::Manual::Flox>, we've just
+performed an C<accept> method on a C<Flox::Invitation> object and we
+want to go back to viewing a user's page.
 
     sub accept :Exported {
         my ($self, $r) = @_;
         my $invitation = $r->objects->[0];
         # [... do stuff to $invitation ...]
-        $r->{objects} = [$r->{user}];
-        $r->{model_class} = "Flox::User";
-        $r->{template} = "view";
+        $r->objects([$r->user]);
+        $r->model_class("Flox::User");
+        $r->template("view");
     }
 
 This hack is so common that it's expected that there'll be a neater
@@ -216,13 +224,13 @@ chords:
             <line> <sup>A</sup>Lay<sup>Dm</sup>la <sup>Bb</sup> </line> 
             <line> <sup>C</sup>Got me on my <sup>Dm</sup>knees </line> 
             ...
+
 I store the title, artist and key in the database, as well as an "xml"
 field which contains the whole song as XML.
 
 To load the songs into the database, I can C<use> the driver class for
 my application, since that's a handy way of setting up the database classes
-we're going to need to use. Then the handy C<XML::TreeBuilder> will handle
+we're going to need to use. Then the handy L<XML::TreeBuilder> will handle
 the XML parsing for us:
 
     use Songbook;
@@ -247,7 +255,7 @@ the XML parsing for us:
     }
 
 Now we need to set up the custom display for each song; thankfully, with
-the C<Template::Plugin::XSLT> module, this is as simple as putting the
+the L<Template::Plugin::XSLT> module, this is as simple as putting the
 following into F<templates/song/view>:
 
     [%
@@ -273,22 +281,23 @@ We also set the C<content_type> using one from the database.
 
     sub view_picture :Exported {
         my ($self, $r) = @_;
-        my $user = $r->{objects}->[0];
-        $r->{content_type} = $user->photo_type;
-        $r->{output} = $user->photo;
+        my $user = $r->objects->[0];
+        $r->content_type($user->photo_type);
+        $r->output($user->photo);
     }
 
 Of course, the file doesn't necessarily need to be in the database
 itself; if your file is stored in the filesystem, but you have a file
 name or some other pointer in the database, you can still arrange for
-the data to be fetched and inserted into C<$r-E<gt>{output}>.
+the data to be fetched and inserted into C<$r-E<gt>output>.
 
 =head3 REST
 
 You want to provide a programmatic interface to your Maypole site.
 
 B<Solution>: The best way to do this is with C<REST>, which uses a
-descriptive URL to encode the request. For instance, in L<Flox.pod> we
+descriptive URL to encode the request. For instance, in
+L<Flox|Maypole::Manual::Flox> we
 describe a social networking system. One neat thing you can do with
 social networks is to use them for reputation tracking, and we can use
 that information for spam detection. So if a message arrives from
@@ -313,15 +322,15 @@ row in the user table, it will appear in the C<args> array:
     use URI::Escape;
     sub relationship_by_email :Exported {
         my ($self, $r) = @_;
-        my $email = uri_unescape($r->{args}[0]);
-        $r->{content_type} = "text/plain";
+        my $email = uri_unescape($r->args->[0]);
+        $r->content_type("text/plain");
         my $user;
         unless (($user) = Flox::User->search(email => $email)) {
-            $r->{content} = "0\n"; return;
+            $r->content("0\n"); return;
         }
 
-        if ($r->{user}->is_friend($user)) { $r->{content} = "2\n"; return; };
-        $r->{content} = "1\n"; return;
+        if ($r->user->is_friend($user)) { $r->contenti("2\n"); return; };
+        $r->content("1\n"); return;
     }
 
 =head3 Component-based Pages
@@ -331,9 +340,13 @@ components, all displaying different bits of information about different
 objects. You want to include the output of one Maypole request call while
 building up another. 
 
-B<Solution>: Use C<Maypole::Component>. By inheriting from this, you can
-call the C<component> method on the Maypole request object to make a 
-"sub-request". For instance, if you have a template
+B<Solution>: Use L<Maypole::Plugin::Component>. By inheriting like this:
+
+    package BeerDB;
+    use Maypole::Application qw(Component);
+
+you can call the C<component> method on the Maypole request object to
+make a "sub-request". For instance, if you have a template
 
     <DIV class="latestnews">
     [% request.component("/news/latest_comp") %]
@@ -367,8 +380,8 @@ Put this in your driver class:
 
     sub error { 
         my ($r, $message) = @_;
-        $r->{template} = "error";
-        $r->{template_args}{error} = $message;
+        $r->template("error");
+        $r->template_args->{error} = $message;
         return OK;
     }
 
@@ -398,7 +411,7 @@ returns C<OK>, you can even use it in your C<authenticate> routine:
         my ($self, $r) = @_;
         $r->get_user;
         return $r->error("You do not exist. Go away.")
-            if $r->{user} and $r->{user}->status ne "real";
+            if $r->user and $r->user->status ne "real";
         ...
     }
 
@@ -409,7 +422,7 @@ Non-showstopper errors or other notifications are best handled by tacking a
 C<messages> template variable onto the request:
 
     if ((localtime)[6] == 1) {
-        push @{$r->{template_args}{messages}}, "Warning: Today is Monday";
+        push @{$r->template_args->{messages}}, "Warning: Today is Monday";
     }
 
 Now F<custom/messages> can contain:
@@ -429,7 +442,7 @@ appropriate point in your template; you may also want to use a template
 switcheroo to ensure that you're displaying a page that has the messages box in
 it.
 
-=head2 Authentication hacks
+=head2 Authentication and Authorization hacks
 
 The next series of hacks deals with providing the concept of a "user" for
 a site, and what you do with one when you've got one.
@@ -439,31 +452,33 @@ a site, and what you do with one when you've got one.
 You need the concept of a "current user".
 
 B<Solution>: Use something like
-C<Maypole::Authentication::UserSessionCookie> to authenticate a user
-against a user class and store a current user object in the request
-object.
+L<Maypole::Plugin::Authentication::UserSessionCookie> to authenticate
+a user against a user class and store a current user object in the
+request object.
 
 C<UserSessionCookie> provides the C<get_user> method which tries to get
 a user object, either based on the cookie for an already authenticated
-session, or by comparing C<username> and C<password> form parameters
+session, or by comparing C<user> and C<password> form parameters
 against a C<user> table in the database. Its behaviour is highly
-customizable, so see the documentation, or the authentication paper at
-C<http://maypole.simon-cozens.org/docs/authentication.html> for examples.
+customizable and described in its documentation.
 
 =head3 Pass-through login
 
 You want to intercept a request from a non-logged-in user and have
 them log in before sending them on their way to wherever they were
-originally going.
+originally going. Override C<Maypole::authenticate> in your driver
+class, something like this:
 
 B<Solution>:
 
+    use Maypole::Constants; # Otherwise it will silently fail!
+
     sub authenticate {
         my ($self, $r) = @_;
         $r->get_user;
-        return OK if $r->{user};
+        return OK if $r->user;
         # Force them to the login page.
-        $r->{template} = "login";
+        $r->template("login");
         return OK;
     }
 
@@ -478,13 +493,14 @@ like this:
     [% IF login_error %]
        <FONT COLOR="#FF0000"> [% login_error %] </FONT>
     [% END %]
-      <FORM ACTION="/[% request.path%]" METHOD="post">
+      <FORM ACTION="[% base ; '/' ; request.path %]" METHOD="post">
     Username: 
-        <INPUT TYPE="text" NAME="[% config.auth.user_field || "user" %]"> <BR>
+        <INPUT TYPE="text" NAME="[% config.auth.user_field || "user" %]"><BR>
     Password: <INPUT TYPE="password" NAME="password"> <BR>
     <INPUT TYPE="submit">
     </FORM>
     </DIV>
+    [% INCLUDE footer %]
 
 Notice that this request gets C<POST>ed back to wherever it came from, using
 C<request.path>. This is because if the user submits correct credentials,
@@ -497,28 +513,11 @@ Now your users are logged in, you want a way of having them log out
 again and taking the authentication cookie away from them, sending
 them back to the front page as an unprivileged user.
 
-B<Solution>: This action, on the user class, is probably overkill, but
-it does the job:
-
-    sub logout :Exported {
-        my ($class, $r) = @_;
-        # Remove the user from the request object
-        my $user = delete $r->{user};
-        # Destroy the session
-        tied(%{$r->{session}})->delete;
-        # Send a new cookie which expires the previous one
-        my $cookie = Apache::Cookie->new($r->{ar},
-            -name => $r->config->{auth}{cookie_name},
-            -value => undef,
-            -path => "/"
-            -expires => "-10m"
-        );
-        $cookie->bake();
-        # Template switcheroo
-        $r->template("frontpage");
-    }
+B<Solution>: Just call the C<logout> method of
+C<Maypole::Plugin::Authentication::UserSessionCookie>. You may also want
+to use the template switcheroo hack to send them back to the frontpage.
 
-=head3 Multi-level Authentication
+=head3 Multi-level Authorization
 
 You have both a global site access policy (for instance, requiring a
 user to be logged in except for certain pages) and a policy for
@@ -529,26 +528,33 @@ provided by the model class.)
 You don't know whether to override the global C<authenticate> method or
 provide one for each class.
 
-B<Solution>: Do both. Have a global C<authenticate> method which calls 
-a C<sub_authenticate> method based on the class:
+B<Solution>: Do both.
+Maypole checks whether there is an C<authenticate> method for the model
+class (e.g. BeerDB::Beer) and if so calls that. If there's no such
+method, it calls the default global C<authenticate> method in C<Maypole>,
+which always succeeds. You can override the global method as we saw
+above, and you can provide methods in the model classes.
 
-    sub authenticate {
-        ...
-        if ($r->{user}) {
-            return $r->model_class->sub_authenticate($r)
-                if $r->model_class && $r->model_class->can("sub_authenticate");
-            return OK;
-        }
-        ...
+To use per-table access control you can just add methods to your model
+subclasses that specify individual policies, perhaps like this:
+
+    sub authenticate { # Ensure we can only create, reject or accept
+        my ($self, $r) = @_;
+        return OK if $r->action =~ /^(issue|accept|reject|do_edit)$/;
+        return; # fail if any other action
     }
 
-And now your C<sub_authenticate> methods can specify the policy for
-each table:
+If you define a method like this, the global C<authenticate> method will
+not be called, so if you want it to be called you need to do so
+explicitly:
 
-    sub sub_authenticate { # Ensure we can only create, reject or accept
+    sub authenticate { # Ensure we can only create, reject or accept
         my ($self, $r) = @_;
-        return OK if $r->{action} =~ /^(issue|accept|reject|do_edit)$/;
-        return;
+        return unless $r->authenticate($r) == OK; # fail if not logged in
+        # now it's safe to use $r->user
+        return OK if $r->action =~ /^(accept|reject)$/
+            or ($r->user eq 'fred' and $r->action =~ /^(issue|do_edit)$/);
+        return; # fail if any other action
     }
 
 =head2 Creating and editing hacks
@@ -562,8 +568,8 @@ You want the user to be able to type in some text that you're later
 going to display on the site, but you don't want them to stick images in
 it, launch cross-site scripting attacks or otherwise insert messy HTML.
 
-B<Solution>: Use the C<CGI::Untaint::html> module to sanitize the HTML
-on input. C<CGI::Untaint::html> uses C<HTML::Sanitizer> to ensure that
+B<Solution>: Use the L<CGI::Untaint::html> module to sanitize the HTML
+on input. C<CGI::Untaint::html> uses L<HTML::Sanitizer> to ensure that
 tags are properly closed and can restrict the use of certain tags and
 attributes to a pre-defined list.
 
@@ -589,15 +595,15 @@ data from another source.
 
 B<Solution>: Munge the contents of C< $r-E<gt>params > before jumping
 to the original C<do_edit> routine. For instance, in this method,
-we use a C<Net::Amazon> object to fill in some fields of a database row based
-on an ISBN:
+we use a L<Net::Amazon> object to fill in some fields of a database row
+based on an ISBN:
 
     sub create_from_isbn :Exported {
        my ($self, $r) = @_;
-       my $response = $ua->search(asin => $r->{params}{isbn});
+       my $response = $ua->search(asin => $r->params->{isbn});
        my ($prop) = $response->properties;
        # Rewrite the CGI parameters with the ones from Amazon
-       @{$r->{params}{qw(title publisher author year)} =            
+       @{$r->params->{qw(title publisher author year)} =            
            ($prop->title,
            $prop->publisher,
            (join "/", $prop->authors()),
@@ -608,6 +614,8 @@ on an ISBN:
 
 The request will carry on as though it were a normal C<do_edit> POST, but
 with the additional fields we have provided.
+You might also want to add a template switcheroo so the user can verify
+the details you imported.
 
 =head3 Catching errors in a form
 
@@ -624,8 +632,8 @@ If there are any errors, these are placed in a hash, with each error
 keyed to the erroneous field. The hash is put into the template as
 C<errors>, and we process the same F<edit> template again:
 
-        $r->{template_args}{errors} = \%errors;
-        $r->{template} = "edit";
+        $r->template_args->{errors} = \%errors;
+        $r->template("edit");
 
 This throws us back to the form, and so the form's template should take
 note of the errors, like so:
@@ -696,7 +704,7 @@ the action:
 
     sub upload_picture : Exported {}
 
-And here's the template:
+And here's the F<custom/upload_picture> template:
 
     <FORM action="/user/do_upload" enctype="multipart/form-data" method="POST">
 
@@ -710,16 +718,16 @@ And here's the template:
 (Although you'll probably want a bit more HTML around it than that.)
 
 Now we need to write the C<do_upload> action. At this point we have to get a
-little friendly with the front-end system. If we're using C<Apache::Request>,
+little friendly with the front-end system. If we're using L<Apache::Request>,
 then the C<upload> method of the C<Apache::Request> object (which
-C<Apache::MVC> helpfully stores in C<$r-E<gt>{ar}>) will work for us:
+L<Apache::MVC> helpfully stores in C<$r-E<gt>{ar}>) will work for us:
 
     sub do_upload :Exported {
         my ($class, $r) = @_;
-        my $user = $r->{user};
-        my $upload = $r->{ar}->upload("picture");
+        my $user = $r->user;
+        my $upload = $r->ar->upload("picture");
 
-This returns a C<Apache::Upload> object, which we can query for its
+This returns a L<Apache::Upload> object, which we can query for its
 content type and a file handle from which we can read the data. It's
 also worth checking the image isn't going to be too massive before we
 try reading it and running out of memory, and that the content type is
@@ -735,26 +743,58 @@ something we're prepared to deal with.
         my $fh = $upload->fh;
         my $image = do { local $/; <$fh> };
 
+Don't forget C<binmode()> in there if you're on a platform that needs it.
 Now we can store the content type and data into our database, store it
 into a file, or whatever:
 
-        $r->{user}->photo_type($ct);
-        $r->{user}->photo($image);
+        $r->user->photo_type($ct);
+        $r->user->photo($image);
     }
 
 And finally, we use our familiar template switcheroo hack to get back to
 a useful page:
 
         $r->objects([ $user ]);
-        $r->{template} = "view";
+        $r->template("view");
     }
 
 Now, as we've mentioned, this only works because we're getting familiar with
-C<Apache::Request> and its C<Apache::Upload> objects. If we're planning to use
-C<CGI::Maypole> instead, or want to write our application in a generic way so
-that it'll work regardless of front-end, then we need to replace the C<upload>
-call with an equivalent which uses the C<CGI> module to get the upload data.
-This is convoluted and horrific and we're not going to show it here, but it's
-possible.
+C<Apache::Request> and its C<Apache::Upload> objects. If we're using
+L<CGI::Maypole> instead, we can write the action in a similar style:
+
+    sub do_upload :Exported {
+        my ($class, $r) = @_;
+        my $user = $r->user;
+        my $cgi = $r->cgi;
+        if ($cgi->upload == 1) { # if there was one file uploaded
+            my $filename = $cgi->param('picture');
+            my $ct = $cgi->upload_info($filename, 'mime');
+            return $r->error("Unknown image file type $ct")
+                if $ct !~ m{image/(jpeg|gif|png)};
+            return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)
+                if $cgi->upload_info($filename, 'size') > MAX_IMAGE_SIZE;
+            my $fh = $cgi->upload($filename);
+            my $image = do { local $/; <$fh> };
+            $r->user->photo_type($ct);
+            $r->user->photo($image);
+        }
+
+        $r->objects([ $user ]);
+        $r->template("view");
+    }
+
+It's easy to adapt this to upload multiple files if desired.
+You will also need to enable uploads in your driver initialization,
+with the slightly confusing statement:
+
+    $CGI::Simple::DISABLE_UPLOADS = 0; # enable uploads
 
 Combine with the "Displaying pictures" hack above for a happy time.
+
+=head2 Links
+
+L<Contents|Maypole::Manual>,
+Next L<Flox|Maypole::Manual::Flox>,
+Previous L<The Beer Database, Twice|Maypole::Manual::Beer>
+
+