=head1 Flox: A Free Social Networking Site Friendster, Tribe, and now Google's Orkut - it seems like in early 2004, everyone wanted to be a social networking site. At the time, I was too busy to be a social networking site, as I was working on my own project at the time - Maypole. However, I realised that if I could implement a social networking system using Maypole, then Maypole could probably do anything. I'd already decided there was room for a free, open-source networking site, and then Peter Sergeant came up with the hook - localizing it to universities and societies, and tying in meet-ups with restaurant bookings. I called it Flox, partially because it flocks people together and partially because it's localised for my home town of Oxford and its university student population. Flox is still in, uh, flux, but it does the essentials. We're going to see how it was put together, and how the techniques shown in the L chapter can help to create a sophisticated web application. Of course, I didn't have this manual available at the time, so it took a bit longer than it should have done... =head2 Mapping the concepts Any Maypole application should start with two things: a database schema, and some idea of what the pages involved are going to look like. Usually, these pages will be tying to displaying or editing some element of the database, so these two concepts should come hand in hand. When I started looking at social networking sites, I began by identifying the concepts which were going to make up the tables of the application. At its most basic, a site like Orkut or Flox has two distinct concepts: a user, and a connection between two users. Additionally, there's the idea of an invitation to a new user, which can be extended, accepted, declined or ignored. These three will make up the key tables; there are an extra two tables in Flox, but they're essentially enumerations that are a bit easier to edit: each user has an affiliation to a particular college or department, and a status in the university. (Undergraduate, graduate, and so on.) For this first run-through, we're going to ignore the ideas of societies and communities, and end up with a schema like so: CREATE TABLE user ( id int not null auto_increment primary key, first_name varchar(50), last_name varchar(50), email varchar(255), profile text, password varchar(255), affiliation int, unistatus int, status ENUM("real", "invitee"), photo blob, photo_type varchar(30) ); CREATE TABLE connection ( id int not null auto_increment primary key, from_user int, to_user int, status ENUM("offered", "confirmed") ); CREATE TABLE invitation ( id char(32) not null primary key, issuer int, recipient int, expires date ); Plus the definition of our two auxilliary tables: CREATE TABLE affiliation ( id int not null auto_increment primary key, name varchar(255) ); CREATE TABLE unistatus ( id int not null auto_increment primary key, name varchar(255) ); Notice that, for simplicity, invitations and friendship connections are quite similar: they are extended from one user to another. This means that people who haven't accepted an invite yet still have a place in the user table, with a different C. Similarly, a connection between users can be offered, and when it is accepted, its status is changed to "confirmed" and a reciprocal relationship put in place. We also have some idea, based on what we want to happen, of what pages and actions we're going to define. Leaving the user aside for the moment, we want an action which extends an invitation from the current user to a new user. We want a page the new user can go to in order to accept that invitation. Similarly, we want an action which offers a friendship connection to an existing user, and a page the user can go to to accept or reject it. This gives us five pages so far: invitation/issue invitation/accept user/befriend connection/accept connection/reject Notice that the C action is performed on a user, not a connection. This is distinct from C because when befriending, we have a real user on the system that we want to do something to. This makes sense if you think of it in terms of object oriented programming - we could say Flox::Connection->create(to => $user) but it's clearer to say $user->befriend Similarly, we could say Flox::User->create({ ... })->issue_invitation_to but it's clearer to say Flox::Invitation->issue( to => Flox::User->create({ ... }) ) because it more accurately reflects the principal subject and object of these actions. Returning to look at the user class, we want to be able to view a user's profile, edit one's own profile, set up the profile for the first time, upload pictures and display pictures. We also need to handle the concepts of logging in and logging out. As usual, though, we'll start with a handler class which sets up the database: package Flox; use base qw(Apache::MVC); Flox->setup("dbi:mysql:flox"); Flox->config->{display_tables} = [qw[user invitation connection]]; 1; Very simple, as these things are meant to be. Now let's build on it. =head2 Authentication The concept of a current user is absolutely critical in a site like Flox; it represents "me", the viewer of the page, as the site explores the connections in my world. We've described the authentication hacks briefly in the L chapter, but now it's time to go into a little more detail about how user handling is done. XXX =head2 Viewing a user The first page that a user will see after logging in will be their own profile, so in order to speed development, we'll start by getting a C page up. The only difference from a programming point of view between this action and the default C action is that, if no user ID is given, then we want to view "me", the current user. Remembering that the default view action does nothing, our C action only needs to do nothing plus ensure it has a user in the C slot, putting C<$r-E{user}> in there if not: sub view :Exported { my ($class, $r) = @_; $r->{objects} = [ $r->{user} ] unless @{$r->{objects}||[]}; } Maypole, unfortunately, is very good at making programming boring. The downside of having to write very little code at all is that we now have to spend most of our time writing nice HTML for the templates. XXX The next stage is viewing the user's photo. Assuming we've got the photo stored in the database already (which is a reasonable assumption for the moment since we don't have a way to upload a photo quite yet) then we can use the a variation of the "Displaying pictures" hack from the Requests chapter: sub view_picture :Exported { my ($self, $r) = @_; my $user = $r->{objects}->[0] || $r->{user}; if ($r->{content_type} = $user->photo_type) { $r->{output} = $user->photo; } else { # Read no-photo photo $r->{content_type} = "image/png"; $r->{output} = slurp_file("images/no-photo.png"); } } We begin by getting a user object, just like in the C action: either the user whose ID was passed in on the URL, or the current user. Then we check if a C has been set in this user's record. If so, then we'll use that as the content type for this request, and the data in the C attribute as the data to send out. The trick here is that setting C<$r-E{output}> overrides the whole view class processing and allows us to write the content out directly. In our template, we can now say and the appropriate user's mugshot will appear. However, if we're throwing big chunks of data around like C, it's now worth optimizing the C class to ensure that only pertitent data is fetched by default, and C and friends are only fetched on demand. The "lazy population" section of C's man page explains how to group the columns by usage so that we can optimize fetches: Flox::User->columns(Primary => qw/id/); Flox::User->columns(Essential => qw/status/); Flox::User->columns(Helpful => qw/ first_name last_name email password/) Flox::User->columns(Display => qw/ profile affiliation unistatus /); Flox::User->columns(Photo => qw/ photo photo_type /); This means that the status and ID columns will always be retrieved when we deal with a user; next, any one of the name, email or password columns will cause that group of data to be retrieved; if we go on to display more information about a user, we also load up the profile, affiliation and university status; finally, if we're throwing around photos, then we load in the photo type and photo data. These groupings are somewhat arbitrary, and there needs to be a lot of profiling to determine the most efficient groupings of columns to load, but they demonstrate one principle about working in Maypole: this is the first time in dealing with Maypole that we've had to explicitly list the columns of a table, but Maypole has so far Just Worked. There's a difference, though, between Maypole just working and Maypole working well, and if you want to optimize your application, then you need to start putting in the code to do that. The beauty of Maypole is that you can do as much or as little of such optimization as you want or need. So now we can view users and their photos. It's time to allow the users to edit their profiles and upload a new photo. =head2 Editing users XXX Editing a profile I introduced Flox to a bunch of friends and told them to be as ruthless as possible in finding bugs and trying to break it. And break it they did; within an hour the screens were thoroughly messed up as users had nasty HTML tags in their profiles, names, email addresses and so on. This spawned another hack in the request cookbook: "Limiting data for display". I changed the untaint columns to use C untainting, and all was better: Flox::User->untaint_columns( html => [qw/first_name last_name profile/], printable => [qw/password/], integer => [qw/affiliation unistatus /], email => [qw/email/] ); The next stage was the ability to upload a photo. We unleash the "Uploading files" recipe, with an additional check to make sure the photo is of a sensible size: use constant MAX_IMAGE_SIZE => 512 * 1024; sub do_upload :Exported { my ($class, $r) = @_; my $user = $r->{user}; my $upload = $r->{ar}->upload("picture"); if ($upload) { my $ct = $upload->info("Content-type"); 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 $upload->size > MAX_IMAGE_SIZE; my $fh = $upload->fh; my $image = do { local $/; <$fh> }; use Image::Size; my ($x, $y) = imgsize(\$image); return $r->error("Image too big! ($x, $y) Maximum size is 350x350") if $y > 350 or $x > 350; $r->{user}->photo_type($ct); $r->{user}->photo($image); } $r->objects([ $user ]); $r->{template} = "view"; } Now we've gone as far as we want to go about user editing at the moment. Let's have a look at the real meat of a social networking site: getting other people involved, and registering connections between users. =head2 Invitations =head2 Friendship Connections =head2