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