1Maypole::Manual::Flox(3U)ser Contributed Perl DocumentatiMoanypole::Manual::Flox(3)
2
3
4

NAME

6       Maypole::Manual::Flox - Flox: A Free Social Networking Site
7

DESCRIPTION

9       Friendster, Tribe, and now Google's Orkut - it seems like in early
10       2004, everyone wanted to be a social networking site. At the time, I
11       was too busy to be a social networking site, as I was working on my own
12       project at the time - Maypole. However, I realised that if I could
13       implement a social networking system using Maypole, then Maypole could
14       probably do anything.
15
16       I'd already decided there was room for a free, open-source networking
17       site, and then Peter Sergeant came up with the hook - localizing it to
18       universities and societies, and tying in meet-ups with restaurant book‐
19       ings. I called it Flox, partially because it flocks people together and
20       partially because it's localised for my home town of Oxford and its
21       university student population.
22
23       Flox is still in, uh, flux, but it does the essentials. We're going to
24       see how it was put together, and how the techniques shown in the
25       Request Cookbook can help to create a sophisticated web application. Of
26       course, I didn't have this manual available at the time, so it took a
27       bit longer than it should have done...
28
29       Mapping the concepts
30
31       Any Maypole application should start with two things: a database
32       schema, and some idea of what the pages involved are going to look
33       like.  Usually, these pages will be displaying or editing some element
34       of the database, so these two concepts should come hand in hand.
35
36       When I started looking at social networking sites, I began by identify‐
37       ing the concepts which were going to make up the tables of the applica‐
38       tion. At its most basic, a site like Orkut or Flox has two distinct
39       concepts: a user, and a connection between two users.  Additionally,
40       there's the idea of an invitation to a new user, which can be extended,
41       accepted, declined or ignored. These three will make up the key tables;
42       there are an extra two tables in Flox, but they're essentially enumera‐
43       tions that are a bit easier to edit: each user has an affiliation to a
44       particular college or department, and a status in the university.
45       (Undergraduate, graduate, and so on.)
46
47       For this first run-through, we're going to ignore the ideas of soci‐
48       eties and communities, and end up with a schema like so:
49
50           CREATE TABLE user (
51               id int not null auto_increment primary key,
52               first_name varchar(50),
53               last_name varchar(50),
54               email varchar(255),
55               profile text,
56               password varchar(255),
57               affiliation int,
58               unistatus int,
59               status ENUM("real", "invitee"),
60               photo blob,
61               photo_type varchar(30)
62           );
63
64           CREATE TABLE connection (
65               id int not null auto_increment primary key,
66               from_user int,
67               to_user int,
68               status ENUM("offered", "confirmed")
69           );
70
71           CREATE TABLE invitation (
72               id char(32) not null primary key,
73               issuer int,
74               recipient int,
75               expires date
76           );
77
78       Plus the definition of our two auxiliary tables:
79
80           CREATE TABLE affiliation (
81               id int not null auto_increment primary key,
82               name varchar(255)
83           );
84
85           CREATE TABLE unistatus (
86               id int not null auto_increment primary key,
87               name varchar(255)
88           );
89
90       Notice that, for simplicity, invitations and friendship connections are
91       quite similar: they are extended from one user to another. This means
92       that people who haven't accepted an invite yet still have a place in
93       the user table, with a different "status". Similarly, a connection
94       between users can be offered, and when it is accepted, its status is
95       changed to "confirmed" and a reciprocal relationship put in place.
96
97       We also have some idea, based on what we want to happen, of what pages
98       and actions we're going to define. Leaving the user aside for the
99       moment, we want an action which extends an invitation from the current
100       user to a new user. We want a page the new user can go to in order to
101       accept that invitation. Similarly, we want an action which offers a
102       friendship connection to an existing user, and a page the user can go
103       to to accept or reject it. This gives us five pages so far:
104
105           invitation/issue
106           invitation/accept
107
108           user/befriend
109           connection/accept
110           connection/reject
111
112       Notice that the "befriend" action is performed on a user, not a connec‐
113       tion. This is distinct from "invitation/issue" because when befriend‐
114       ing, we have a real user on the system that we want to do something to.
115       This makes sense if you think of it in terms of object oriented pro‐
116       gramming - we could say
117
118           Flox::Connection->create(to => $user)
119
120       but it's clearer to say
121
122           $user->befriend
123
124       Similarly, we could say
125
126           Flox::User->create({ ... })->issue_invitation_to
127
128       but it's clearer to say
129
130           Flox::Invitation->issue( to => Flox::User->create({ ... }) )
131
132       because it more accurately reflects the principal subject and object of
133       these actions.
134
135       Returning to look at the user class, we want to be able to view a
136       user's profile, edit one's own profile, set up the profile for the
137       first time, upload pictures and display pictures. We also need to han‐
138       dle the concepts of logging in and logging out.
139
140       As usual, though, we'll start with a handler class which sets up the
141       database:
142
143           package Flox;
144           use Maypole::Application;
145           Flox->setup("dbi:mysql:flox");
146           Flox->config->display_tables([qw[user invitation connection]]);
147           1;
148
149       Very simple, as these things are meant to be. Now let's build on it.
150
151       Users and Authentication
152
153       The concept of a current user is absolutely critical in a site like
154       Flox; it represents "me", the viewer of the page, as the site explores
155       the connections in my world. We've described the authentication hacks
156       briefly in the Request Cookbook, but now it's time to go into a little
157       more detail about how user handling is done.
158
159       We also want to be able to refer to the current user from the tem‐
160       plates, so we use the overridable "additional_data" method in the
161       driver class to give us a "my" template variable:
162
163           sub additional_data {
164               my $r = shift; $r->template_args->{my} = $r->user;
165           }
166
167       I've called it "my" rather than "me" because we it lets us check "[%
168       my.name %]", and so on.
169
170       Viewing a user
171
172       The first page that a user will see after logging in will be their own
173       profile, so in order to speed development, we'll start by getting a
174       "user/view" page up.
175
176       The only difference from a programming point of view between this
177       action and the default "view" action is that, if no user ID is given,
178       then we want to view "me", the current user. Remembering that the
179       default view action does nothing, our "Flox::User::view" action only
180       needs to do nothing plus ensure it has a user in the "objects" slot,
181       putting "$r->{user}" in there if not:
182
183           sub view :Exported {
184               my ($class, $r) = @_;
185               $r->objects([ $r->user ]) unless @{ $r->objects ⎪⎪ [] };
186           }
187
188       Maypole, unfortunately, is very good at making programming boring. The
189       downside of having to write very little code at all is that we now have
190       to spend most of our time writing nice HTML for the templates.
191
192       Pictures of Users
193
194       The next stage is viewing the user's photo. Assuming we've got the
195       photo stored in the database already (which is a reasonable assumption
196       for the moment since we don't have a way to upload a photo quite yet)
197       then we can use a variation of the "Displaying pictures" hack from the
198       Request Cookbook:
199
200           sub view_picture :Exported {
201               my ($self, $r) = @_;
202               my $user = $r->objects->[0] ⎪⎪ $r->user;
203               if ($r->content_type($user->photo_type)) {
204                  $r->output($user->photo);
205               } else {
206                  # Read no-photo photo
207                  $r->content_type("image/png");
208                  $r->output(slurp_file("images/no-photo.png"));
209               }
210           }
211
212       We begin by getting a user object, just like in the "view" action:
213       either the user whose ID was passed in on the URL, or the current user.
214       Then we check if a "photo_type" has been set in this user's record. If
215       so, then we'll use that as the content type for this request, and the
216       data in the "photo" attribute as the data to send out. The trick here
217       is that setting "$r->{output}" overrides the whole view class process‐
218       ing and allows us to write the content out directly.
219
220       In our template, we can now say
221
222           <IMG SRC="[%base%]/user/view_picture/[% user.id %]">
223
224       and the appropriate user's mugshot will appear.
225
226       However, if we're throwing big chunks of data around like "photo", it's
227       now worth optimizing the "User" class to ensure that only pertitent
228       data is fetched by default, and "photo" and friends are only fetched on
229       demand. The "lazy population" section of Class::DBI's man page explains
230       how to group the columns by usage so that we can optimize fetches:
231
232           Flox::User->columns(Primary   => qw/id/);
233           Flox::User->columns(Essential => qw/status/);
234           Flox::User->columns(Helpful   => qw/ first_name last_name email password/)
235           Flox::User->columns(Display   => qw/ profile affiliation unistatus /);
236           Flox::User->columns(Photo     => qw/ photo photo_type /);
237
238       This means that the status and ID columns will always be retrieved when
239       we deal with a user; next, any one of the name, email or password col‐
240       umns will cause that group of data to be retrieved; if we go on to dis‐
241       play more information about a user, we also load up the profile, affil‐
242       iation and university status; finally, if we're throwing around photos,
243       then we load in the photo type and photo data.
244
245       These groupings are somewhat arbitrary, and there needs to be a lot of
246       profiling to determine the most efficient groupings of columns to load,
247       but they demonstrate one principle about working in Maypole: this is
248       the first time in dealing with Maypole that we've had to explicitly
249       list the columns of a table, but Maypole has so far Just Worked.
250       There's a difference, though, between Maypole just working and Maypole
251       working well, and if you want to optimize your application, then you
252       need to start putting in the code to do that. The beauty of Maypole is
253       that you can do as much or as little of such optimization as you want
254       or need.
255
256       So now we can view users and their photos. It's time to allow the users
257       to edit their profiles and upload a new photo.
258
259       Editing user profiles
260
261       I introduced Flox to a bunch of friends and told them to be as ruthless
262       as possible in finding bugs and trying to break it. And break it they
263       did; within an hour the screens were thoroughly messed up as users had
264       nasty HTML tags in their profiles, names, email addresses and so on.
265       This spawned another hack in the request cookbook: "Limiting data for
266       display". I changed the untaint columns to use "html" untainting, and
267       all was better:
268
269           Flox::User->untaint_columns(
270               html      => [qw/first_name last_name profile/],
271               printable => [qw/password/],
272               integer   => [qw/affiliation unistatus /],
273               email     => [qw/email/]
274           );
275
276       The next stage was the ability to upload a photo. We unleash the
277       "Uploading files" recipe, with an additional check to make sure the
278       photo is of a sensible size:
279
280           use constant MAX_IMAGE_SIZE => 512 * 1024;
281           sub do_upload :Exported {
282               my ($class, $r) = @_;
283               my $user = $r->user;
284               my $upload = $r->ar->upload("picture");
285               if ($upload) {
286                   my $ct = $upload->info("Content-type");
287                   return $r->error("Unknown image file type $ct")
288                       if $ct !~ m{image/(jpeg⎪gif⎪png)};
289                   return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)
290                       if $upload->size > MAX_IMAGE_SIZE;
291
292                   my $fh = $upload->fh;
293                   my $image = do { local $/; <$fh> };
294
295                   use Image::Size;
296                   my ($x, $y) = imgsize(\$image);
297                   return $r->error("Image too big! ($x, $y) Maximum size is 350x350")
298                       if $y > 350 or $x > 350;
299                   $r->user->photo_type($ct);
300                   $r->user->photo($image);
301               }
302
303               $r->objects([ $user ]);
304               $r->template("view");
305           }
306
307       Now we've gone as far as we want to go about user editing at the
308       moment.  Let's have a look at the real meat of a social networking
309       site: getting other people involved, and registering connections
310       between users.
311
312       Invitations
313
314       We need to do two things to make invitations work: first provide a way
315       to issue an invitation, and then provide a way to accept it. Since what
316       we're doing in issuing an invitation is essentially creating a new one,
317       we'll use our usual practice of having a page to display the form to
318       offer an invitation, and then use a "do_edit" method to actually do the
319       work. So our "issue" method is just an empty action:
320
321           sub issue :Exported {}
322
323       and the template proceeds as normal:
324
325           [% PROCESS header %]
326           <h2> Invite a friend </h2>
327
328           <FORM ACTION="[%base%]/invitation/do_edit/" METHOD="post">
329           <TABLE>
330
331       Now we use the "Catching errors in a form" recipe from the Request
332       Cookbook and write our form template:
333
334           <TR><TD>
335           First name: <INPUT TYPE="text" NAME="forename"
336           VALUE="[%request.params.forename%]">
337           </TD>
338           <TD>
339           Last name: <INPUT TYPE="text" NAME="surname"
340           VALUE="[%request.params.surname%]">
341           </TD></TR>
342           [% IF errors.forename OR errors.surname %]
343               <TR>
344               <TD><SPAN class="error">[% errors.forename %]</SPAN> </TD>
345               <TD><SPAN class="error">[% errors.surname %]</SPAN> </TD>
346               </TR>
347           [% END %]
348           <TR>
349           ...
350
351       Now we need to work on the "do_edit" action. This has to validate the
352       form parameters, create the invited user, create the row in the "invi‐
353       tation" table, and send an email to the new user asking them to join.
354
355       We'd normally use "create_from_cgi" to do the first two stages, but
356       this time we handle the untainting manually, because there are a sur‐
357       prising number of things we need to check before we actually do the
358       create. So here's the untainting of the parameters:
359
360           sub do_edit :Exported {
361               my ($self, $r) = @_;
362               my $h = CGI::Untaint->new(%{$r->params});
363               my (%errors, %ex);
364               for (qw( email forename surname )) {
365                   $ex{$_} = $h->extract(
366                           "-as_".($_ eq "email" ? "email" : "printable") => $_
367                   ) or $errors{$_} = $h->error;
368               }
369
370       Next, we do the usual dance of throwing the user back at the form in
371       case of errors:
372
373               if (keys %errors) {
374                   $r->template_args->{message} =
375                       "There was something wrong with that...";
376                   $r->template_args->{errors} = \%errors;
377                   $r->template("issue");
378                   return;
379               }
380
381       We've introduced a new template variable here, "message", which we'll
382       use to display any important messages to the user.
383
384       The first check we need to do is whether or not we already have a user
385       with that email address. If we have, and they're a real user, then we
386       abort the invite progress and instead redirect them to viewing that
387       user's profile.
388
389               my ($user) = Flox::User->search({ email => $ex{email} });
390               if ($user) {
391                   if ($user->status eq "real") {
392                       $r->template_args->{message} =
393                           "That user already seems to exist on Flox. ".
394                           "Is this the one you meant?";
395
396                       $self->redirect_to_user($r, $user);
397                   }
398
399       Where "redirect_to_user" looks like this:
400
401           sub redirect_to_user {
402               my ($self, $r, $user) = @_;
403               $r->objects([ $user ]);
404               $r->template("view");
405               $r->model_class("Flox::User"); # Naughty.
406           }
407
408       This is, as the comment quite rightly points out, naughty. We're cur‐
409       rently doing a "/invitation/do_edit/" and we want to turn this into a
410       "/user/view/xxx", changing the table, template and arguments all at
411       once.  To do this, we have to change the Maypole request object's idea
412       of the model class, since this determines where to look for the tem‐
413       plate: if we didn't, we'd end up with "invitation/view" instead of
414       "user/view".
415
416       Ideally, we'd do this with a Apache redirect, but we want to get that
417       "message" in there as well, so this will have to do. This isn't good
418       practice; we put it into a subroutine so that we can fix it up if we
419       find a better way to do it.
420
421       Anyway back in the "do_edit" action, this is what we should do if a
422       user already exists on the system and has accepted an invite already.
423       What if we're trying to invite a user but someone else has invited them
424       first and they haven't replied yet?
425
426                    } else {
427                       # Put it back to the form
428                       $r->template_args->{message} =
429                           "That user has already been invited; " .
430                           "please wait for them to accept";
431                       $r->template("issue");
432                    }
433                    return;
434               }
435
436       Race conditions suck.
437
438       Okay. Now we know that the user doesn't exist, and so can create the
439       new one:
440
441               my $new_user = Flox::User->create({
442                   email      => $ex{email},
443                   first_name => $ex{forename},
444                   last_name  => $ex{surname},
445                   status     => "invitee"
446               });
447
448       We want to give the invitee a URL that they can go to in order to
449       accept the invite. Now we don't just want the IDs of our invites to be
450       sequential, since someone could get one invite, and then guess the rest
451       of the invite codes. We provide a relatively secure MD5 hash as the
452       invite ID:
453
454               my $random = md5_hex(time.(0+{}).$$.rand);
455
456       For additional security, we're going to have the URL in the form
457       "/invitation/accept/id/from_id/to_id", encoding the user ids of the two
458       users. Now we can send email to the invitee to ask them to visit that
459       URL:
460
461               my $newid = $new_user->id;
462               my $myid  = $r->user->id;
463               _send_mail(to   => $ex{email},
464                          url  => "$random/$myid/$newid",
465                          user => $r->user);
466
467       I'm not going to show the "_send_mail" routine, since it's boring.  We
468       haven't actually created the "Invitation" object yet, so let's do that
469       now.
470
471               Flox::Invitation->create({
472                   id        => $random,
473                   issuer    => $r->user,
474                   recipient => $new_user,
475                   expires   => Time::Piece->new(time + LIFETIME)->datetime
476               });
477
478       You can also imagine a daily cron job that cleans up the "Invitation"
479       table looking for invitations that ever got replied to within their
480       lifetime:
481
482          ($_->expires > localtime && $_->delete)
483              for Flox::Invitation->retrieve_all;
484
485       Notice that we don't check whether the ID is already used. We could,
486       but, you know, if MD5 sums start colliding, we have much bigger prob‐
487       lems on our hands.
488
489       Anyway, now we've got the invitation created, we can go back to whence
490       we came: viewing the original user:
491
492               $self->redirect_to_user($r, $r->user);
493
494       Now our invitee has an email, and goes click on the URL. What happens?
495
496       XXX
497
498       Friendship Connections
499
500       XXX
501
502       Links
503
504       The source for Flox is available at <http://cvs.simon-coz
505       ens.org/viewcvs.cgi/flox>.
506
507       Contents, Next The Maypole iBuySpy Portal, Previous Maypole Request
508       Hacking Cookbook
509
510
511
512perl v5.8.8                       2005-11-23          Maypole::Manual::Flox(3)
Impressum