root/branches/release-33/lib/MT/App/Comments.pm @ 1718

Revision 1718, 60.6 kB (checked in by bchoate, 20 months ago)

Only register the CommentThrottleFilter callback once.

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