root/branches/release-34/lib/MT/App/Comments.pm @ 1823

Revision 1823, 61.8 kB (checked in by takayama, 20 months ago)

Fixed BugId:67959
* Added check for result of object loading

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