root/veekun/trunk/lib/Vee/Controller/Users.pm

Revision 406, 15.0 KB (checked in by eevee, 2 years ago)

Database refactoring. Renamed columns and tables to be more consistent and more readable. (#58)

Line 
1package Vee::Controller::Users;
2
3use strict;
4use warnings;
5use base 'Catalyst::Controller';
6
7use Vee::BBCode;
8use Vee::Utils;
9use Digest::SHA1 qw/sha1_hex/;
10use Image::Size;
11use Email::Valid;
12
13our @ContactTypes = (
14    { name => 'AIM', url => '', },
15    { name => 'ICQ', url => '', },
16    { name => 'MSN', url => '', },
17    { name => 'YIM', url => '', },
18    { name => 'LJ', url => 'http://%s.livejournal.com/', },
19    { name => 'Homepage', url => '%s', },
20    { name => 'email', url => 'mailto:%s', },
21);
22
23=head1 NAME
24
25Vee::Controller::Users - Users Controller
26
27=head1 SYNOPSIS
28
29See L<Vee>
30
31=head1 DESCRIPTION
32
33Catalyst Controller for logging in/out, registration, and viewing user information.
34
35=head1 METHODS
36
37=cut
38
39=head2 register
40
41Creates a new account.
42
43=cut
44
45my $register_fields = {
46    username => { type => 'text', size => 20, maxlength => 20 },
47    password => { type => 'password', size => 20, maxlength => 20, count => 2 },
48    remember => { type => 'checkbox', default => 1 },
49};
50
51sub register : Local {
52    my ($self, $c) = @_;
53    my $s = $c->stash;
54    my $p = $c->req->params;
55   
56    $s->{page_title} = "Register";
57
58    if ($c->user) {
59        $c->vee_stop("You are already logged in as ", $c->user->name, ".");
60    }
61   
62    my $form = Vee::Form->new(
63        id => 'register',
64        fields => $register_fields,
65        params => $c->req->params,
66    );
67
68    $s->{form} = $form;
69    $s->{template} = 'users/register.tt';
70
71    if (!$form->submitted) {
72        return;
73    }
74
75    # quietly make the username a little more HTML-friendly
76    $p->{username} =~ s/^ +| +$//g;
77    $p->{username} =~ s/ {2,}/ /g;
78
79    # check for sensible username/password
80    my @errors;
81    if (!ref $p->{password} or $#{ $p->{password} } != 1 or $p->{password}[0] ne $p->{password}[1]) {
82        push @errors, 'Your password must be typed exactly the same way twice.';
83    } elsif ($p->{password}[0] eq '') {
84        push @errors, 'Please enter a password.';
85    } elsif (6 > length $p->{password}[0]) {
86        push @errors, 'Your password must be at least six characters long.';
87    }
88    if ($p->{username} eq '') {
89        push @errors, 'Please enter a username.';
90    } elsif ($p->{username} !~ /^[-_ a-zA-Z0-9]{1,20}$/) {
91        push @errors, 'Your username must be at least one character and no more than 20.  It can only contain letters, numbers, dashes, underscores, and spaces.';
92    } elsif ($p->{username} !~ /\S/) {
93        push @errors, 'No, you can\'t have just spaces for a username.';
94    } elsif ( $c->model('DBIC::Users')->count({ name => $p->{username} }) ) {
95        push @errors, 'The username you selected is already taken.';
96    }
97    if (@errors) {
98        $s->{error_msg} = \@errors;
99        return;
100    }
101   
102    # create new user and log in
103    $c->model('DBIC::Users')->create({
104        name => $p->{username},
105        password => sha1_hex( $p->{password}[0] ),
106        time_joined => time,
107        time_active => time,
108        signature => '',
109        thread_view_cutoff => time,
110    });
111    $c->login($p->{username}, $p->{password}[0]) or die "Can't log in new user $p->{username}.  Should not happen, ever, period.";
112
113    $c->flash->{success_msg} = 'Your account has been created and you have been logged in, ' . $c->vee_cleanse($c->user->name) . '.';
114   
115    $c->session->{session_cookie} = !$p->{remember};
116   
117    # given that the referer is the registration form, the logical redirect is the new user's userinfo
118    $c->res->redirect( $c->uri('Users', 'info', $c->user->obj->id) );
119}
120
121=head2 login
122
123Logs a user in, or shows a login form if there is no POSTdata.
124
125=cut
126
127my $login_fields = {
128    username => { type => 'text', size => 20, maxlength => 20 },
129    password => { type => 'password', size => 20, maxlength => 20 },
130    remember => { type => 'checkbox', default => 1 },
131};
132
133sub login : Local {
134    my ($self, $c) = @_;
135    my $s = $c->stash;
136   
137    $s->{page_title} = "Log In";
138
139    if ($c->user) {
140        $c->vee_stop("You are already logged in as ", $c->user->name, ".");
141    }
142   
143    my $form = Vee::Form->new(
144        id => 'biglogin',
145        fields => $login_fields,
146        params => $c->req->params,
147    );
148    $s->{form} = $form;
149    $s->{template} = 'users/login.tt';
150
151    my $username = $c->req->params->{username};
152    my $password = $c->req->params->{password};
153   
154    # if parameters missing, just show login page
155    if (!$form->submitted) {
156        return;
157    }
158   
159    # attempt to log in
160    if ($c->login($username, $password)) {
161        $c->flash->{success_msg} = 'You are now logged in.  Welcome back, ' . $c->vee_cleanse($c->user->name) . '!';
162       
163        $c->session->{session_cookie} = !$c->req->params->{remember};
164       
165        # if we don't know where the user came from or e came from the login
166        #   form, send back to the main page; otherwise, send to referer
167        $c->res->redirect(
168            !$c->req->referer || $c->req->referer =~ m#/login/?$#
169            ? $c->uri_for('/')
170            : $c->req->referer
171        );
172    } else {
173        $s->{error_msg} = "Bad username or password.";
174    }
175}
176
177=head2 logout
178
179Logs the user out.
180
181=cut
182
183sub logout : Local {
184    my ($self, $c) = @_;
185
186    $c->logout;
187    $c->flash->{success_msg} = 'You have been logged out.  Bye  :(';
188   
189    $c->res->redirect( $c->req->referer or $c->uri_for('/') );
190}
191
192=head2 list
193
194Userlist. Displays some basic info about a user in a list. User names link to /users/info/x.
195
196=cut
197
198sub list : Path : Args(0) {
199    my ($self, $c) = @_;
200    my $s = $c->stash;
201   
202    my $order  = $c->req->params->{order} || 'id';
203    my $sort   = $c->req->params->{sort} || 'asc';
204    my $filter = $c->req->params->{filter};
205    my $skip   = $c->req->params->{skip} || 0;
206       
207    unless (Vee::Utils::in($order => qw/id name time_active time_joined post_count/)) { $order = 'id'; }
208    unless (Vee::Utils::in($sort => qw/asc desc/)) { $sort = 'asc'; }
209   
210    my $users_rs = $c->model('DBIC::Users')->search(
211            ( $filter ? ( 'me.name' => { like => '%' . $filter . '%' } ) : (undef) ),
212            { order_by => 'me.' . $order . ' ' . $sort . '', offset => $skip, rows => $c->site_opts->{page_sizes}->{users} }
213    );
214   
215    $s->{page_title}   = 'User List - Page ' . ($skip / 10 + 1) . '';
216    $s->{extra_css}    = [qw/users forum/];
217    $s->{breadcrumbs}  = ['Users'];
218
219    $s->{users_rs}     = $users_rs;
220    $s->{user_count}   = $c->model('DBIC::Users')->count( $filter ? { 'me.name' => { like => '%$filter%' } } : () );
221    $s->{params}       = $c->req->params;
222        $s->{ContactTypes} = \@ContactTypes;
223    $s->{template}     = 'users/list.tt';
224}
225
226=head2 info
227
228Displays the profile/userinfo/whatever for a user.
229
230=cut
231
232sub info : Path : Args(1) {
233    my ($self, $c) = @_;
234    my $s = $c->stash;
235
236    my $user = $c->model('DBIC::Users')->search({ id => $c->req->args->[0] })->single
237        or $c->vee_abort('There is currently no user with id ', $c->req->args->[0], '.');
238
239    $s->{page_title}   = $user->name;
240    $s->{link_name }   = 'user/info';
241    $s->{extra_css }   = [qw/users forum/];
242    $s->{crumbs    }   = [ '<a href="' . $c->uri('User') . '">Users</a>', $user->name ];
243    $s->{ContactTypes} = \@ContactTypes;
244    $s->{recent_post}  = $c->model('DBIC::Posts')->search({ -and => [ user_id => $c->req->args->[0], \'NOT FIND_IN_SET("deleted", me.flags)' ] }, {
245        order_by => 'me.time DESC',
246        rows => 1
247    } )->single;
248
249    $s->{user} = $user;
250    $s->{template} = 'users/info.tt';
251}
252
253=head2 edit
254
255User info editing, for changing AIM/MSN/avatar/sig/etc.
256
257=cut
258
259sub edit : Local : Args(0) {
260    my ($self, $c) = @_;
261    my $s = $c->stash;
262    my %changes;
263    my (@errors, @success);
264
265    if (not $c->user) { $c->vee_abort('You must be logged in to edit user info.'); }   
266
267    $s->{page_title} = 'Edit Options for ' . $c->user->name;
268    $s->{link_name } = 'user/edit';
269    $s->{crumbs    } = [ '<a href="' . $c->uri('User') . '">Users</a>', 'Edit ' . $c->user->name ];
270
271    $s->{userinfo  } = $c->user->obj;
272        $s->{extra_css } = [qw/users forum/];
273    $s->{template} = 'users/edit.tt';
274
275    return unless %{ $c->req->params };
276
277    # Check to see what has been changed, and what hasn't (this block IGNORES avatars, it assumes it has been changed)
278    for my $k (keys %{ $c->req->params }) {
279        unless (Vee::Utils::in($k => qw/submit reset avatardel avatarurl avatarfile/)) {
280            $changes{$k} = $c->req->params->{$k} unless $c->req->params->{$k} eq $c->user->obj->$k;
281        }
282        if ($k eq 'avatarfile' or $k eq 'avatarurl') { $changes{avatar} = $c->req->params->{$k}; }
283    }
284   
285    # Setup various variables?
286    my $query = $c->req->params;
287    my $padid = Vee::Utils::pad($c->user->obj->id, 11);
288    my $user_limits = $c->site_opts->{user_limits};
289    my $avmaxbytes = $user_limits->{avatarbytes};
290    my $avmaxpixels = $user_limits->{avatarpixels};
291    my $contacts = $user_limits->{contacts};
292
293    # Working with avatars; to me, this seems oddly.. disorganized and messy. If anyone wants to recode it, please, be my guest.
294    if ($query->{avatardel}) {
295        # Compared to the rest, this is simple; delete the avatar?
296        unlink $c->path_to('root/images/avatars', $c->user->obj->avatar);
297        $changes{avatar} = '';
298        push @success, "Deleted your avatar.";
299
300    } elsif (exists $query->{avatarfile} or $query->{avatarurl}) {
301        my $av;
302
303        # Check which method the user is trying to use for avatar: uploaded file, or URL?
304        if (exists $query->{avatarfile}) {
305            # This is for file (obviously); very straight forward, simply uploads the file.
306            $av = $c->request->upload('avatarfile');
307
308            # Gets the height/width/filetype of the file
309            my ($width, $height, $type) = imgsize($av->tempname);
310            if ($avmaxbytes * 1024 < $av->size) { push @errors, 'Filesize for avatar is bigger than maximum of ' . $avmaxbytes . ' kibibytes.'; delete $changes{avatar}; }
311            if (not Vee::Utils::in(lc $type => qw/png gif jpg jpeg/)) { push @errors, 'Avatar is using an invalid filetype (' . lc $type . ').'; delete $changes{avatar}; }
312            if ($width > $avmaxpixels or $height > $avmaxpixels) { push @errors, 'The height and/or width of your avatar is too large.'; delete $changes{avatar}; }
313
314            my $target = $c->path_to('root/images/avatars', $padid);
315            if ($changes{avatar}) {
316                unless ($av->link_to($target) || $av->copy_to($target) ) { $c->vee_abort("Failed to copy '" . $padid . "' to '" . $target . "': " . $!); }
317                push @success, "Updated your avatar from file $query->{avatarfile}";
318                $changes{avatar} = $padid;
319            }
320
321        } elsif (exists $query->{avatarurl}) {
322            # Check to see if the filesize of the remote avatar follows the restrictions of the max avatar size. I dunno how to explain it, really; I'm sure you who is reading this can understand?
323            my $ua = new LWP::UserAgent;
324            $ua->agent( $c->site_opts->{title} );
325            $ua->env_proxy;
326            $ua->protocols_allowed([ 'http', 'https' ]);
327            $ua->timeout(10);  # this is pretty low, but we don't want to keep the user waiting for too long
328
329            my $req = new HTTP::Request('HEAD' => $query->{avatarurl});
330            $req->header('Accept' => 'text/html');
331
332            my $response = $ua->request($req);
333            unless ($response->is_success) { $c->vee_abort('Remote file not found; maybe the server is down or you mistyped the URL?') }
334            my $header = $response->headers;
335            $req = HTTP::Request->new(GET => $query->{avatarurl});
336            $av = $ua->request($req)->content;
337
338            # Do error checking
339            my ($width, $height, $type) = imgsize(\$av);
340            if ($width > $avmaxpixels or $height > $avmaxpixels) { push @errors, 'The height and/or width of your avatar is too large.'; delete $changes{avatar}; }
341            if (not Vee::Utils::in(lc $type => qw/png gif jpg jpeg/)) { push @errors, 'Avatar is using an invalid filetype (.' . lc $type . ').'; delete $changes{avatar}; }
342            if ($avmaxbytes * 1024 < $header->content_length) { push @errors, 'Filesize for avatar is bigger than ' . $avmaxbytes . ' kibibytes.'; delete $changes{avatar}; }
343
344            # Okay, we're good; copy the file to $padid (which is the user's id)!               
345            if ($changes{avatar}) { open AVATAR, '>' . $c->path_to('root/images/avatars', $padid); binmode AVATAR; print AVATAR $av; close AVATAR; $changes{avatar} = $padid; push @success, "Updated your avatar from $query->{avatarurl}"; }
346        }
347    }   
348
349    # Working with text data (sigs, title, etc)
350    # Title
351    if ($changes{custom_title}) {
352        # no fathomable reason this should ever be needed un-filtered, so just sanitize it now
353        $changes{custom_title} =~ s:[\x0d\x0a]::g;
354        $changes{custom_title} = Vee::Utils::cleanse( $changes{custom_title} );
355
356        if (length $query->{custom_title} >= $user_limits->{custom_title}) {
357            push @errors, 'Custom title is longer than the maximum of ' . $user_limits->{custom_title} . ' characters.';
358            delete $changes{custom_title};
359        } else {
360            push @success, 'Updated your custom title to "' . $changes{custom_title} . '".';
361        }
362    } elsif (exists $changes{custom_title} and $c->user->custom_title) {
363        push @success, 'Deleted your custom title.';
364    }
365
366    # Various contacts (AIM, MSN, ICQ, etc)
367    for my $contact_row (@ContactTypes) {
368        my $k = $contact_row->{name};
369        my $join = "contact_" . lc $k;
370        next unless exists $changes{$join};
371        if (not $c->req->params->{$join} and $c->user->obj->$join) {
372            push @success, 'Deleted your ' . $k . ' contact information.';
373            $changes{$join} = '';
374            next;
375        }
376        if ($changes{$join}) {
377            # Validate email (and MSN!) to RFC822
378            if ((lc $k eq 'email' or lc $k eq 'msn') and not Email::Valid->address($changes{$join})) {
379                push @errors, 'The ' . $k . ' address ' . $changes{$join} . ' is invalid.';
380                delete $changes{$join};
381                next;
382            }
383            if ($k eq 'homepage') {
384                push @success, 'Updated your homepage to: ' . $changes{contact_homepage};
385            } else {
386                push @success, 'Updated your ' . $k . ' contact information to ' . $changes{$join} . '.';
387            }
388        }
389    }
390   
391    # Signature
392    if ($changes{signature}) {
393        ($changes{signature}, my @bbcode_errors) = Vee::BBCode::validate_bbcode( $changes{signature} );
394        if (@bbcode_errors) {
395            push @errors, 'Your signature contains invalid bbcode; please go back and fix it.';
396            delete $changes{signature};
397            next;
398        }
399        push @success, 'Updated your signature.';
400    } elsif (exists $changes{signature} and $c->user->signature) {
401        push @success, 'Deleted your signature.';
402    }
403   
404    $s->{success_msg} = \@success if @success;
405    $s->{error_msg} = \@errors if @errors;
406    $c->user->obj->update(\%changes);
407}
408   
409=head1 AUTHOR
410
411Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)
412
413See the included F<AUTHORS> file for a full list of contributers.
414
415=head1 LICENSE
416
417See the included F<LICENSE> file.
418
419=cut
420
4211;
Note: See TracBrowser for help on using the browser.