]> git.decadent.org.uk Git - maypole.git/blob - doc/Request.pod
80e4037cd9bd216f3bf707880efa213327ff456f
[maypole.git] / doc / Request.pod
1 =head1 Maypole Request Hacking Cookbook
2
3 Hacks; design patterns; recipes: call it what you like, this chapter is a
4 developing collection of techniques which can be slotted in to Maypole
5 applications to solve common problems or make the development process easier.
6
7 As Maypole developers, we don't necessarily know the "best practice" for
8 developing Maypole applications ourselves, in the same way that Larry Wall
9 didn't know all about the best Perl programming style as soon as he wrote
10 Perl. These techniques are what we're using at the moment, but they may
11 be refined, modularized, or rendered irrelevant over time. But they've
12 certainly saved us a bunch of hours work.
13
14 =head2 Frontend hacks
15
16 These hacks deal with changing the way Maypole relates to the outside world;
17 alternate front-ends to the Apache and CGI interfaces, or subclassing chunks
18 of the front-end modules to alter Maypole's behaviour in particular ways.
19
20 =head3 Debugging with the command line
21
22 You're seeing bizarre problems with Maypole output, and you want to test it in
23 some place outside of the whole Apache/mod_perl/HTTP/Internet/browser circus.
24
25 B<Solution>: Use the C<Maypole::CLI> module to go directly from a URL to
26 standard output, bypassing Apache and the network altogether.
27
28 C<Maypole::CLI> is not a standalone front-end, but to allow you to debug your
29 applications without having to change the front-end they use, it temporarily
30 "borgs" an application. If you run it from the command line, you're expected
31 to use it like so:
32
33     perl -MMaypole::CLI=Application -e1 'http://your.server/path/table/action'
34
35 For example:
36
37     perl -MMaypole::CLI=BeerDB -e1 'http://localhost/beerdb/beer/view/1?o2=desc'
38
39 You can also use the C<Maypole::CLI> module programatically to create
40 test suites for your application. See the Maypole tests themselves or
41 the documentation to C<Maypole::CLI> for examples of this.
42
43 =head3 Changing how params are parsed
44
45 =head3 REST
46
47 =head2 Content display hacks
48
49 These hacks deal primarily with the presentation of data to the user,
50 modifying the C<view> template or changing the way that the results of
51 particular actions are displayed.
52
53 =head3 Template Switcheroo
54
55 An action doesn't have any data of its own to display, but needs to display
56 B<something>.
57
58 B<Solution>: This is an B<extremely> common hack. You've just issued an
59 action like C<beer/do_edit>, which updates the database. You don't want
60 to display a page that says "Record updated" or similar. Lesser
61 application servers would issue a redirect to have the browser request
62 C</beer/view/I<id>> instead, but we can actually modify the Maypole
63 request on the fly and, after doing the update, pretend that we were
64 going to C</beer/view/I<id>> all along. We do this by setting the
65 objects in the C<objects> slot and changing the C<template> to the
66 one we wanted to go to.
67
68 In this example from L<Flox.pod>, we've just performed an C<accept>
69 method on a C<Flox::Invitation> object and we want to go back to viewing
70 a user's page.
71
72     sub accept :Exported {
73         my ($self, $r) = @_;
74         my $invitation = $r->objects->[0];
75         # [... do stuff to $invitation ...]
76         $r->{objects} = [$r->{user}];
77         $r->{model_class} = "Flox::User";
78         $r->{template} = "view";
79     }
80
81 This hack is so common that it's expected that there'll be a neater
82 way of doing this in the future.
83
84 =head3 Maypole for mobile devices
85
86 XXX
87
88 =head3 XSLT
89
90 Here's a hack I've used a number of times. You want to store structured
91 data in a database and to abstract out its display.
92
93 B<Solution>: You have your data as XML, because handling big chunks of
94 XML is a solved problem. Build your database schema as usual around the
95 important elements that you want to be able to search and browse on. For
96 instance, I have an XML format for songs which has a header section of
97 the key, title and so on, plus another section for the lyrics and
98 chords:
99
100     <song>
101         <header>
102             <title>Layla</title>
103             <artist>Derek and the Dominos</artist>
104             <key>Dm</key>
105         </header>
106         <lyrics>
107           <verse>...</verse>
108           <chorus>
109             <line> <sup>A</sup>Lay<sup>Dm</sup>la <sup>Bb</sup> </line> 
110             <line> <sup>C</sup>Got me on my <sup>Dm</sup>knees </line> 
111             ...
112  
113 I store the title, artist and key in the database, as well as an "xml"
114 field which contains the whole song as XML.
115
116 To load the songs into the database, I can C<use> the driver class for
117 my application, since that's a handy way of setting up the database classes
118 we're going to need to use. Then the handy C<XML::TreeBuilder> will handle
119 the XML parsing for us:
120
121     use Songbook;
122     use XML::TreeBuilder;
123     my $t = XML::TreeBuilder->new;
124     $t->parse_file("songs.xml");
125
126     for my $song ($t->find("song")) {
127         my ($key) = $song->find("key"); $key &&= $key->as_text;
128         my ($title) = $song->find("title"); $title = $title->as_text;
129         my ($artist) = $song->find("artist"); $artist = $artist->as_text;
130         my ($first_line) = $song->find("line");
131         $first_line = join "", grep { !ref } $first_line->content_list;
132         $first_line =~ s/[,\.\?!]\s*$//;
133         Songbook::Song->find_or_create({
134             title => $title,
135             first_line => $first_line,
136             song_key => Songbook::SongKey->find_or_create({name => $key}),
137             artist => Songbook::Artist->find_or_create({name => $artist}),
138             xml => $song->as_XML
139         });
140     }
141
142 Now we need to set up the custom display for each song; thankfully, with
143 the C<Template::Plugin::XSLT> module, this is as simple as putting the
144 following into F<templates/song/view>:
145
146     [%
147         USE transform = XSLT("song.xsl");
148         song.xml | $transform
149     %]
150
151 We essentially pipe the XML for the selected song through to an XSL
152 transformation, and this will fill out all the HTML we need. Job done.
153
154 =head3 Displaying pictures
155
156 You want to serve a picture, a Word document, or something else which
157 doesn't have a content type of C<text/html>, out of your database.
158
159 B<Solution>: Fill the content and content-type yourself.
160
161 Here's a subroutine which displays the C<photo> for either a specified
162 user or the currently logged in user. We set the C<output> slot of the
163 Maypole request object: if this is done then the view class is not called
164 upon to process a template, since we already have some output to display.
165 We also set the C<content_type> using one from the database.
166
167     sub view_picture :Exported {
168         my ($self, $r) = @_;
169         my $user = $r->{objects}->[0];
170         $r->{content_type} = $user->photo_type;
171         $r->{output} = $user->photo;
172     }
173
174 Of course, the file doesn't necessarily need to be in the database
175 itself; if your file is stored in the filesystem, but you have a file
176 name or some other pointer in the database, you can still arrange for
177 the data to be fetched and inserted into C<$r-E<gt>{output}>.
178
179 =head3 Component-based Pages
180
181 You're designing something like a portal site which has a number of
182 components, all displaying different bits of information about different
183 objects. You want to include the output of one Maypole request call while
184 building up another. 
185
186 B<Solution>: Use C<Maypole::Component>. By inheriting from this, you can
187 call the C<component> method on the Maypole request object to make a 
188 "sub-request". For instance, if you have a template
189
190     <DIV class="latestnews">
191     [% request.component("/news/latest_comp") %]
192     </DIV>
193
194     <DIV class="links">
195     [% request.component("/links/list_comp") %]
196     </DIV>
197
198 then the results of calling the C</news/latest_comp> action and template
199 will be inserted in the C<latestnews> DIV, and the results of calling
200 C</links/list_comp> will be placed in the C<links> DIV. Naturally, you're
201 responsible for exporting actions and creating templates which return 
202 fragments of HTML suitable for inserting into the appropriate locations.
203
204 =head3 Bailing out with an error
205
206 Maypole's error handling sucks. Something really bad has happened to the
207 current request, and you want to stop processing now and tell the user about
208 it.
209
210 B<Solution>: Maypole's error handling sucks because you haven't written it
211 yet. Maypole doesn't know what you want to do with an error, so it doesn't
212 guess. One common thing to do is to display a template with an error message
213 in it somewhere.
214
215 Put this in your driver class:
216
217     sub error { 
218         my ($r, $message) = @_;
219         $r->{template} = "error";
220         $r->{template_args}{error} = $message;
221         return OK;
222     }
223
224 And then have a F<custom/error> template like so:
225
226     [% PROCESS header %]
227     <H2> There was some kind of error... </H2>
228     <P>
229     I'm sorry, something went so badly wrong, we couldn't recover. This
230     may help:
231     </P>
232     <DIV CLASS="messages"> [% error %] </DIV>
233
234 Now in your actions you can say things like this:
235
236     if (1 == 0) { return $r->error("Sky fell!") }
237
238 This essentially uses the template switcheroo hack to always display the
239 error template, while populating the template with an C<error> parameter.
240 Since you C<return $r-E<gt>error>, this will terminate the processing
241 of the current action.
242
243 The really, really neat thing about this hack is that since C<error>
244 returns C<OK>, you can even use it in your C<authenticate> routine:
245
246     sub authenticate {
247         my ($self, $r) = @_;
248         $r->get_user;
249         return $r->error("You do not exist. Go away.")
250             if $r->{user} and $r->{user}->status ne "real";
251         ...
252     }
253
254 This will bail out processing the authentication, the model class, and
255 everything, and just skip to displaying the error message. 
256
257 Non-showstopper errors or other notifications are best handled by tacking a
258 C<messages> template variable onto the request:
259
260     if ((localtime)[6] == 1) {
261         push @{$r->{template_args}{messages}}, "Warning: Today is Monday";
262     }
263
264 Now F<custom/messages> can contain:
265
266     [% IF messages %]
267     <DIV class="messages">
268     <UL>
269         [% FOR message = messages %]
270            <LI> [% message %] </LI>
271         [% END %]
272     </UL>
273     </DIV>
274     [% END %]
275
276 And you can display messages to your user by adding C<PROCESS messages> at an
277 appropriate point in your template; you may also want to use a template
278 switcheroo to ensure that you're displaying a page that has the messages box in
279 it.
280
281 =head2 Authentication hacks
282
283 The next series of hacks deals with providing the concept of a "user" for
284 a site, and what you do with one when you've got one.
285
286 =head3 Logging In
287
288 You need the concept of a "current user".
289
290 B<Solution>: Use something like
291 C<Maypole::Authentication::UserSessionCookie> to authenticate a user
292 against a user class and store a current user object in the request
293 object.
294
295 C<UserSessionCookie> provides the C<get_user> method which tries to get
296 a user object, either based on the cookie for an already authenticated
297 session, or by comparing C<username> and C<password> form parameters
298 against a C<user> table in the database. Its behaviour is highly
299 customizable, so see the documentation, or the authentication paper at
300 C<http://maypole.simon-cozens.org/docs/authentication.html> for examples.
301
302 =head3 Pass-through login
303
304 You want to intercept a request from a non-logged-in user and have
305 them log in before sending them on their way to wherever they were
306 originally going.
307
308 B<Solution>:
309
310     sub authenticate {
311         my ($self, $r) = @_;
312         $r->get_user;
313         return OK if $r->{user};
314         # Force them to the login page.
315         $r->{template} = "login";
316         return OK;
317     }
318
319 This will display the C<login> template, which should look something
320 like this:
321
322     [% INCLUDE header %]
323
324       <h2> You need to log in </h2>
325
326     <DIV class="login">
327     [% IF login_error %]
328        <FONT COLOR="#FF0000"> [% login_error %] </FONT>
329     [% END %]
330       <FORM ACTION="/[% request.path%]" METHOD="post">
331     Username: 
332         <INPUT TYPE="text" NAME="[% config.auth.user_field || "user" %]"> <BR>
333     Password: <INPUT TYPE="password" NAME="password"> <BR>
334     <INPUT TYPE="submit">
335     </FORM>
336     </DIV>
337
338 Notice that this request gets C<POST>ed back to wherever it came from, using
339 C<request.path>. This is because if the user submits correct credentials,
340 C<get_user> will now return a valid user object, and the request will pass
341 through unhindered to the original URL.
342
343 =head3 Logging Out
344
345 Now your users are logged in, you want a way of having them log out
346 again and taking the authentication cookie away from them, sending
347 them back to the front page as an unprivileged user.
348
349 B<Solution>: This action, on the user class, is probably overkill, but
350 it does the job:
351
352     sub logout :Exported {
353         my ($class, $r) = @_;
354         # Remove the user from the request object
355         my $user = delete $r->{user};
356         # Destroy the session
357         tied(%{$r->{session}})->delete;
358         # Send a new cookie which expires the previous one
359         my $cookie = Apache::Cookie->new($r->{ar},
360             -name => $r->config->{auth}{cookie_name},
361             -value => undef,
362             -path => "/"
363             -expires => "-10m"
364         );
365         $cookie->bake();
366         # Template switcheroo
367         $r->template("frontpage");
368     }
369
370 =head3 Multi-level Authentication
371
372 You have both a global site access policy (for instance, requiring a
373 user to be logged in except for certain pages) and a policy for
374 particular tables. (Only allowing an admin to delete records in some
375 tables, say, or not wanting people to get at the default set of methods
376 provided by the model class.) 
377
378 You don't know whether to override the global C<authenticate> method or
379 provide one for each class.
380
381 B<Solution>: Do both. Have a global C<authenticate> method which calls 
382 a C<sub_authenticate> method based on the class:
383
384     sub authenticate {
385         ...
386         if ($r->{user}) {
387             return $r->model_class->sub_authenticate($r)
388                 if $r->model_class->can("sub_authenticate");
389             return OK;
390         }
391         ...
392     }
393
394 And now your C<sub_authenticate> methods can specify the policy for
395 each table:
396
397     sub sub_authenticate { # Ensure we can only create, reject or accept
398         my ($self, $r) = @_;
399         return OK if $r->{action} =~ /^(issue|accept|reject|do_edit)$/;
400         return;
401     }
402
403 =head2 Creating and editing hacks
404
405 These hacks particularly deal with issues related to the C<do_edit>
406 built-in action.
407
408 =head3 Limiting data for display
409
410 You want the user to be able to type in some text that you're later
411 going to display on the site, but you don't want them to stick images in
412 it, launch cross-site scripting attacks or otherwise insert messy HTML.
413
414 B<Solution>: Use the C<CGI::Untaint::html> module to sanitize the HTML
415 on input. C<CGI::Untaint::html> uses C<HTML::Sanitizer> to ensure that
416 tags are properly closed and can restrict the use of certain tags and
417 attributes to a pre-defined list.
418
419 Simply replace:
420
421     App::Table->untaint_columns(
422         text      => [qw/name description/]
423     );
424
425 with:
426
427     App::Table->untaint_columns(
428         html      => [qw/name description/]
429     );
430
431 And incoming HTML will be checked and cleaned before it is written to
432 the database.
433
434 =head3 Getting data from external sources
435
436 You want to supplement the data received from a form with additional
437 data from another source.
438
439 B<Solution>: Munge the contents of C< $r-E<gt>params > before jumping
440 to the original C<do_edit> routine. For instance, in this method,
441 we use a C<Net::Amazon> object to fill in some fields of a database row based
442 on an ISBN:
443
444     sub create_from_isbn :Exported {
445        my ($self, $r) = @_;
446        my $response = $ua->search(asin => $r->{params}{isbn});
447        my ($prop) = $response->properties;
448        # Rewrite the CGI parameters with the ones from Amazon
449        @{$r->{params}{qw(title publisher author year)} =            
450            ($prop->title,
451            $prop->publisher,
452            (join "/", $prop->authors()),
453            $prop->year());
454        # And jump to the usual edit/create routine
455        $self->do_edit($r);
456     }
457
458 The request will carry on as though it were a normal C<do_edit> POST, but
459 with the additional fields we have provided.
460
461 =head3 Uploading files and other data
462
463 You want the user to be able to upload files to store in the database.
464
465 B<Solution>: It's messy.
466
467 First, we set up an upload form, in an ordinary dummy action. Here's
468 the action:
469
470     sub upload_picture : Exported {}
471
472 And here's the template:
473
474     <FORM action="/user/do_upload" enctype="multipart/form-data" method="POST">
475
476     <P> Please provide a picture in JPEG, PNG or GIF format:
477     </P>
478     <INPUT TYPE="file" NAME="picture">
479     <BR>
480     <INPUT TYPE="submit">
481     </FORM>
482
483 (Although you'll probably want a bit more HTML around it than that.)
484
485 Now we need to write the C<do_upload> action. At this point we have to get a
486 little friendly with the front-end system. If we're using C<Apache::Request>,
487 then the C<upload> method of the C<Apache::Request> object (which
488 C<Apache::MVC> helpfully stores in C<$r-E<gt>{ar}>) will work for us:
489
490     sub do_upload :Exported {
491         my ($class, $r) = @_;
492         my $user = $r->{user};
493         my $upload = $r->{ar}->upload("picture");
494
495 This returns a C<Apache::Upload> object, which we can query for its
496 content type and a file handle from which we can read the data. It's
497 also worth checking the image isn't going to be too massive before we
498 try reading it and running out of memory, and that the content type is
499 something we're prepared to deal with. 
500
501     if ($upload) {
502         my $ct = $upload->info("Content-type");
503         return $r->error("Unknown image file type $ct")
504             if $ct !~ m{image/(jpeg|gif|png)};
505         return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)
506             if $upload->size > MAX_IMAGE_SIZE;
507
508         my $fh = $upload->fh;
509         my $image = do { local $/; <$fh> };
510
511 Now we can store the content type and data into our database, store it
512 into a file, or whatever:
513
514         $r->{user}->photo_type($ct);
515         $r->{user}->photo($image);
516     }
517
518 And finally, we use our familiar template switcheroo hack to get back to
519 a useful page:
520
521         $r->objects([ $user ]);
522         $r->{template} = "view";
523     }
524
525 Now, as we've mentioned, this only works because we're getting familiar with
526 C<Apache::Request> and its C<Apache::Upload> objects. If we're planning to use
527 C<CGI::Maypole> instead, or want to write our application in a generic way so
528 that it'll work regardless of front-end, then we need to replace the C<upload>
529 call with an equivalent which uses the C<CGI> module to get the upload data.
530 This is convoluted and horrific and we're not going to show it here, but it's
531 possible.
532
533 Combine with the "Displaying pictures" hack above for a happy time.