root/branches/release-36/lib/MT/App/Comments.pm @ 2062

Revision 2062, 62.6 kB (checked in by bchoate, 19 months ago)

Updates to blog-side javascript regarding user state and permissions. BugId:79077,69644,67754,69814,79258,62643. Fixed declarations for conditional tags. BugId:79476. Display auth'd user nickname rather than name from comment object. BugId:79475

  • Property svn:keywords set to Author Date Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::App::Comments;
8use strict;
9
10use base 'MT::App';
11
12use MT::Comment;
13use MT::I18N qw( wrap_text encode_text );
14use MT::Util
15  qw( remove_html encode_html encode_url decode_url is_valid_email is_valid_url is_url escape_unicode format_ts encode_js );
16use MT::Entry qw(:constants);
17use MT::Author;
18use MT::JunkFilter qw(:constants);
19
20sub id { 'comments' }
21
22sub init {
23    my $app = shift;
24    $app->SUPER::init(@_) or return;
25    $app->add_methods(
26        login            => \&login,
27        login_external   => \&login_external,
28        do_login         => \&do_login,
29        signup           => \&signup,
30        do_signup        => \&do_signup,
31        register         => \&register,
32        do_register      => \&do_register,
33        preview          => \&preview,
34        post             => \&post,
35        handle_sign_in   => \&handle_sign_in,
36        session_js       => \&session_js,
37        edit_profile     => \&edit_commenter_profile,
38        save_profile     => \&save_commenter_profile,
39        red              => \&do_red,
40        generate_captcha => \&generate_captcha,
41
42        # deprecated
43        cmtr_name_js     => \&commenter_name_js,
44        cmtr_status_js   => \&commenter_status_js,
45    );
46    $app->{template_dir} = 'comment';
47    $app->init_commenter_authenticators;
48    $app->init_captcha_providers();
49    MT->add_callback( 'CommentThrottleFilter', 1, undef,
50        \&MT::App::Comments::_builtin_throttle );
51    $app;
52}
53
54sub init_request {
55    my $app = shift;
56    $app->SUPER::init_request(@_);
57    $app->set_no_cache;
58    $app->{default_mode} = 'post';
59    my $q = $app->param;
60
61    ## We don't really have a __mode parameter, because we have to
62    ## use named submit buttons for Preview and Post. So we hack it.
63    if (   $q->param('post')
64        || $q->param('post_x')
65        || $q->param('post.x') )
66    {
67        $app->mode('post');
68    }
69    elsif ($q->param('preview')
70        || $q->param('preview_x')
71        || $q->param('preview.x') )
72    {
73        $app->mode('preview');
74    }
75    elsif ($q->param('reply')
76        || $q->param('reply_x')
77        || $q->param('reply.x') )
78    {
79        $app->mode('reply');
80    }
81    elsif ($q->param('reply_preview')
82        || $q->param('reply_preview_x')
83        || $q->param('reply_preview.x') )
84    {
85        $app->mode('reply_preview');
86    }
87    elsif ( $app->path_info =~ /captcha/ ) {
88        $app->mode('generate_captcha');
89    }
90}
91
92#
93# $app->_get_commenter_session()
94# Creates a commenter record based on the cookies in the $app, if
95# one already exists corresponding to the browser's session.
96#
97# Returns a pair ($session_key, $commenter) where $session_key is the
98# key to the MT::Session object (as well as the cookie value) and
99# $commenter is an MT::Author record. Both values are undef when no
100# session is active.
101#
102sub _get_commenter_session {
103    my $app = shift;
104    my $q   = $app->param;
105
106    my $session_key;
107
108    # First, check for a real MT user login. If one exists,
109    # return that as the commenter identity
110    my ($user, $first_time) = $app->SUPER::login();
111    if ( $user ) {
112        my $sess = $app->session;
113        return ( $sess->id, $user );
114    }
115
116    my %cookies = $app->cookies();
117    my $cookie_name = $app->commenter_cookie;
118    if ( !$cookies{$cookie_name} ) {
119        return ( undef, undef );
120    }
121    $session_key = $cookies{$cookie_name}->value() || "";
122    $session_key =~ y/+/ /;
123    my $cfg = $app->config;
124    require MT::Session;
125    my $sess_obj = MT::Session->load( { id => $session_key } );
126    my $timeout = $cfg->CommentSessionTimeout;
127    my $user;
128   
129    if ( $sess_obj
130        && ( $user = MT::Author->load( { name => $sess_obj->name } ) ) )
131    {
132        return ( $session_key, $user ) if $user->type eq MT::Author::AUTHOR();
133    }
134    if (   !$sess_obj
135        || ( $sess_obj->start() + $timeout < time )
136      )
137    {
138        $app->_invalidate_commenter_session( \%cookies );
139        return ( undef, undef );
140    }
141    else {
142        # session is valid!
143        return ( $session_key, $user );
144    }
145}
146
147sub login {
148    my $app   = shift;
149    my %param = @_;
150
151    my $param = {
152        blog_id => ($app->param('blog_id') || 0),
153        static  => ($app->param('static') || ''),
154        return_url => ($app->param('return_url') || ''),
155    };
156    $param->{entry_id} = $app->param('entry_id') if $app->param('entry_id');
157    while ( my ( $key, $val ) = each %param ) {
158        $param->{$key} = $val;
159    }
160
161    my $blog = MT::Blog->load( $param->{blog_id} );
162    my $external_authenticators = $app->external_authenticators($blog, $param);
163
164    if ( @$external_authenticators ) {
165        $param->{auth_loop}      = $external_authenticators;
166        $param->{default_signin} = $external_authenticators->[0]->{key}
167          unless exists $param->{default_signin};
168    }
169
170    $app->build_page( 'login.tmpl', $param );
171}
172
173sub login_external {
174    my $app = shift;
175    my $q   = $app->param;
176
177    my $authenticator = MT->commenter_authenticator( $q->param('key') );
178    my $auth_class    = $authenticator->{class};
179    eval "require $auth_class;";
180    if ( my $e = $@ ) {
181        return $app->handle_error( $e, 403 );
182    }
183    $auth_class->login($app);
184}
185
186sub _create_commenter_assign_role {
187    my $app = shift;
188    my ($blog_id) = @_;
189    require MT::Auth;
190    my $error = MT::Auth->sanity_check($app);
191    if ($error) {
192        $app->log(
193            {
194                message  => $error,
195                level    => MT::Log::ERROR(),
196                class    => 'system',
197                category => 'register_commenter'
198            }
199        );
200        return undef;
201    }
202    my $commenter = $app->model('author')->new;
203    $commenter->name( $app->param('username') );
204    $commenter->nickname( $app->param('nickname') );
205    $commenter->set_password( $app->param('password') );
206    $commenter->email( $app->param('email') );
207    $commenter->external_id( $app->param('external_id') );
208    $commenter->type( MT::Author::AUTHOR() );
209    $commenter->status( MT::Author::ACTIVE() );
210    $commenter->auth_type( $app->config->AuthenticationModule );
211    return undef unless ( $commenter->save );
212
213    require MT::Role;
214    require MT::Association;
215    my $role = MT::Role->load_same( undef, undef, 1, 'comment' );
216    my $blog = MT::Blog->load($blog_id);
217    if ( $role && $blog ) {
218        MT::Association->link( $commenter => $role => $blog );
219    }
220    else {
221        my $blog_name = $blog ? $blog->name : '(Blog not found)';
222        $app->log(
223            {
224                message => MT->translate(
225"Error assigning commenting rights to user '[_1] (ID: [_2])' for weblog '[_3] (ID: [_4])'. No suitable commenting role was found.",
226                    $commenter->name, $commenter->id,
227                    $blog_name,      $blog->id,
228                ),
229                level    => MT::Log::ERROR(),
230                class    => 'system',
231                category => 'new'
232            }
233        );
234    }
235    $app->user($commenter);
236    $commenter;
237}
238
239sub do_login {
240    my $app     = shift;
241    my $q       = $app->param;
242    my $name    = $q->param('username');
243    my $blog_id = $q->param('blog_id');
244    my $blog    = MT::Blog->load($blog_id)
245        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
246    my $auths   = $blog->commenter_authenticators;
247    if ( $auths !~ /MovableType/ ) {
248        $app->log(
249            {
250                message => $app->translate(
251'Invalid commenter login attempt from [_1] to blog [_2](ID: [_3]) which does not allow Movable Type native authentication.',
252                    $name, $blog->name, $blog_id
253                ),
254                level    => MT::Log::WARNING(),
255                category => 'login_commenter',
256            }
257        );
258        return $app->login( error => $app->translate('Invalid login.') );
259    }
260
261    require MT::Auth;
262    my $ctx = MT::Auth->fetch_credentials( { app => $app } );
263    $ctx->{blog_id} = $blog_id;
264    my $result = MT::Auth->validate_credentials($ctx);
265    my ($message, $error);
266    if (   ( MT::Auth::NEW_LOGIN() == $result )
267        || ( MT::Auth::NEW_USER() == $result )
268        || ( MT::Auth::SUCCESS() == $result ) )
269    {
270        my $commenter = $app->user;
271        if ( $q->param('external_auth') && !$commenter ) {
272            $app->param( 'name', $name );
273            if ( MT::Auth::NEW_USER() == $result ) {
274                $commenter =
275                  $app->_create_commenter_assign_role( $q->param('blog_id') );
276                return $app->login( error => $app->translate('Invalid login') )
277                  unless $commenter;
278            }
279            elsif ( MT::Auth::NEW_LOGIN() == $result ) {
280                my $registration = $app->config->CommenterRegistration;
281                unless ( $registration && $registration->{Allow} && $blog->allow_commenter_regist ) {
282                    return $app->login( error => $app->translate('Successfully authenticated but signing up is not allowed.  Please contact system administrator.') )
283                      unless $commenter;
284                }
285                else {
286                    return $app->signup( error => $app->translate('You need to sign up first.') )
287                      unless $commenter;
288                }
289            }
290        }
291        MT::Auth->new_login( $app, $commenter );
292        if ( $app->_check_commenter_author( $commenter, $blog_id ) ) {
293            $app->make_commenter_session( $app->make_magic_token,
294                $commenter->email, $commenter->name,
295                ($commenter->nickname || $app->translate('(Display Name not set)')),
296                $commenter->id, undef, $ctx->{permanent} ? '+10y' : 0, $blog_id );
297            return $app->redirect_to_target;
298        }
299        $error = $app->translate("Permission denied.");
300        $message =
301          $app->translate( "Login failed: permission denied for user '[_1]'",
302            $name );
303    }
304    elsif ( MT::Auth::INVALID_PASSWORD() == $result ) {
305        $message =
306          $app->translate( "Login failed: password was wrong for user '[_1]'",
307            $name );
308    }
309    elsif ( MT::Auth::INACTIVE() == $result ) {
310        $message =
311          $app->translate( "Failed login attempt by disabled user '[_1]'",
312            $name );
313    }
314    else {
315        $message =
316          $app->translate( "Failed login attempt by unknown user '[_1]'",
317            $name );
318    }
319    $app->log(
320        {
321            message  => $message,
322            level    => MT::Log::WARNING(),
323            category => 'login_commenter',
324        }
325    );
326    $ctx->{app} ||= $app;
327    MT::Auth->invalidate_credentials($ctx);
328    return $app->login( error => $error || $app->translate("Invalid login") );
329}
330
331sub signup {
332    my $app   = shift;
333    my %opt   = @_;
334    my $param = {};
335    $param->{$_} = $app->param($_) foreach qw(blog_id entry_id static username);
336    my $blog = $app->model('blog')->load( $param->{blog_id} )
337        or return $app->error($app->translate('Can\'t load blog #[_1].', $param->{blog_id}));
338    my $cfg  = $app->config;
339    if ( my $registration = $cfg->CommenterRegistration ) {
340        return $app->handle_error(
341            $app->translate('Signing up is not allowed.') )
342          unless $registration->{Allow} && $blog->allow_commenter_regist;
343        if ( my $provider = MT->effective_captcha_provider( $blog->captcha_provider ) ) {
344            $param->{captcha_fields} = $provider->form_fields( $blog->id );
345        }
346        $param->{$_} = $opt{$_} foreach keys %opt;
347        $param->{ 'auth_mode_' . $cfg->AuthenticationModule } = 1;
348        return $app->build_page( 'signup.tmpl', $param );
349    }
350    $app->handle_error( $app->translate('Signing up is not allowed.') );
351}
352
353sub do_signup {
354    my $app = shift;
355    my $q   = $app->param;
356
357    my $param = {};
358    $param->{$_} = $q->param($_)
359      foreach
360      qw(blog_id entry_id static email url username nickname email hint);
361
362    my $user = $app->create_user_pending($param);
363    unless ($user) {
364        my $blog = $app->model('blog')->load( $param->{blog_id} )
365            or return $app->error($app->translate('Can\'t load blog #[_1].', $param->{blog_id}));
366        if ( my $provider = MT->effective_captcha_provider( $blog->captcha_provider ) ) {
367            $param->{captcha_fields} = $provider->form_fields( $blog->id );
368        }
369        $param->{error} = $app->errstr;
370        return $app->build_page( 'signup.tmpl', $param );
371    }
372
373    ## Send confirmation email in the background.
374    MT::Util::start_background_task(
375        sub {
376            $app->_send_signup_confirmation( $user->id, $user->email,
377                $param->{entry_id}, $param->{blog_id}, $param->{static} );
378        }
379    );
380
381    my $entry = MT::Entry->load( $param->{entry_id} );
382    if ($entry) {
383        my $entry_url = $entry->permalink;
384        $app->build_page( 'signup_thanks.tmpl',
385            { email => $user->email, entry_url => $entry_url } );
386    }
387    else {
388        $app->build_page( 'signup_thanks.tmpl',
389            { email => $user->email, return_url => is_valid_url( $param->{static} ) }
390        );
391    }
392}
393
394sub _send_signup_confirmation {
395    my $app = shift;
396    my ( $id, $email, $entry_id, $blog_id, $static ) = @_;
397    my $cfg = $app->config;
398
399    my $blog   = MT::Blog->load($blog_id)
400        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
401    my $entry  = MT::Entry->load($entry_id);
402    my $author = $entry ? $entry->author : q();
403
404    my $token = $app->make_magic_token;
405
406    my $subject = $app->translate('Movable Type Account Confirmation');
407    my $cgi_path = $app->config('CGIPath');
408    $cgi_path .= '/' unless $cgi_path =~ m!/$!;
409    my $url     = $cgi_path
410      . $cfg->CommentScript
411      . $app->uri_params(
412        'mode' => 'do_register',
413        args   => {
414            'token' => $token,
415            $entry ? ( 'entry_id' => $entry->id ) : (),
416            'blog_id' => $blog_id,
417            'email'   => $email,
418            'static'  => $static,
419        },
420      );
421
422    if ( $url =~ m!^/! ) {
423        my ($blog_domain) = $blog->site_url =~ m|(.+://[^/]+)|;
424        $url = $blog_domain . $url;
425    }
426
427    my $param = {
428        blog => $blog,
429        confirm_url => $url,
430        author => $author,
431    };
432    my $body = MT->build_email( 'commenter_confirm.tmpl', $param );
433
434    require MT::Mail;
435    my $from_addr;
436    my $reply_to;
437    if ( $cfg->EmailReplyTo ) {
438        $reply_to = $cfg->EmailAddressMain;
439    }
440    else {
441        $from_addr = $cfg->EmailAddressMain;
442    }
443    $from_addr = undef if $from_addr && !is_valid_email($from_addr);
444    $reply_to  = undef if $reply_to  && !is_valid_email($reply_to);
445
446    unless ( $from_addr || $reply_to ) {
447        $app->log(
448            {
449                message =>
450                  MT->translate("System Email Address is not configured."),
451                level    => MT::Log::ERROR(),
452                class    => 'system',
453                category => 'email'
454            }
455        );
456        return;
457    }
458
459    my %head = (
460        id => 'commenter_confirm',
461        To => $email,
462        $from_addr ? ( From       => $from_addr ) : (),
463        $reply_to  ? ( 'Reply-To' => $reply_to )  : (),
464        Subject => $subject,
465    );
466    my $charset = $cfg->MailEncoding || $cfg->PublishCharset;
467    $head{'Content-Type'} = qq(text/plain; charset="$charset");
468
469    ## Save it in session to purge later
470    require MT::Session;
471    my $sess = MT::Session->new;
472    $sess->id($token);
473    $sess->kind('CR');    # CR == Commenter Registration
474    $sess->email($email);
475    $sess->name($id);
476    $sess->start(time);
477    $sess->save;
478
479    MT::Mail->send( \%head, $body )
480      or die MT::Mail->errstr() ;
481}
482
483sub do_register {
484    my $app = shift;
485    my $q   = $app->param;
486    my $cfg = $app->config;
487
488    my $entry_id = $q->param('entry_id');
489    my $blog_id  = $q->param('blog_id');
490    my $static   = $q->param('static');
491    my $email    = $q->param('email');
492    my $token    = $q->param('token');
493
494    my $param = {};
495    $param->{$_} = $app->param($_) foreach qw(blog_id entry_id static);
496
497    my $blog = $app->model('blog')->load($blog_id)
498        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
499    ## Token expiration check
500    require MT::Session;
501    my $commenter;
502    my $sess =
503      MT::Session->load( { id => $token, kind => 'CR', email => $email } );
504    if ($sess) {
505        $commenter = MT::Author->load( $sess->name );
506        if ( $sess->start() < ( time - 60 * 60 * 24 ) ) {
507            $commenter->remove if $commenter;
508            $sess->remove;
509            $sess = $commenter = undef;
510        }
511    }
512    unless ($sess) {
513        if ( my $provider = MT->effective_captcha_provider( $blog->captcha_provider ) ) {
514            $param->{captcha_fields} = $provider->form_fields( $blog->id );
515        }
516        return $app->build_page( 'signup.tmpl', $param );
517    }
518    $sess->remove;
519
520    $commenter->status( MT::Author::ACTIVE() );
521    if ( $commenter->save ) {
522        $app->log(
523            {
524                message => $app->translate(
525"Commenter '[_1]' (ID:[_2]) has been successfully registered.",
526                    $commenter->name,
527                    $commenter->id
528                ),
529                level    => MT::Log::INFO(),
530                class    => 'author',
531                category => 'new',
532            }
533        );
534        require MT::Role;
535        require MT::Association;
536        my $role = MT::Role->load_same( undef, undef, 1, 'comment' );
537        if ( $role && $blog ) {
538            MT::Association->link( $commenter => $role => $blog );
539        }
540        else {
541            $app->log(
542                {
543                    message => MT->translate(
544"Error assigning commenting rights to user '[_1] (ID: [_2])' for weblog '[_3] (ID: [_4])'. No suitable commenting role was found.",
545                        $commenter->name, $commenter->id,
546                        $blog->name,      $blog->id,
547                    ),
548                    level    => MT::Log::ERROR(),
549                    class    => 'system',
550                    category => 'new'
551                }
552            );
553        }
554    }
555    else {
556        if ( my $provider = MT->effective_captcha_provider( $blog->captcha_provider ) ) {
557            $param->{captcha_fields} = $provider->form_fields( $blog->id );
558        }
559        $param->{error} = $commenter->errstr;
560        return $app->build_page( 'signup.tmpl', $param );
561    }
562
563    if ( my $registration = $cfg->CommenterRegistration ) {
564        if ( my $ids = $registration->{Notify} ) {
565            ## Send notification email in the background.
566            MT::Util::start_background_task(
567                sub {
568                    $app->_send_registration_notification( $commenter, $entry_id, $blog_id, $ids );
569                }
570            );
571        }
572    }
573
574    $app->login(
575        message => $app->translate(
576            'Thanks for the confirmation.  Please sign in to comment.')
577    );
578}
579
580sub _send_registration_notification {
581    my $app = shift;
582    my ( $user, $entry_id, $blog_id, $ids ) = @_;
583
584    my $blog    = MT::Blog->load($blog_id)
585        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
586    my $subject = $app->translate( "[_1] registered to the blog '[_2]'",
587        $user->name, $blog->name );
588
589    my $url = $app->mt_uri(
590                mode => 'view',
591                args => {
592                    '_type' => 'author',
593                    id      => $user->id
594                }
595            );
596
597    if ( $url =~ m!^/! ) {
598        my ($blog_domain) = $blog->site_url =~ m|(.+://[^/]+)|;
599        $url = $blog_domain . $url;
600    }
601
602    my $param = {
603        blog => $blog,
604        commenter => $user,
605        profile_url => $url
606    };
607    my $body = MT->build_email( 'commenter_notify.tmpl', $param );
608
609    $app->_send_sysadmins_email($ids, 'commenter_notify', $body, $subject, $user->email);
610}
611
612sub generate_captcha {
613    my $app = shift;
614
615    my $pi = $app->path_info; 
616    $pi =~ s!^/!!;
617    my $cmtscript = $app->config('CommentScript');
618    $pi =~ s!.*\Q$cmtscript\E/!!;
619    $pi =~ s,captcha/,,; #remove prefix..
620    my ($blog_id, $token) = split '/', $pi;
621    unless ( $blog_id && $token ) {
622        $app->error('Required parameter was missing.');
623        return undef;
624    }
625    my $blog = $app->model('blog')->load($blog_id)
626        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
627    if ( my $provider = MT->effective_captcha_provider( $blog->captcha_provider ) ) {
628        my $image_data = $provider->generate_captcha($app, $blog_id, $token);
629        if ($image_data) {
630            $app->{no_print_body} = 1;
631            $app->set_header( 'Cache-Control' => 'no-cache' );
632            $app->set_header( 'Expires'       => '-1' );
633            $app->send_http_header('image/png');
634            $app->print($image_data);
635            return 1;
636        }
637    }
638    return undef;
639}
640
641sub do_red {
642    my $app = shift;
643    my $q   = $app->param;
644    my $id  = $q->param('id') or return $app->error( $app->translate("No id") );
645    my $comment = MT::Comment->load($id)
646      or return $app->error( $app->translate("No such comment") );
647    return $app->error( $app->translate("No such comment") )
648      unless ( $comment->visible );
649    my $uri = encode_html( $comment->url );
650    return <<HTML;
651<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
652<html><head><title>Redirecting...</title>
653<meta name="robots" content="noindex, nofollow">
654<script type="text/javascript">
655window.onload = function() { document.location = '$uri'; };
656</script></head>
657<body>
658<p><a href="$uri">Click here</a> if you are not redirected</p>
659</body>
660</html>
661HTML
662}
663
664# _builtin_throttle is the builtin throttling code
665# others can be added by plugins
666# a filtering callback must return true or false; true
667#    means OK, false means filter it out.
668sub _builtin_throttle {
669    my $eh      = shift;
670    my $app     = shift;
671    my ($entry) = @_;
672    my $cfg     = $app->config;
673
674    my $throttle_period = $cfg->ThrottleSeconds;
675    my $user_ip         = $app->remote_ip;
676    return 1 if ( $throttle_period <= 0 );    # Disabled by ThrottleSeconds 0
677
678    require MT::Util;
679    my @ts =
680      MT::Util::offset_time_list( time - $throttle_period, $entry->blog_id );
681    my $from = sprintf(
682        "%04d%02d%02d%02d%02d%02d",
683        $ts[5] + 1900,
684        $ts[4] + 1,
685        @ts[ 3, 2, 1, 0 ]
686    );
687
688    if (
689        MT::Comment->exist(
690            {
691                ip         => $user_ip,
692                created_on => [$from],
693                blog_id    => $entry->blog_id
694            },
695            { range => { created_on => 1 } }
696        )
697      )
698    {
699        return 0;    # Put a collar on that puppy.
700    }
701    @ts = MT::Util::offset_time_list( time - $throttle_period * 10 - 1,
702        $entry->blog_id );
703    $from = sprintf(
704        "%04d%02d%02d%02d%02d%02d",
705        $ts[5] + 1900,
706        $ts[4] + 1,
707        @ts[ 3, 2, 1, 0 ]
708    );
709    my $count = MT::Comment->count(
710        {
711            ip         => $user_ip,
712            created_on => [$from],
713            blog_id    => $entry->blog_id
714        },
715        { range => { created_on => 1 } }
716    );
717    if ( $count >= 8 ) {
718        require MT::IPBanList;
719        my $ipban = MT::IPBanList->new();
720        $ipban->blog_id( $entry->blog_id );
721        $ipban->ip($user_ip);
722        $ipban->save();
723        $app->log(
724            {
725                message => $app->translate(
726"IP [_1] banned because comment rate exceeded 8 comments in [_2] seconds.",
727                    $user_ip,
728                    10 * $throttle_period
729                ),
730                class    => 'comment',
731                category => 'ip_ban',
732                blog_id  => $entry->blog_id,
733                level    => MT::Log::INFO(),
734                metadata => $user_ip,
735            }
736        );
737        require MT::Mail;
738        my $author = $entry->author;
739        $app->set_language( $author->preferred_language )
740          if $author && $author->preferred_language;
741
742        my $blog = MT::Blog->load( $entry->blog_id )
743            or return $app->error($app->translate('Can\'t load blog #[_1].', $entry->blog_id));
744        if ( $author && $author->email ) {
745            my %head = (
746                id      => 'comment_throttle',
747                To      => $author->email,
748                From    => $cfg->EmailAddressMain,
749                Subject => '['
750                  . $blog->name . '] '
751                  . $app->translate("IP Banned Due to Excessive Comments")
752            );
753            my $charset = $cfg->MailEncoding || $cfg->PublishCharset;
754            $head{'Content-Type'} = qq(text/plain; charset="$charset");
755            my $body = $app->build_email('comment_throttle.tmpl', {
756                blog => $blog,
757                throttled_ip => $user_ip,
758                throttle_seconds => 10 * $throttle_period,
759            });
760            $body = wrap_text( $body, 72 );
761            MT::Mail->send( \%head, $body );
762        }
763        return 0;
764    }
765    return 1;
766}
767
768sub post {
769    my $app = shift;
770    my $q   = $app->param;
771
772    return $app->error( $app->translate("Invalid request") )
773      if $app->request_method() ne 'POST';
774
775    my $entry_id = int($q->param('entry_id'))
776      or return $app->error( $app->translate("No entry_id") );
777    require MT::Entry;
778    my $entry = MT::Entry->load($entry_id)
779      or return $app->error(
780        $app->translate(
781            "No such entry '[_1]'.", scalar $q->param('entry_id')
782        )
783      );
784    return $app->error(
785        $app->translate(
786            "No such entry '[_1]'.", scalar $q->param('entry_id')
787        )
788    ) if $entry->status != RELEASE;
789
790    require MT::IPBanList;
791    my $iter = MT::IPBanList->load_iter( { blog_id => $entry->blog_id } );
792    while ( my $ban = $iter->() ) {
793        my $banned_ip = $ban->ip;
794        if ( $app->remote_ip =~ /$banned_ip/ ) {
795            return $app->handle_error(
796                $app->translate("Invalid request") );
797        }
798    }
799
800    my $blog = $app->model('blog')->load( $entry->blog_id )
801        or return $app->error($app->translate('Can\'t load blog #[_1].', $entry->blog_id));
802
803    my $armor = $q->param('armor');
804    if (defined $armor) {
805        # For this to work, we must create a site path exactly like
806        # <MTBlogSitePath> does.
807        my $path = $blog->site_path;
808        $path .= '/' unless $path =~ m!/$!;
809        my $site_path_sha1 = MT::Util::perl_sha1_digest_hex($path);
810        if ($armor ne $site_path_sha1) {
811            return $app->handle_error($app->translate("Invalid request; $armor != $site_path_sha1"));
812        }
813    }
814
815    # Run all the Comment-throttling callbacks
816    my $passed_filter =
817      MT->run_callbacks( 'CommentThrottleFilter', $app, $entry );
818
819    $passed_filter
820      || return $app->handle_error( $app->translate("_THROTTLED_COMMENT"),
821        "403 Throttled" );
822
823    my $cfg = $app->config;
824    if ( my $state = $q->param('comment_state') ) {
825        require MT::Serialize;
826        my $ser = MT::Serialize->new( $cfg->Serializer );
827        $state = $ser->unserialize( pack 'H*', $state );
828        $state = $$state;
829        for my $f ( keys %$state ) {
830            $q->param( $f, $state->{$f} );
831        }
832    }
833    unless ( $cfg->AllowComments && $entry->allow_comments eq '1' ) {
834        return $app->handle_error(
835            $app->translate("Comments are not allowed on this entry.") );
836    }
837
838    my $text = $q->param('text') || '';
839    $text =~ s/^\s+|\s+$//g;
840    if ( $text eq '' ) {
841        return $app->handle_error(
842            $app->translate("Comment text is required.") );
843    }
844
845    my ( $comment, $commenter ) = _make_comment( $app, $entry, $blog );
846    return $app->handle_error(
847        $app->translate( "An error occurred: [_1]", $app->errstr() ) )
848      unless $comment;
849
850    my $remember = $q->param('bakecookie') || 0;
851    $remember = 0 if $remember eq 'Forget Info';    # another value for '0'
852    if ( $commenter && $remember ) {
853        $app->_extend_commenter_session( Duration => "+1y" );
854    }
855    if ( !$blog->allow_unreg_comments ) {
856        if ( !$commenter ) {
857            return $app->handle_error(
858                $app->translate("Registration is required.") );
859        }
860    }
861    if (
862           $blog->require_comment_emails()
863        && !$commenter
864        && !(
865               $comment->author
866            && $comment->email
867            && is_valid_email( $comment->email )
868        )
869      )
870    {
871        return $app->handle_error(
872            $app->translate("Name and email address are required.") );
873    }
874    if ( $blog->allow_unreg_comments() ) {
875        $comment->email( $q->param('email') ) unless $comment->email();
876    }
877
878    if ( $comment->email ) {
879        if ( my $fixed = is_valid_email( $comment->email ) ) {
880            $comment->email($fixed);
881        }
882        elsif ( $comment->email =~ /^[0-9A-F]{40}$/i ) {
883
884            # It's a FOAF-style mbox hash; accept it if blog config says to.
885            return $app->handle_error("A real email address is required")
886              if ( !$commenter && $blog->require_comment_emails() );
887        }
888        else {
889            return $app->handle_error(
890                $app->translate(
891                    "Invalid email address '[_1]'",
892                    $comment->email
893                )
894            );
895        }
896    }
897    if ( $comment->url ) {
898        if ( my $fixed = is_valid_url( $comment->url ) ) {
899            $comment->url($fixed);
900        }
901        else {
902            return $app->handle_error(
903                $app->translate( "Invalid URL '[_1]'", $comment->url ) );
904        }
905    }
906
907    if ( !$commenter && ( my $provider = MT->effective_captcha_provider( $blog->captcha_provider ) ) ) {
908        unless ( $provider->validate_captcha($app) ) {
909            return $app->handle_error(
910                $app->translate("Text entered was wrong.  Try again.") );
911        }
912    }
913
914    $comment = $app->eval_comment( $blog, $commenter, $comment, $entry );
915    return $app->preview('pending') unless $comment;
916
917    $app->user($commenter);
918    $comment->save
919      or $app->log(
920        {
921            message => $app->translate(
922                "Comment save failed with [_1]",
923                $comment->errstr
924            ),
925            blog_id => $blog->id,
926            class   => 'comment',
927            level   => MT::Log::ERROR()
928        }
929      );
930    if ( $comment->id && !$comment->is_junk ) {
931        $app->log(
932            {
933                message => $app->translate(
934                    'Comment on "[_1]" by [_2].', $entry->title,
935                    $comment->author
936                ),
937                class    => 'comment',
938                category => 'new',
939                blog_id  => $blog->id,
940                metadata => $comment->id,
941            }
942        );
943    }
944
945    # Form a link to the comment
946    my $comment_link;
947    if ( !$q->param('static') ) {
948        my $url = $app->base . $app->uri;
949        $url .= '?entry_id=' . $q->param('entry_id');
950        $comment_link = $url;
951    }
952    else {
953        my $static = $q->param('static');
954        if ( $static eq '1' ) {
955            # I think what we really want is the individual archive.
956            $comment_link = $entry->permalink;
957        }
958        else {
959            $static =~ s/[\r\n].*$//s;
960            $comment_link = $static . '#comment-' . $comment->id;
961        }
962    }
963
964    if ( $comment->visible ) {
965
966        # Rebuild the entry synchronously so that if the user gets
967        # redirected to the indiv. page it will be up-to-date.
968        $app->rebuild_entry( Entry => $entry->id )
969          or return $app->handle_error(
970            $app->translate( "Publish failed: [_1]", $app->errstr ) );
971    }
972
973    if ( $comment->is_junk ) {
974        $app->run_tasks('JunkExpiration');
975        return $app->preview('pending');
976    }
977    if ( !$comment->visible ) {
978        $app->_send_comment_notification( $comment, $comment_link, $entry,
979            $blog, $commenter );
980        return $app->preview('pending');
981    }
982
983    # Index rebuilds and notifications are done in the background.
984    MT::Util::start_background_task(
985        sub {
986            $app->rebuild_indexes( Blog => $blog )
987              or return $app->errtrans( "Publish failed: [_1]", $app->errstr );
988            $app->_send_comment_notification( $comment, $comment_link, $entry,
989                $blog, $commenter );
990            _expire_sessions( $cfg->CommentSessionTimeout )
991              if ( $commenter && ( $commenter->type ne MT::Author::AUTHOR() ) );
992        }
993    );
994
995    if ( $blog->use_comment_confirmation ) {
996        my $tmpl =
997          MT::Template->load(
998            { type => 'comment_response', blog_id => $entry->blog_id } );
999        unless ($tmpl) {
1000            require MT::DefaultTemplates;
1001            $tmpl = MT::DefaultTemplates->load({ type => 'comment_response' })
1002                or return $app->handle_error($app->translate("Can\'t load template"));
1003            $tmpl->text( $app->translate_templatized( $tmpl->text ) );
1004        }
1005        my $ctx = $tmpl->context;
1006        $tmpl->param(
1007            { 'body_class' => 'mt-comment-confirmation', 'comment_link' => $comment_link, 'comment_response_template' => 1, 'system_template' => 1 } );
1008        $ctx->stash('entry', $entry);
1009        $ctx->stash('comment', $comment);
1010        $ctx->stash('commenter', $commenter) if $commenter;
1011        my $html = $tmpl->output();
1012        $html = $tmpl->errstr unless defined $html;
1013        return $html;
1014    }
1015    else {
1016        return $app->redirect($comment_link);
1017    }
1018}
1019
1020sub eval_comment {
1021    my $app = shift;
1022    my ( $blog, $commenter, $comment, $entry ) = @_;
1023
1024    if (   $commenter
1025        && ( $commenter->type == MT::Author::COMMENTER() )
1026        && ( $commenter->commenter_status( $blog->id ) == MT::Author::BLOCKED() ) )
1027    {
1028        return undef;
1029    }
1030
1031    my $commenter_status;
1032    if ($commenter) {
1033        $commenter_status = $commenter->commenter_status( $entry->blog_id );
1034        if ( $commenter_status == MT::Author::APPROVED() ) {
1035            if ( $blog->publish_trusted_commenters ) {
1036                $comment->approve;
1037                return $comment;
1038            }
1039            else {
1040                $comment->moderate;
1041                return $comment;
1042            }
1043        }
1044        if ( $commenter_status == MT::Author::PENDING() ) {
1045
1046            # just in case record doesn't exist...
1047            $commenter->pending( $entry->blog_id );
1048        }
1049        if ( $commenter_status == MT::Author::BANNED() ) {
1050            return undef;
1051        }
1052    }
1053
1054    my $not_declined = MT->run_callbacks( 'CommentFilter', $app, $comment );
1055    return unless $not_declined;
1056
1057    MT::JunkFilter->filter($comment);
1058
1059    ## Here comes the built-in logic for deciding whether the
1060    ## comment is moderated or published.
1061
1062    # from here to #mark should set "visible" no matter what
1063    if ( $comment->is_junk ) {
1064        $comment->visible(0);    # forcibly set to unpublished
1065    }
1066    elsif ( !defined $comment->visible ) {
1067        if ($commenter) {
1068            if ( $blog->publish_authd_untrusted_commenters ) {
1069                $comment->approve;
1070            }
1071            else {
1072                $comment->moderate;
1073            }
1074        }
1075        else {
1076
1077            # We don't have a commenter object, but the user wasn't booted
1078            # so unless moderation is on, we can publish the comment.
1079            if ( $blog->publish_unauthd_commenters ) {
1080                $comment->approve;
1081            }
1082            else {
1083                $comment->moderate;
1084            }
1085        }
1086    }
1087
1088    #mark
1089
1090    $comment;
1091}
1092
1093# only handles Duration => +xxxu where u is one of y, d, s
1094sub _extend_commenter_session {
1095    my $app         = shift;
1096    my %param       = @_;
1097    my %cookies     = $app->cookies();
1098    my $cookie_name = $app->commenter_cookie_name;
1099    my $session_key = $cookies{$cookie_name}->value() || "";
1100    $session_key =~ y/+/ /;
1101    my $sessobj = MT::Session->load($session_key);
1102    return
1103      if
1104      !$sessobj;   # no point changing the cookie if the session's already lost.
1105    my ( $sign, $number, $units ) = $param{Duration} =~ /([+-]?)(\d+)(\w+)/;
1106    $number *= $sign eq '-' ? -1 : +1;
1107    $number *=
1108        $units eq 'y' ? 60 * 60 * 24 * 365
1109      : $units eq 'd' ? 60 * 60 * 24
1110      :                 $number;
1111    $sessobj->start( $sessobj->start + $number );
1112    $sessobj->save();
1113    my %sess_cookie = (
1114        -name    => $cookie_name,
1115        -value   => $session_key,
1116        -path    => '/',
1117        -expires => "+${number}s"
1118    );
1119    $app->bake_cookie(%sess_cookie);
1120    my %name_kookee = (
1121        -name    => "commenter_name",
1122        -value   => $cookies{commenter_name}->value,
1123        -path    => '/',
1124        -expires => "+${number}s"
1125    );
1126    $app->bake_cookie(%name_kookee);
1127    1;
1128}
1129
1130sub _check_commenter_author {
1131    my $app = shift;
1132    my ( $commenter, $blog_id ) = @_;
1133
1134    return 0 unless $blog_id;
1135
1136    # Using MT::Author::commenter_status here, since it also
1137    # takes the permission "restrictions" into account.
1138    my $status = $commenter->commenter_status($blog_id);
1139
1140    # INACTIVE == BANNED
1141    return 0 if $status == MT::Author::BANNED();
1142    return 0 if $commenter->status == MT::Author::BANNED();
1143
1144    # NOT using $status for this test, since $status may be
1145    # assigned 'PENDING' by 'commenter_status' if no permission
1146    # record exists at all. We want to check below to see if
1147    # commenting permission is auto-vivified based on blog configuration
1148    # in such a case.
1149    if ( MT::Author::PENDING() == $commenter->status() ) {
1150        $app->error(
1151            $app->translate(
1152                "Failed comment attempt by pending registrant '[_1]'",
1153                $commenter->name
1154            )
1155        );
1156        return 0;
1157    }
1158    elsif ( $commenter->blog_perm($blog_id)->can_comment ) {
1159        return 1;
1160    }
1161    else {
1162        # No explicit permissions are given for this commenter, so
1163        # see if blog is configured as "open to registration" for
1164        # commenting. If it is, auto-assign commenting permissions
1165        # for this blog only.
1166        if ( my $registration = $app->config->CommenterRegistration ) {
1167            my $blog = MT::Blog->load($blog_id)
1168                or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
1169            if ( $registration->{Allow} && $blog->allow_commenter_regist ) {
1170                # By policy, this blog permits this type of user
1171                # and they are not banned (as they have no blog perms/
1172                # restrictions, so permit this comment)
1173                return 1;
1174            }
1175        }
1176    }
1177    $app->error(
1178        $app->translate(
1179            "Login failed: permission denied for user '[_1]'",
1180            $commenter->name
1181        )
1182    );
1183    return 0;
1184}
1185
1186#
1187# $app->_make_comment($entry)
1188#
1189# _make_comment creates an MT::Comment record attached to the $entry,
1190# based on the query information in $app (It neeeds the whole app object
1191# so it can get the user's IP). Also creates an MT::Author record
1192# representing the person who placed the comment, if necessary.
1193#
1194# Always returns a pair ($comment, $commenter). The latter is undef if
1195# there is no commenter for the session (or if there is no active
1196# session).
1197#
1198# Validation of the comment data is left to the caller.
1199#
1200sub _make_comment {
1201    my ( $app, $entry, $blog ) = @_;
1202    my $q = $app->param;
1203
1204    my $nick  = $q->param('author');
1205    my $email = $q->param('email');
1206    my ( $session, $commenter );
1207    if ( $blog->accepts_registered_comments ) {
1208        ( $session, $commenter ) = $app->_get_commenter_session();
1209    }
1210    if ( $commenter && ( 'do_reply' ne $app->mode ) ) {
1211        if ( MT::Author::AUTHOR() == $commenter->type ) {
1212            if ( $blog->commenter_authenticators !~ /MovableType/ ) {
1213                $commenter = undef;
1214            }
1215            else {
1216                unless (
1217                    $app->_check_commenter_author( $commenter, $blog->id ) )
1218                {
1219                    $app->error( $app->translate('Permission denied.') );
1220                    return ( undef, undef );
1221                }
1222            }
1223        }
1224    }
1225    if ($commenter) {
1226        $nick = $commenter->nickname()
1227          || $app->translate('Registered User');
1228        $email = $commenter->email();
1229    }
1230
1231    my $url = $q->param('url') || '';    #($commenter ? $commenter->url() : '');
1232    my $comment = MT::Comment->new;
1233    if ($commenter) {
1234        $comment->commenter_id( $commenter->id );
1235    }
1236    ## Strip linefeed characters.
1237    my $text = $q->param('text');
1238    $text = '' unless defined $text;
1239    $text =~ tr/\r//d;
1240    $comment->ip( $app->remote_ip );
1241    $comment->blog_id( $entry->blog_id );
1242    $comment->entry_id( $entry->id );
1243    $comment->author( remove_html($nick) );
1244    $comment->email( remove_html($email) );
1245    $url = is_valid_url( $url );
1246    $comment->url( $url eq 'http://' ? '' : $url );
1247    $comment->text($text);
1248
1249    #$comment->visible(0); # leave as undefined
1250    $comment->is_junk(0);
1251
1252    # strip of any null characters (done after junk checks so they can
1253    # monitor for that kind of activity)
1254    for my $field (qw(author email url text)) {
1255        my $val = $comment->column($field);
1256        if ( $val =~ m/\x00/ ) {
1257            $val =~ tr/\x00//d;
1258            $comment->column( $field, $val );
1259        }
1260    }
1261
1262    if (my $parent_id = $app->param('parent_id')) {
1263        # verify that parent_id is for a comment that is
1264        # published for this entry
1265        my $parent_comment = MT::Comment->load( $parent_id );
1266        if ($parent_comment && $parent_comment->is_published() && $parent_comment->entry_id == $entry->id) {
1267            $comment->parent_id( $parent_id );
1268        }
1269        else {
1270            return $app->error("Invalid 'parent_id' parameter.");
1271        }
1272    }
1273
1274    return ( $comment, $commenter );
1275}
1276
1277sub preview { my $app = shift; do_preview( $app, $app->{query}, @_ ) }
1278
1279sub _make_commenter {
1280    my $app    = shift;
1281    my %params = @_;
1282    require MT::Author;
1283    my $cmntr = MT::Author->load(
1284        {
1285            name => $params{name},
1286            type => MT::Author::COMMENTER,
1287            auth_type => $params{auth_type},
1288        }
1289    );
1290    if ( !$cmntr ) {
1291        $cmntr = $app->model('author')->new();
1292        $cmntr->set_values(
1293            {
1294                email     => $params{email},
1295                name      => $params{name},
1296                nickname  => $params{nickname},
1297                password  => "(none)",
1298                type      => MT::Author::COMMENTER,
1299                url       => $params{url},
1300                auth_type => $params{auth_type},
1301                ($params{external_id} ? (external_id => $params{external_id}) : ()),
1302                ($params{remote_auth_username} ? (remote_auth_username => $params{remote_auth_username}) : ()),
1303            }
1304        );
1305        $cmntr->save();
1306    }
1307    else {
1308        $cmntr->set_values(
1309            {
1310                email    => $params{email},
1311                nickname => $params{nickname},
1312                password => "(none)",
1313                type     => MT::Author::COMMENTER,
1314                url      => $params{url},
1315                ($params{external_id} ? (external_id => $params{external_id}) : ()),
1316            }
1317        );
1318        $cmntr->save();
1319    }
1320    return $cmntr;
1321}
1322
1323# TBD: Move this to MT::Session and store expiration date in
1324# the record
1325sub _expire_sessions {
1326    my ($timeout) = @_;
1327
1328    require MT::Session;
1329    my @old_sessions = MT::Session->load(
1330        {
1331            start => [ 0, time() - $timeout ],
1332            kind  => 'SI'
1333        },
1334        { range => { start => 1 } }
1335    );
1336    foreach (@old_sessions) {
1337        $_->remove() || die "couldn't remove sessions because " . $_->errstr();
1338    }
1339}
1340
1341# This actually handles a UI-level sign-in or sign-out request.
1342sub handle_sign_in {
1343    my $app = shift;
1344    my $q   = $app->param;
1345
1346    my $result = 0;
1347    if ( $q->param('logout') ) {
1348        my ( $s, $commenter ) = $app->_get_commenter_session();
1349
1350        # invalidate credentials in auth layer
1351        if ($commenter) {
1352           require MT::Auth;
1353           my $ctx = MT::Auth->fetch_credentials( { app => $app } );
1354           my $cmntr_sess =
1355             $app->session_user( $commenter, $ctx->{session_id},
1356               permanent => $ctx->{permanent} );
1357           if ($cmntr_sess) {
1358               $app->user($commenter);
1359               MT::Auth->invalidate_credentials( { app => $app } );
1360           }
1361        }
1362
1363        my %cookies = $app->cookies();
1364        $app->_invalidate_commenter_session( \%cookies );
1365        $app->user($commenter) if $commenter;
1366        $result = 1;
1367    }
1368    else {
1369        my $authenticator = MT->commenter_authenticator( $q->param('key') );
1370        my $auth_class    = $authenticator->{class};
1371        eval "require $auth_class;";
1372        if ( my $e = $@ ) {
1373            return $app->handle_error( $e, 403 );
1374        }
1375        $result = $auth_class->handle_sign_in( $app, $q->param('key') );
1376    }
1377
1378    return $app->handle_error(
1379        $app->errstr() || $app->translate(
1380            "The sign-in attempt was not successful; please try again."),
1381        403
1382    ) unless $result;
1383
1384    $app->redirect_to_target;
1385}
1386
1387sub redirect_to_target {
1388    my $app = shift;
1389    my $q   = $app->param;
1390
1391    my $cfg = $app->config;
1392    my $target;
1393    require MT::Util;
1394    my $static = $q->param('static') || '';
1395
1396    if ( ($static eq '') || ($static eq 1) ) {
1397        require MT::Entry;
1398        my $entry = MT::Entry->load( $q->param('entry_id') || 0 )
1399            or return $app->error($app->translate('Can\'t load entry #[_1].', $q->param('entry_id')));
1400        $target = $entry->archive_url;
1401        my $blog = MT::Blog->load( $entry->blog_id );
1402        $target = MT::Util::strip_index( $target, $blog );
1403    }
1404    elsif ($static ne '') {
1405        $target = $static;
1406    }
1407    if ( $q->param('logout') ) {
1408        if ( $app->user &&
1409            ( 'TypeKey' eq $app->user->auth_type ) ) {
1410            return $app->redirect(
1411                $cfg->SignOffURL . "&_return=" .
1412                MT::Util::encode_url($target . '#_logout'),
1413                UseMeta => 1 );
1414        }
1415    }
1416    return $app->redirect( $target . '#_' .
1417        ($q->param('logout') ? 'logout' :  'login'), UseMeta => 1 );
1418}
1419
1420sub _commenter_status {
1421    my $app = shift;
1422    my ( $commenter_id ) = @_;
1423    my $blog_id          = $app->param('blog_id') || 0;
1424    my $commenter_status = '0';
1425    my $user = $app->model('author')->load($commenter_id);
1426    if ($user && $user->is_superuser) {
1427        $commenter_status = 'AUTHOR';
1428    }
1429    else {
1430        # FIXME: this may be incomplete since the user
1431        # may in fact be able to comment on other blogs;
1432        # they just haven't signed into them yet
1433        my $perm = MT::Permission->load(
1434              {
1435                blog_id     => $blog_id,
1436                permissions => { like => "\%'comment'\%" },
1437                author_id   => $commenter_id
1438              }
1439            );
1440        if ( $perm ) {
1441            if ( $perm->is_restricted('comment')
1442              && !$perm->can_administer_blog() ) {
1443                $commenter_status = '0';
1444            }
1445            else {
1446                $commenter_status = 'AUTHOR';
1447            }
1448        }
1449        elsif ( MT::Author::COMMENTER() == $user->type ) {
1450            $commenter_status = 'COMMENTER';
1451        }
1452        elsif ( $app->_check_commenter_author($user, $blog_id) ) {
1453            $commenter_status = 'AUTHOR';
1454        }
1455    }
1456    $commenter_status;
1457}
1458
1459sub session_js {
1460    my $app = shift;
1461    my $blog_id = int($app->param('blog_id'));
1462    my $blog = MT::Blog->load( $blog_id ) if $blog_id;
1463    my $jsonp = $app->param('jsonp');
1464    $jsonp = undef if $jsonp !~ m/^\w+$/;
1465
1466    my $c;
1467    if ( $jsonp && $blog_id && $blog ) {
1468        my ( $session, $commenter ) = $app->_get_commenter_session();
1469        if ( $session && $commenter ) {
1470            my $blog_perms = $commenter->blog_perm($blog_id);
1471            my $banned = $commenter->is_banned($blog_id) ? "1" : "0";
1472            $banned = 0 if $blog_perms && $blog_perms->can_administer;
1473            $banned ||= 1 if $commenter->status == MT::Author::BANNED();
1474
1475            my $sessobj = MT::Session->load($session);
1476            if ($banned) {
1477                $sessobj->remove;
1478            } else {
1479                $sessobj->start( time +
1480                    $app->config->CommentSessionTimeout); # extend by timeou
1481                $sessobj->save();
1482            }
1483
1484            my $can_comment = $banned ? 0 : 1;
1485            $can_comment = 0 unless $blog->allow_unreg_comments || $blog->allow_reg_comments;
1486            my $can_post = ($blog_perms && $blog_perms->can_create_post) ? "1" : "0";
1487            $c = {
1488                name => $commenter->nickname,
1489                url => $commenter->url,
1490                email => $commenter->email,
1491                userpic => scalar $commenter->userpic_url,
1492                profile => "", # profile link url
1493                is_authenticated => "1",
1494                is_trusted => ($commenter->is_trusted() ? "1" : "0"),
1495                is_author => ($commenter->type == MT::Author::AUTHOR() ? "1" : "0"),
1496                is_anonymous => "0",
1497                is_banned => $banned,
1498                can_comment => $can_comment,
1499                can_post => $can_post,
1500            };
1501        }
1502    }
1503
1504    unless ($c) {
1505        my $can_comment = $blog->allow_anon_comments ? "1" : "0";
1506        $c = {
1507            is_authenticated => "0",
1508            is_trusted => "0",
1509            is_anonymous => "1",
1510            can_post => "0", # no anonymous posts
1511            can_comment => $can_comment,
1512            is_banned => "0",
1513        };
1514    }
1515
1516    require JSON;
1517    $app->{no_print_body} = 1;
1518    $app->send_http_header("text/javascript");
1519    my $json = JSON::objToJson($c);
1520    $app->print("$jsonp(" . $json . ");\n");
1521    return undef;
1522}
1523
1524# deprecated
1525sub commenter_status_js {
1526    local $SIG{__WARN__} = sub { };
1527    my $app     = shift;
1528    my $ids     = $app->cookie_val('commenter_id') || q();
1529
1530    my $commenter_id;
1531    if ($ids) {
1532        my @ids = split ':', $ids;
1533        $commenter_id    = $ids[0];
1534    }
1535
1536    my $commenter_status = '0';
1537    if ($commenter_id) {
1538        $commenter_status = $app->_commenter_status( $commenter_id );
1539    }
1540    $commenter_status = encode_js( $commenter_status );
1541    return <<JS;
1542commenter_status = $commenter_status;
1543JS
1544}
1545
1546# deprecated
1547sub commenter_name_js {
1548    local $SIG{__WARN__} = sub { };
1549    my $app            = shift;
1550    my $commenter_name = $app->cookie_val('commenter_name');
1551    my $ids            = $app->cookie_val('commenter_id') || q();
1552    my $commenter_url  = $app->cookie_val('commenter_url') || q();
1553
1554    my $commenter_id;
1555    if ($ids) {
1556        my @ids = split ':', $ids;
1557        $commenter_id    = $ids[0];
1558    }
1559
1560    # FIXME: how do we know this is coming in as utf-8?
1561    $commenter_name = encode_text( $commenter_name, 'utf-8' );
1562
1563    $app->set_header( 'Cache-Control' => 'no-cache' );
1564    $app->set_header( 'Expires'       => '-1' );
1565
1566    my $commenter_status = '0';
1567    if ($commenter_id) {
1568        $commenter_status = $app->_commenter_status( $commenter_id );
1569    }
1570    elsif ($commenter_name) {
1571        $commenter_status = 'COMMENTER';
1572    }
1573    $commenter_name   = encode_js( $commenter_name );
1574    $commenter_url    = encode_js( $commenter_url );
1575    $commenter_id     = encode_js( $commenter_id );
1576    $commenter_status = encode_js( $commenter_status );
1577    return <<JS;
1578commenter_name = '$commenter_name';
1579commenter_id = '$commenter_id';
1580commenter_url = '$commenter_url';
1581commenter_status = $commenter_status;
1582JS
1583}
1584
1585sub handle_error {
1586    my $app = shift;
1587    my ( $err, $status_line ) = @_;
1588    my $html = do_preview( $app, $app->{query}, $err )
1589      || return "An error occurred: " . $err;
1590    $app->{status_line} = $status_line;
1591    $html;
1592}
1593
1594sub do_preview {
1595    my ( $app, $q, $err ) = @_;
1596
1597    return $app->error( $app->translate("Invalid request") )
1598      if $app->request_method() ne 'POST';
1599
1600    my $cfg = $app->config;
1601    require MT::Template;
1602    require MT::Template::Context;
1603    require MT::Entry;
1604    require MT::Util;
1605    require MT::Comment;
1606    require MT::Blog;
1607    my $entry_id = $q->param('entry_id')
1608      || return $app->error(
1609        $app->translate(
1610            'No entry was specified; perhaps there is a template problem?')
1611      );
1612    my $entry = MT::Entry->load($entry_id)
1613      || return $app->error(
1614        $app->translate(
1615            "Somehow, the entry you tried to comment on does not exist")
1616      );
1617    my $ctx  = MT::Template::Context->new;
1618    my $blog = MT::Blog->load( $entry->blog_id );
1619
1620    my ( $comment, $commenter ) = $app->_make_comment( $entry, $blog );
1621    return $app->translate( "An error occurred: [_1]", $app->errstr() )
1622      unless $comment;
1623
1624    ## Set timestamp as we would usually do in ObjectDriver.
1625    my @ts = MT::Util::offset_time_list( time, $entry->blog_id );
1626    my $ts = sprintf "%04d%02d%02d%02d%02d%02d", $ts[5] + 1900, $ts[4] + 1,
1627      @ts[ 3, 2, 1, 0 ];
1628    $comment->created_on($ts);
1629    $comment->commenter_id( $commenter->id ) if $commenter;
1630    $ctx->stash( 'comment', $comment );
1631
1632    unless ($err) {
1633        ## Serialize comment state, then hex-encode it.
1634        require MT::Serialize;
1635        my $ser   = MT::Serialize->new( $cfg->Serializer );
1636        my $state = $comment->column_values;
1637        $state->{static} = $q->param('static');
1638        $ctx->stash( 'comment_state', unpack 'H*', $ser->serialize( \$state ) );
1639    }
1640    $ctx->stash( 'comment_is_static', $q->param('static') );
1641    $ctx->stash( 'entry',             $entry );
1642    $ctx->{current_timestamp} = $ts;
1643    $ctx->stash( 'commenter', $commenter );
1644    my ($tmpl);
1645    $err ||= '';
1646    if ($err) {
1647        $tmpl = MT::Template->load(
1648            {
1649                type    => 'comment_response',
1650                blog_id => $entry->blog_id
1651            }
1652          );
1653        unless ($tmpl) {
1654            require MT::DefaultTemplates;
1655            $tmpl = MT::DefaultTemplates->load({ type => 'comment_response' })
1656                or return $app->error($app->translate("Can\'t load template"));
1657            $tmpl->text( $app->translate_templatized( $tmpl->text ) );
1658        }
1659        if ( $err eq 'pending' ) {
1660            $tmpl->context($ctx);
1661            $tmpl->param(
1662                { 'body_class' => 'mt-comment-pending', 'comment_response_template' => 1, 'system_template' => 1 } );
1663        }
1664        else {
1665            $ctx->stash( 'error_message', $err );
1666            $tmpl->context($ctx);
1667            $tmpl->param(
1668                { 'body_class' => 'mt-comment-error', 'comment_response_template' => 1, 'system_template' => 1 } );
1669        }
1670    }
1671    else {
1672        $tmpl = MT::Template->load(
1673            {
1674                type    => 'comment_preview',
1675                blog_id => $entry->blog_id
1676            }
1677          );
1678        unless ($tmpl) {
1679            require MT::DefaultTemplates;
1680            $tmpl = MT::DefaultTemplates->load({ type => 'comment_preview' })
1681                or return $app->error($app->translate("Can\'t load template"));
1682            $tmpl->text( $app->translate_templatized( $tmpl->text ) );
1683        }
1684        $tmpl->context($ctx);
1685        $tmpl->param(
1686            { 'body_class' => 'mt-comment-preview', 'comment_preview_template' => 1, 'system_template' => 1 } );
1687    }
1688    my %cond;
1689    my $html = $tmpl->build( $ctx, \%cond );
1690    $html = $tmpl->errstr unless defined $html;
1691    $html;
1692}
1693
1694sub edit_commenter_profile {
1695    my $app = shift;
1696
1697    my ( $session, $commenter ) = $app->_get_commenter_session();
1698    if ($commenter) {
1699        my $url;
1700        my $entry_id = $app->param('entry_id');
1701        if ($entry_id) {
1702            my $entry = MT::Entry->load($entry_id);
1703            return $app->handle_error( $app->translate("Invalid entry ID provided") )
1704              unless $entry;
1705            $url = $entry->permalink;
1706        }
1707        else {
1708            $url = is_valid_url( $app->param('static') );
1709        }
1710
1711        #require MT::Auth;
1712        #my $ctx = MT::Auth->fetch_credentials( { app => $app } );
1713        #my $cmntr_sess =
1714        #  $app->session_user( $commenter, $ctx->{session_id},
1715        #    permanent => $ctx->{permanent} );
1716        #return $app->handle_error( $app->translate('Invalid login') )
1717        #  unless $cmntr_sess;
1718
1719        my $blog_id = $app->param('blog_id');
1720        $app->user($commenter);
1721        my $param = {
1722            id       => $commenter->id,
1723            name     => $commenter->name,
1724            nickname => $commenter->nickname,
1725            email    => $commenter->email,
1726            hint     => $commenter->hint,
1727            url      => $commenter->url,
1728            $entry_id ? ( entry_url => $url ) : ( return_url => $url ),
1729        };
1730        $param->{ 'auth_mode_' . $commenter->auth_type } = 1;
1731        require MT::Auth;
1732        $param->{'email_required'} = MT::Auth->can_recover_password ? 1 : 0;
1733        return $app->build_page( 'profile.tmpl', $param );
1734    }
1735    return $app->handle_error( $app->translate('Invalid login') );
1736}
1737
1738sub save_commenter_profile {
1739    my $app = shift;
1740    my $q   = $app->param;
1741
1742    my %param =
1743      map { $_ => scalar( $q->param($_) ) }
1744      qw( id name nickname email password pass_verify hint url entry_url return_url external_auth);
1745
1746    unless ( $param{id} =~ /\d+/ ) {
1747        $param{error} = $app->translate('Invalid commenter ID');
1748        return $app->build_page( 'profile.tmpl', \%param );
1749    }
1750
1751    my $cmntr = MT::Author->load( $param{id} );
1752    unless ($cmntr) {
1753        $param{error} = $app->translate('Invalid commenter ID');
1754        return $app->build_page( 'profile.tmpl', \%param );
1755    }
1756
1757    $param{ 'auth_mode_' . $cmntr->auth_type } = 1;
1758
1759    # require MT::Auth;
1760    # my $ctx = MT::Auth->fetch_credentials( { app => $app } );
1761    # my $cmntr_sess =
1762    #  $app->session_user( $cmntr, $ctx->{session_id},
1763    #    permanent => $ctx->{permanent} );
1764    # return $app->handle_error( $app->translate('Invalid login') )
1765    #  unless $cmntr_sess;
1766
1767    $app->user($cmntr);
1768    $app->validate_magic
1769      or return $app->handle_error( $app->translate('Invalid request') );
1770
1771    unless ( $param{external_auth} ) {
1772        unless ( $param{nickname} && $param{email} && $param{hint} ) {
1773            $param{error} =
1774              $app->translate('All required fields must have valid values.');
1775            return $app->build_page( 'profile.tmpl', \%param );
1776        }
1777        if ( $param{password} ne $param{pass_verify} ) {
1778            $param{error} = $app->translate('Passwords do not match.');
1779            return $app->build_page( 'profile.tmpl', \%param );
1780        }
1781    }
1782    if ( $param{email} && !is_valid_email( $param{email} ) ) {
1783        $param{error} = $app->translate('Email Address is invalid.');
1784        return $app->build_page( 'profile.tmpl', \%param );
1785    }
1786    if ( $param{url} && !is_url( $param{url} ) ) {
1787        $param{error} = $app->translate('URL is invalid.');
1788        return $app->build_page( 'profile.tmpl', \%param );
1789    }
1790
1791    my $renew_session =
1792      $param{nickname} && ( $param{nickname} ne $cmntr->nickname ) ? 1 : 0;
1793    $cmntr->nickname( $param{nickname} ) if $param{nickname};
1794    $cmntr->email( $param{email} )       if $param{email};
1795    $cmntr->hint( $param{hint} )         if $param{hint};
1796    $cmntr->url( $param{url} )           if $param{url};
1797    $cmntr->set_password( $param{password} )
1798      if $param{password} && !$param{external_auth};
1799    if ( $cmntr->save ) {
1800        $param{saved} =
1801          $app->translate('Commenter profile has successfully been updated.');
1802    }
1803    else {
1804        $param{error} =
1805          $app->translate( 'Commenter profile could not be updated: [_1]',
1806            $cmntr->errstr );
1807    }
1808    if ($renew_session) {
1809        $app->make_commenter_session( $app->make_magic_token, $cmntr->email,
1810            $cmntr->name,
1811            ($cmntr->nickname || $app->translate('(Display Name not set)')),
1812            $cmntr->id );
1813    }
1814
1815    return $app->build_page( 'profile.tmpl', \%param );
1816}
1817
1818sub blog {
1819    my $app = shift;
1820    return $app->{_blog} if $app->{_blog};
1821    return undef unless $app->{query};
1822    if ( my $entry_id = $app->param('entry_id') ) {
1823        require MT::Entry;
1824        my $entry = MT::Entry->load($entry_id);
1825        return undef unless $entry;
1826        $app->{_blog} = $entry->blog if $entry;
1827    }
1828    return $app->{_blog};
1829}
1830
18311;
1832__END__
1833
1834=head1 NAME
1835
1836MT::App::Comments
1837
1838=head1 SYNOPSIS
1839
1840The application-level callbacks of the C<MT::App::Comments> application
1841are documented here.
1842
1843=head1 METHODS
1844
1845=head2 $app->init
1846
1847Initializes the application and defines the serviceable modes.
1848
1849=head2 $app->init_request
1850
1851Initializes the application to service the request.
1852
1853=head2 $app->do_preview($cgi[, $err])
1854
1855Handles the comment preview request and displays the preview using
1856the Comment Preview blog template. If C<$err> is specified, the
1857error message is relayed to the user using the Comment Error blog
1858template.
1859
1860=head2 $app->blog
1861
1862Returns the L<MT::Blog> object related to the entry being commented on.
1863
1864=head2 $app->eval_comment
1865
1866Evaluates the comment being posted in a variety of ways and an L<MT::Comment>
1867object is returned. If the comment request is rejected due to throttling,
1868no object is returned and the Comment Pending blog template is displayed.
1869
1870=head2 $app->handle_error
1871
1872Returns an error message to the user using the Comment Error blog template.
1873
1874=head1 APPLICATION MODES
1875
1876=head2 $app->commenter_name_js
1877
1878Returns some JavaScript code that sets the 'commenter_name' variable
1879based on the 'tk_commenter' cookie that is accessible to the comments
1880CGI script.
1881
1882=head2 $app->do_red
1883
1884Handles a commenter URL redirect, where the comment_id points to a
1885L<MT::Comment> object with a URL. The response redirects the user to
1886that URL. The comment must be approved and published.
1887
1888Note: This behavior has been deprecated in favor of using the 'nofollow'
1889plugin.
1890
1891=head2 $app->handle_sign_in
1892
1893Handles the sign-in process for a sign-in request handled by external
1894such authentication APIs as TypeKey and OpenID.
1895
1896=head2 $app->post
1897
1898Mode that handles posting of a new comment.
1899
1900=head2 $app->preview
1901
1902Mode for previewing a comment before posting.
1903
1904=head1 CALLBACKS
1905
1906=over 4
1907
1908=item CommentThrottleFilter
1909
1910Called as soon as a new comment has been received. The callback must
1911return a boolean value. If the return value is false, the incoming
1912comment data will be discarded and the app will output an error page
1913about throttling. A CommentThrottleFilter callback has the following
1914signature:
1915
1916    sub comment_throttle_filter($cb, $app, $entry)
1917    {
1918        ...
1919    }
1920
1921I<$app> is the C<MT::App::Comments> object, whose interface is documented
1922in L<MT::App::Comments>, and I<$entry> is the entry on which the
1923comment is to be placed.
1924
1925Note that no comment object is passed, because it has not yet been
1926built. As such, this callback can be used to tell the application to
1927exit early from a comment attempt, before much processing takes place.
1928
1929When more than one CommentThrottleFilter is installed, the data is
1930discarded unless all callbacks return true.
1931
1932=item CommentFilter
1933
1934Called once the comment object has been constructed, but before saving
1935it. If any CommentFilter callback returns false, the comment will not
1936be saved. The callback has the following signature:
1937
1938    sub comment_filter($cb, $app, $comment)
1939    {
1940        ...
1941    }
1942
1943=head1 SPAM PROTECTION
1944
1945Spam filtering (or "Junk" filtering in MT terminology) is handled using
1946the L<MT::JunkFilter> package and plugins that implement them. Please
1947refer to that module for further documentation.
1948
1949=head1 AUTHOR & COPYRIGHT
1950
1951Please see the I<MT> manpage for author, copyright, and license information.
1952
1953=back
Note: See TracBrowser for help on using the browser.