root/branches/release-38/lib/MT/App/Comments.pm @ 2393

Revision 2393, 64.0 kB (checked in by bchoate, 19 months ago)

Better handling for case where blog cookie and app session are out of sync. BugId:79508

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