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