root/branches/release-35/lib/MT/App/Comments.pm @ 1924

Revision 1924, 61.9 kB (checked in by auno, 20 months ago)

Not to save URL, if author is commenter. BugzID:79285

  • 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->exist(
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 )
927          if $comment->url && $commenter->type != MT::Author::COMMENTER();
928        $commenter->save
929          or $app->log(
930            {
931                message => $app->translate(
932                    "Commenter save failed with [_1]",
933                    $commenter->errstr
934                ),
935                class   => 'system',
936                blog_id => $blog->id,
937                level   => MT::Log::ERROR()
938            }
939          );
940    }
941
942    #    return $app->handle_error($app->errstr()) unless $comment;
943
944    # Form a link to the comment
945    my $comment_link;
946    if ( !$q->param('static') ) {
947        my $url = $app->base . $app->uri;
948        $url .= '?entry_id=' . $q->param('entry_id');
949        $url .= '&static=0&arch=1' if ( $q->param('arch') );
950        $comment_link = $url;
951    }
952    else {
953        my $static = $q->param('static');
954        if ( $static eq '1' ) {
955
956            # I think what we really want is the individual archive.
957            $comment_link = $entry->permalink;
958        }
959        else {
960            $static =~ s/[\r\n].*$//s;
961            $comment_link = $static . '#' . $comment->id;
962        }
963    }
964
965    if ( $comment->visible ) {
966
967        # Rebuild the entry synchronously so that if the user gets
968        # redirected to the indiv. page it will be up-to-date.
969        $app->rebuild_entry( Entry => $entry->id )
970          or return $app->error(
971            $app->translate( "Publish failed: [_1]", $app->errstr ) );
972    }
973
974    if ( $comment->is_junk ) {
975        $app->run_tasks('JunkExpiration');
976        return $app->preview('pending');
977    }
978    if ( !$comment->visible ) {
979        $app->_send_comment_notification( $comment, $comment_link, $entry,
980            $blog, $commenter );
981        return $app->preview('pending');
982    }
983
984    # Index rebuilds and notifications are done in the background.
985    MT::Util::start_background_task(
986        sub {
987            $app->rebuild_indexes( Blog => $blog )
988              or return $app->errtrans( "Publish failed: [_1]", $app->errstr );
989            $app->_send_comment_notification( $comment, $comment_link, $entry,
990                $blog, $commenter );
991            _expire_sessions( $cfg->CommentSessionTimeout )
992              if ( $commenter && ( $commenter->type ne MT::Author::AUTHOR() ) );
993        }
994    );
995
996    if ( $blog->use_comment_confirmation ) {
997        my $tmpl =
998          MT::Template->load(
999            { type => 'comment_response', blog_id => $entry->blog_id } );
1000        unless ($tmpl) {
1001            require MT::DefaultTemplates;
1002            $tmpl = MT::DefaultTemplates->load({ type => 'comment_response' })
1003                or return $app->error($app->translate("Can\'t load template"));
1004            $tmpl->text( $app->translate_templatized( $tmpl->text ) );
1005        }
1006        my $ctx = $tmpl->context;
1007        $tmpl->param(
1008            { 'body_class' => 'mt-comment-confirmation', 'comment_link' => $comment_link, 'comment_response_template' => 1, 'system_template' => 1 } );
1009        $ctx->stash('entry', $entry);
1010        $ctx->stash('comment', $comment);
1011        $ctx->stash('commenter', $commenter) if $commenter;
1012        my $html = $tmpl->output();
1013        $html = $tmpl->errstr unless defined $html;
1014        return $html;
1015    }
1016    else {
1017        return $app->redirect($comment_link);
1018    }
1019}
1020
1021sub eval_comment {
1022    my $app = shift;
1023    my ( $blog, $commenter, $comment, $entry ) = @_;
1024
1025    if (   $commenter
1026        && ( $commenter->type == MT::Author::COMMENTER() )
1027        && ( $commenter->commenter_status( $blog->id ) == MT::Author::BLOCKED() ) )
1028    {
1029        return undef;
1030    }
1031
1032    my $commenter_status;
1033    if ($commenter) {
1034        $commenter_status = $commenter->commenter_status( $entry->blog_id );
1035        if ( $commenter_status == MT::Author::APPROVED() ) {
1036            if ( $blog->publish_trusted_commenters ) {
1037                $comment->approve;
1038                return $comment;
1039            }
1040            else {
1041                $comment->moderate;
1042                return $comment;
1043            }
1044        }
1045        if ( $commenter_status == MT::Author::PENDING() ) {
1046
1047            # just in case record doesn't exist...
1048            $commenter->pending( $entry->blog_id );
1049        }
1050        if ( $commenter_status == MT::Author::BANNED() ) {
1051            return undef;
1052        }
1053    }
1054
1055    my $not_declined = MT->run_callbacks( 'CommentFilter', $app, $comment );
1056    return unless $not_declined;
1057
1058    MT::JunkFilter->filter($comment);
1059
1060    ## Here comes the built-in logic for deciding whether the
1061    ## comment is moderated or published.
1062
1063    # from here to #mark should set "visible" no matter what
1064    if ( $comment->is_junk ) {
1065        $comment->visible(0);    # forcibly set to unpublished
1066    }
1067    elsif ( !defined $comment->visible ) {
1068        if ($commenter) {
1069            if ( $blog->publish_authd_untrusted_commenters ) {
1070                $comment->approve;
1071            }
1072            else {
1073                $comment->moderate;
1074            }
1075        }
1076        else {
1077
1078            # We don't have a commenter object, but the user wasn't booted
1079            # so unless moderation is on, we can publish the comment.
1080            if ( $blog->publish_unauthd_commenters ) {
1081                $comment->approve;
1082            }
1083            else {
1084                $comment->moderate;
1085            }
1086        }
1087    }
1088
1089    #mark
1090
1091    $comment;
1092}
1093
1094# only handles Duration => +xxxu where u is one of y, d, s
1095sub _extend_commenter_session {
1096    my $app         = shift;
1097    my %param       = @_;
1098    my %cookies     = $app->cookies();
1099    my $cookie_name = MT::App::COMMENTER_COOKIE_NAME();
1100    my $session_key = $cookies{$cookie_name}->value() || "";
1101    $session_key =~ y/+/ /;
1102    my $sessobj = MT::Session->load($session_key);
1103    return
1104      if
1105      !$sessobj;   # no point changing the cookie if the session's already lost.
1106    my ( $sign, $number, $units ) = $param{Duration} =~ /([+-]?)(\d+)(\w+)/;
1107    $number *= $sign eq '-' ? -1 : +1;
1108    $number *=
1109        $units eq 'y' ? 60 * 60 * 24 * 365
1110      : $units eq 'd' ? 60 * 60 * 24
1111      :                 $number;
1112    $sessobj->start( $sessobj->start + $number );
1113    $sessobj->save();
1114    my %sess_cookie = (
1115        -name    => $cookie_name,
1116        -value   => $session_key,
1117        -path    => '/',
1118        -expires => "+${number}s"
1119    );
1120    $app->bake_cookie(%sess_cookie);
1121    my %name_kookee = (
1122        -name    => "commenter_name",
1123        -value   => $cookies{commenter_name}->value,
1124        -path    => '/',
1125        -expires => "+${number}s"
1126    );
1127    $app->bake_cookie(%name_kookee);
1128    1;
1129}
1130
1131sub _check_commenter_author {
1132    my $app = shift;
1133    my ( $commenter, $blog_id ) = @_;
1134
1135    return 0 unless $blog_id;
1136
1137    # Using MT::Author::commenter_status here, since it also
1138    # takes the permission "restrictions" into account.
1139    my $status = $commenter->commenter_status($blog_id);
1140
1141    # INACTIVE == BANNED
1142    return 0 if $status == MT::Author::BANNED();
1143
1144    # NOT using $status for this test, since $status may be
1145    # assigned 'PENDING' by 'commenter_status' if no permission
1146    # record exists at all. We want to check below to see if
1147    # commenting permission is auto-vivified based on blog configuration
1148    # in such a case.
1149    if ( MT::Author::PENDING() == $commenter->status() ) {
1150        $app->error(
1151            $app->translate(
1152                "Failed comment attempt by pending registrant '[_1]'",
1153                $commenter->name
1154            )
1155        );
1156        return 0;
1157    }
1158    elsif ( $commenter->blog_perm($blog_id)->can_comment ) {
1159        return 1;
1160    }
1161    else {
1162        # No explicit permissions are given for this commenter, so
1163        # see if blog is configured as "open to registration" for
1164        # commenting. If it is, auto-assign commenting permissions
1165        # for this blog only.
1166        if ( my $registration = $app->config->CommenterRegistration ) {
1167            my $blog = MT::Blog->load($blog_id)
1168                or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
1169            if ( $registration->{Allow} && $blog->allow_commenter_regist ) {
1170                my $perm = $commenter->blog_perm($blog_id);
1171                return 0 unless $perm->is_empty;
1172                require MT::Role;
1173                require MT::Association;
1174                my $role = MT::Role->load_same( undef, undef, 1, 'comment' );
1175                if ( $role && $blog ) {
1176                    MT::Association->link( $commenter => $role => $blog );
1177                }
1178                else {
1179                    # FIXME: In this case, we should probably return 0
1180                    # here, since no permission was actually granted.
1181                    $app->log(
1182                        {
1183                            message => MT->translate(
1184"Error assigning commenting rights to user '[_1] (ID: [_2])' for weblog '[_3] (ID: [_4])'. No suitable commenting role was found.",
1185                                $commenter->name, $commenter->id,
1186                                $blog->name,      $blog->id,
1187                            ),
1188                            level    => MT::Log::ERROR(),
1189                            class    => 'system',
1190                            category => 'new'
1191                        }
1192                    );
1193                }
1194                return 1;
1195            }
1196        }
1197    }
1198    $app->error(
1199        $app->translate(
1200            "Login failed: permission denied for user '[_1]'",
1201            $commenter->name
1202        )
1203    );
1204    return 0;
1205}
1206
1207#
1208# $app->_make_comment($entry)
1209#
1210# _make_comment creates an MT::Comment record attached to the $entry,
1211# based on the query information in $app (It neeeds the whole app object
1212# so it can get the user's IP). Also creates an MT::Author record
1213# representing the person who placed the comment, if necessary.
1214#
1215# Always returns a pair ($comment, $commenter). The latter is undef if
1216# there is no commenter for the session (or if there is no active
1217# session).
1218#
1219# Validation of the comment data is left to the caller.
1220#
1221sub _make_comment {
1222    my ( $app, $entry, $blog ) = @_;
1223    my $q = $app->param;
1224
1225    my $nick  = $q->param('author');
1226    my $email = $q->param('email');
1227    my ( $session, $commenter );
1228    if ( $blog->accepts_registered_comments ) {
1229        ( $session, $commenter ) = $app->_get_commenter_session();
1230    }
1231    if ( $commenter && ( 'do_reply' ne $app->mode ) ) {
1232        if ( MT::Author::AUTHOR() == $commenter->type ) {
1233            if ( $blog->commenter_authenticators !~ /MovableType/ ) {
1234                $commenter = undef;
1235            }
1236            else {
1237                unless (
1238                    $app->_check_commenter_author( $commenter, $blog->id ) )
1239                {
1240                    $app->error( $app->translate('Permission denied.') );
1241                    return ( undef, undef );
1242                }
1243            }
1244        }
1245    }
1246    if ($commenter) {
1247        $nick = $commenter->nickname()
1248          || $app->translate('Registered User');
1249        $email = $commenter->email();
1250    }
1251
1252    my $url = $q->param('url') || '';    #($commenter ? $commenter->url() : '');
1253    my $comment = MT::Comment->new;
1254    if ($commenter) {
1255        $comment->commenter_id( $commenter->id );
1256    }
1257    ## Strip linefeed characters.
1258    my $text = $q->param('text');
1259    $text = '' unless defined $text;
1260    $text =~ tr/\r//d;
1261    $comment->ip( $app->remote_ip );
1262    $comment->blog_id( $entry->blog_id );
1263    $comment->entry_id( $entry->id );
1264    $comment->author( remove_html($nick) );
1265    $comment->email( remove_html($email) );
1266    $url = is_valid_url( $url );
1267    $comment->url( $url eq 'http://' ? '' : $url );
1268    $comment->text($text);
1269
1270    #$comment->visible(0); # leave as undefined
1271    $comment->is_junk(0);
1272
1273    # strip of any null characters (done after junk checks so they can
1274    # monitor for that kind of activity)
1275    for my $field (qw(author email url text)) {
1276        my $val = $comment->column($field);
1277        if ( $val =~ m/\x00/ ) {
1278            $val =~ tr/\x00//d;
1279            $comment->column( $field, $val );
1280        }
1281    }
1282
1283    if (my $parent_id = $app->param('parent_id')) {
1284        # verify that parent_id is for a comment that is
1285        # published for this entry
1286        my $parent_comment = MT::Comment->load( $parent_id );
1287        if ($parent_comment && $parent_comment->is_published() && $parent_comment->entry_id == $entry->id) {
1288            $comment->parent_id( $parent_id );
1289        }
1290        else {
1291            return $app->error("Invalid 'parent_id' parameter.");
1292        }
1293    }
1294
1295    return ( $comment, $commenter );
1296}
1297
1298sub preview { my $app = shift; do_preview( $app, $app->{query}, @_ ) }
1299
1300sub _make_commenter {
1301    my $app    = shift;
1302    my %params = @_;
1303    require MT::Author;
1304    my $cmntr = MT::Author->load(
1305        {
1306            name => $params{name},
1307            type => MT::Author::COMMENTER,
1308            auth_type => $params{auth_type},
1309        }
1310    );
1311    if ( !$cmntr ) {
1312        $cmntr = $app->model('author')->new();
1313        $cmntr->set_values(
1314            {
1315                email     => $params{email},
1316                name      => $params{name},
1317                nickname  => $params{nickname},
1318                password  => "(none)",
1319                type      => MT::Author::COMMENTER,
1320                url       => $params{url},
1321                auth_type => $params{auth_type},
1322                ($params{external_id} ? (external_id => $params{external_id}) : ()),
1323                ($params{remote_auth_username} ? (remote_auth_username => $params{remote_auth_username}) : ()),
1324            }
1325        );
1326        $cmntr->save();
1327    }
1328    else {
1329        $cmntr->set_values(
1330            {
1331                email    => $params{email},
1332                nickname => $params{nickname},
1333                password => "(none)",
1334                type     => MT::Author::COMMENTER,
1335                url      => $params{url},
1336                ($params{external_id} ? (external_id => $params{external_id}) : ()),
1337            }
1338        );
1339        $cmntr->save();
1340    }
1341    return $cmntr;
1342}
1343
1344# TBD: Move this to MT::Session and store expiration date in
1345# the record
1346sub _expire_sessions {
1347    my ($timeout) = @_;
1348
1349    require MT::Session;
1350    my @old_sessions = MT::Session->load(
1351        {
1352            start => [ 0, time() - $timeout ],
1353            kind  => 'SI'
1354        },
1355        { range => { start => 1 } }
1356    );
1357    foreach (@old_sessions) {
1358        $_->remove() || die "couldn't remove sessions because " . $_->errstr();
1359    }
1360}
1361
1362# This actually handles a UI-level sign-in or sign-out request.
1363sub handle_sign_in {
1364    my $app = shift;
1365    my $q   = $app->param;
1366
1367    my $result = 0;
1368    if ( $q->param('logout') ) {
1369        my ( $s, $commenter ) = $app->_get_commenter_session();
1370        #if ($commenter) {
1371        #    require MT::Auth;
1372        #    my $ctx = MT::Auth->fetch_credentials( { app => $app } );
1373        #    my $cmntr_sess =
1374        #      $app->session_user( $commenter, $ctx->{session_id},
1375        #        permanent => $ctx->{permanent} );
1376        #    if ($cmntr_sess) {
1377        #        $app->user($commenter);
1378        #        MT::Auth->invalidate_credentials( { app => $app } );
1379        #    }
1380        #}
1381
1382        my %cookies = $app->cookies();
1383        $app->_invalidate_commenter_session( \%cookies );
1384        if ( $commenter && ( 'TypeKey' ne $commenter->auth_type ) ) {
1385            # Remove logout parameter so MT does not go to TypeKey
1386            # when unnecessary.
1387            $app->param( 'logout', 0 );
1388        }
1389        $result = 1;
1390    }
1391    else {
1392        my $authenticator = MT->commenter_authenticator( $q->param('key') );
1393        my $auth_class    = $authenticator->{class};
1394        eval "require $auth_class;";
1395        if ( my $e = $@ ) {
1396            return $app->handle_error( $e, 403 );
1397        }
1398        $result = $auth_class->handle_sign_in( $app, $q->param('key') );
1399    }
1400
1401    return $app->handle_error(
1402        $app->errstr() || $app->translate(
1403            "The sign-in attempt was not successful; please try again."),
1404        403
1405    ) unless $result;
1406
1407    $app->redirect_to_target;
1408}
1409
1410sub redirect_to_target {
1411    my $app = shift;
1412    my $q   = $app->param;
1413
1414    my $cfg = $app->config;
1415    my $target;
1416    require MT::Util;
1417    if ( $q->param('static') ) {
1418        if ( $q->param('static') eq 1 ) {
1419            require MT::Entry;
1420            my $entry = MT::Entry->load( $q->param('entry_id') )
1421                or return $app->error($app->translate('Can\'t load entry #[_1].', $q->param('entry_id')));
1422            $target = $entry->archive_url;
1423            my $blog = MT::Blog->load( $entry->blog_id );
1424            $target = MT::Util::strip_index( $target, $blog );
1425        }
1426        else {
1427            $target = $q->param('static');
1428        }
1429    }
1430    else {
1431        $target =
1432          (     $cfg->CGIPath
1433              . $cfg->CommentScript
1434              . "?entry_id="
1435              . $q->param('entry_id')
1436              . ( $q->param('arch') ? '&static=0&arch=1' : '' ) );
1437    }
1438    if ( $q->param('logout') ) {
1439        return $app->redirect(
1440            $cfg->SignOffURL . "&_return=" . MT::Util::encode_url($target),
1441            UseMeta => 1 );
1442    }
1443    else {
1444        return $app->redirect( $target, UseMeta => 1 );
1445    }
1446}
1447
1448sub _commenter_status {
1449    my $app = shift;
1450    my ( $commenter_id ) = @_;
1451    my $blog_id          = $app->param('blog_id') || 0;
1452    my $commenter_status = '0';
1453    my $user = $app->model('author')->load($commenter_id);
1454    if ($user && $user->is_superuser) {
1455        $commenter_status = 'AUTHOR';
1456    }
1457    else {
1458        # FIXME: this may be incomplete since the user
1459        # may in fact be able to comment on other blogs;
1460        # they just haven't signed into them yet
1461        my $perm = MT::Permission->load(
1462              {
1463                blog_id     => $blog_id,
1464                permissions => { like => "\%'comment'\%" },
1465                author_id   => $commenter_id
1466              }
1467            );
1468        if ( $perm ) {
1469            if ( $perm->is_restricted('comment')
1470              && !$perm->can_administer_blog() ) {
1471                $commenter_status = '0';
1472            }
1473            else {
1474                $commenter_status = 'AUTHOR';
1475            }
1476        }
1477        elsif ( MT::Author::COMMENTER() == $user->type ) {
1478            $commenter_status = 'COMMENTER';
1479        }
1480        elsif ( $app->_check_commenter_author($user, $blog_id) ) {
1481            $commenter_status = 'AUTHOR';
1482        }
1483    }
1484    $commenter_status;
1485}
1486
1487sub commenter_status_js {
1488    local $SIG{__WARN__} = sub { };
1489    my $app     = shift;
1490    my $ids     = $app->cookie_val('commenter_id') || q();
1491
1492    my $commenter_id;
1493    if ($ids) {
1494        my @ids = split ':', $ids;
1495        $commenter_id    = $ids[0];
1496    }
1497
1498    my $commenter_status = '0';
1499    if ($commenter_id) {
1500        $commenter_status = $app->_commenter_status( $commenter_id );
1501    }
1502    $commenter_status = encode_js( $commenter_status );
1503    return <<JS;
1504commenter_status = $commenter_status;
1505JS
1506}
1507
1508sub commenter_name_js {
1509    local $SIG{__WARN__} = sub { };
1510    my $app            = shift;
1511    my $commenter_name = $app->cookie_val('commenter_name');
1512    my $ids            = $app->cookie_val('commenter_id') || q();
1513    my $commenter_url  = $app->cookie_val('commenter_url') || q();
1514
1515    my $commenter_id;
1516    if ($ids) {
1517        my @ids = split ':', $ids;
1518        $commenter_id    = $ids[0];
1519    }
1520
1521    # FIXME: how do we know this is coming in as utf-8?
1522    $commenter_name = encode_text( $commenter_name, 'utf-8' );
1523
1524    $app->set_header( 'Cache-Control' => 'no-cache' );
1525    $app->set_header( 'Expires'       => '-1' );
1526
1527    my $commenter_status = '0';
1528    if ($commenter_id) {
1529        $commenter_status = $app->_commenter_status( $commenter_id );
1530    }
1531    elsif ($commenter_name) {
1532        $commenter_status = 'COMMENTER';
1533    }
1534    $commenter_name   = encode_js( $commenter_name );
1535    $commenter_url    = encode_js( $commenter_url );
1536    $commenter_id     = encode_js( $commenter_id );
1537    $commenter_status = encode_js( $commenter_status );
1538    return <<JS;
1539commenter_name = '$commenter_name';
1540commenter_id = '$commenter_id';
1541commenter_url = '$commenter_url';
1542commenter_status = $commenter_status;
1543JS
1544}
1545
1546sub handle_error {
1547    my $app = shift;
1548    my ( $err, $status_line ) = @_;
1549    my $html = do_preview( $app, $app->{query}, $err )
1550      || return "An error occurred: " . $err;
1551    $app->{status_line} = $status_line;
1552    $html;
1553}
1554
1555sub do_preview {
1556    my ( $app, $q, $err ) = @_;
1557
1558    return $app->error( $app->translate("Invalid request") )
1559      if $app->request_method() ne 'POST';
1560
1561    my $cfg = $app->config;
1562    require MT::Template;
1563    require MT::Template::Context;
1564    require MT::Entry;
1565    require MT::Util;
1566    require MT::Comment;
1567    require MT::Blog;
1568    my $entry_id = $q->param('entry_id')
1569      || return $app->error(
1570        $app->translate(
1571            'No entry was specified; perhaps there is a template problem?')
1572      );
1573    my $entry = MT::Entry->load($entry_id)
1574      || return $app->error(
1575        $app->translate(
1576            "Somehow, the entry you tried to comment on does not exist")
1577      );
1578    my $ctx  = MT::Template::Context->new;
1579    my $blog = MT::Blog->load( $entry->blog_id );
1580
1581    my ( $comment, $commenter ) = $app->_make_comment( $entry, $blog );
1582    return $app->translate( "An error occurred: [_1]", $app->errstr() )
1583      unless $comment;
1584
1585    ## Set timestamp as we would usually do in ObjectDriver.
1586    my @ts = MT::Util::offset_time_list( time, $entry->blog_id );
1587    my $ts = sprintf "%04d%02d%02d%02d%02d%02d", $ts[5] + 1900, $ts[4] + 1,
1588      @ts[ 3, 2, 1, 0 ];
1589    $comment->created_on($ts);
1590    $comment->commenter_id( $commenter->id ) if $commenter;
1591    $ctx->stash( 'comment', $comment );
1592
1593    unless ($err) {
1594        ## Serialize comment state, then hex-encode it.
1595        require MT::Serialize;
1596        my $ser   = MT::Serialize->new( $cfg->Serializer );
1597        my $state = $comment->column_values;
1598        $state->{static} = $q->param('static');
1599        $ctx->stash( 'comment_state', unpack 'H*', $ser->serialize( \$state ) );
1600    }
1601    $ctx->stash( 'comment_is_static', $q->param('static') );
1602    $ctx->stash( 'entry',             $entry );
1603    $ctx->{current_timestamp} = $ts;
1604    $ctx->stash( 'commenter', $commenter );
1605    my ($tmpl);
1606    $err ||= '';
1607    if ($err) {
1608        $tmpl = MT::Template->load(
1609            {
1610                type    => 'comment_response',
1611                blog_id => $entry->blog_id
1612            }
1613          );
1614        unless ($tmpl) {
1615            require MT::DefaultTemplates;
1616            $tmpl = MT::DefaultTemplates->load({ type => 'comment_response' })
1617                or return $app->error($app->translate("Can\'t load template"));
1618            $tmpl->text( $app->translate_templatized( $tmpl->text ) );
1619        }
1620        if ( $err eq 'pending' ) {
1621            $tmpl->context($ctx);
1622            $tmpl->param(
1623                { 'body_class' => 'mt-comment-pending', 'comment_response_template' => 1, 'system_template' => 1 } );
1624        }
1625        else {
1626            $ctx->stash( 'error_message', $err );
1627            $tmpl->context($ctx);
1628            $tmpl->param(
1629                { 'body_class' => 'mt-comment-error', 'comment_response_template' => 1, 'system_template' => 1 } );
1630        }
1631    }
1632    else {
1633        $tmpl = MT::Template->load(
1634            {
1635                type    => 'comment_preview',
1636                blog_id => $entry->blog_id
1637            }
1638          );
1639        unless ($tmpl) {
1640            require MT::DefaultTemplates;
1641            $tmpl = MT::DefaultTemplates->load({ type => 'comment_preview' })
1642                or return $app->error($app->translate("Can\'t load template"));
1643            $tmpl->text( $app->translate_templatized( $tmpl->text ) );
1644        }
1645        $tmpl->context($ctx);
1646        $tmpl->param(
1647            { 'body_class' => 'mt-comment-preview', 'comment_preview_template' => 1, 'system_template' => 1 } );
1648    }
1649    my %cond;
1650    my $html = $tmpl->build( $ctx, \%cond );
1651    $html = $tmpl->errstr unless defined $html;
1652    $html;
1653}
1654
1655sub edit_commenter_profile {
1656    my $app = shift;
1657
1658    my $id = $app->param('commenter');
1659    return $app->handle_error( $app->translate('Invalid commenter ID') )
1660      unless $id =~ /\d+/;
1661
1662    my $url;
1663    my $entry_id = $app->param('entry_id');
1664    if ($entry_id) {
1665        my $entry = MT::Entry->load($entry_id);
1666        return $app->handle_error( $app->translate("No entry ID provided") )
1667          unless $entry;
1668        $url = $entry->permalink;
1669    }
1670    else {
1671        $url = is_valid_url( $app->param('static') );
1672    }
1673
1674    my ( $session, $commenter ) = $app->_get_commenter_session();
1675    if ($commenter) {
1676        #require MT::Auth;
1677        #my $ctx = MT::Auth->fetch_credentials( { app => $app } );
1678        #my $cmntr_sess =
1679        #  $app->session_user( $commenter, $ctx->{session_id},
1680        #    permanent => $ctx->{permanent} );
1681        #return $app->handle_error( $app->translate('Invalid login') )
1682        #  unless $cmntr_sess;
1683
1684        my $blog_id = $app->param('blog_id');
1685        return $app->handle_error( $app->translate('Permission denied') )
1686          unless $commenter->blog_perm($blog_id)->can_comment;
1687
1688        $app->user($commenter);
1689        my $param = {
1690            id       => $commenter->id,
1691            name     => $commenter->name,
1692            nickname => $commenter->nickname,
1693            email    => $commenter->email,
1694            hint     => $commenter->hint,
1695            url      => $commenter->url,
1696            $entry_id ? ( entry_url => $url ) : ( return_url => $url ),
1697        };
1698        $param->{ 'auth_mode_' . $app->config->AuthenticationModule } = 1;
1699        require MT::Auth;
1700        $param->{'email_required'} = MT::Auth->can_recover_password ? 1 : 0;
1701        return $app->build_page( 'profile.tmpl', $param );
1702    }
1703    return $app->handle_error( $app->translate('Invalid login') );
1704}
1705
1706sub save_commenter_profile {
1707    my $app = shift;
1708    my $q   = $app->param;
1709
1710    my %param =
1711      map { $_ => scalar( $q->param($_) ) }
1712      qw( id name nickname email password pass_verify hint url entry_url return_url external_auth);
1713    $param{ 'auth_mode_' . $app->config->AuthenticationModule } = 1;
1714
1715    unless ( $param{id} =~ /\d+/ ) {
1716        $param{error} = $app->translate('Invalid commenter ID');
1717        return $app->build_page( 'profile.tmpl', \%param );
1718    }
1719
1720    my $cmntr = MT::Author->load( $param{id} );
1721    unless ($cmntr) {
1722        $param{error} = $app->translate('Invalid commenter ID');
1723        return $app->build_page( 'profile.tmpl', \%param );
1724    }
1725
1726    #require MT::Auth;
1727    #my $ctx = MT::Auth->fetch_credentials( { app => $app } );
1728    #my $cmntr_sess =
1729    #  $app->session_user( $cmntr, $ctx->{session_id},
1730    #    permanent => $ctx->{permanent} );
1731    #return $app->handle_error( $app->translate('Invalid login') )
1732    #  unless $cmntr_sess;
1733
1734    $app->user($cmntr);
1735    $app->validate_magic
1736      or return $app->handle_error( $app->translate('Invalid request') );
1737
1738    unless ( $param{external_auth} ) {
1739        unless ( $param{nickname} && $param{email} && $param{hint} ) {
1740            $param{error} =
1741              $app->translate('All required fields must have valid values.');
1742            return $app->build_page( 'profile.tmpl', \%param );
1743        }
1744        if ( $param{password} ne $param{pass_verify} ) {
1745            $param{error} = $app->translate('Passwords do not match.');
1746            return $app->build_page( 'profile.tmpl', \%param );
1747        }
1748    }
1749    if ( $param{email} && !is_valid_email( $param{email} ) ) {
1750        $param{error} = $app->translate('Email Address is invalid.');
1751        return $app->build_page( 'profile.tmpl', \%param );
1752    }
1753    if ( $param{url} && !is_url( $param{url} ) ) {
1754        $param{error} = $app->translate('URL is invalid.');
1755        return $app->build_page( 'profile.tmpl', \%param );
1756    }
1757
1758    my $renew_session =
1759      $param{nickname} && ( $param{nickname} ne $cmntr->nickname ) ? 1 : 0;
1760    $cmntr->nickname( $param{nickname} ) if $param{nickname};
1761    $cmntr->email( $param{email} )       if $param{email};
1762    $cmntr->hint( $param{hint} )         if $param{hint};
1763    $cmntr->url( $param{url} )           if $param{url};
1764    $cmntr->set_password( $param{password} )
1765      if $param{password} && !$param{external_auth};
1766    if ( $cmntr->save ) {
1767        $param{saved} =
1768          $app->translate('Commenter profile has successfully been updated.');
1769    }
1770    else {
1771        $param{error} =
1772          $app->translate( 'Commenter profile could not be updated: [_1]',
1773            $cmntr->errstr );
1774    }
1775    if ($renew_session) {
1776        $app->_make_commenter_session( $app->make_magic_token, $cmntr->email,
1777            $cmntr->name,
1778            ($cmntr->nickname || $app->translate('(Display Name not set)')),
1779            $cmntr->id );
1780        #    $app->start_session( $cmntr, $ctx->{permanent} );
1781    }
1782    $param{ 'auth_mode_' . $app->config->AuthenticationModule } = 1;
1783    return $app->build_page( 'profile.tmpl', \%param );
1784}
1785
1786sub blog {
1787    my $app = shift;
1788    return $app->{_blog} if $app->{_blog};
1789    return undef unless $app->{query};
1790    if ( my $entry_id = $app->param('entry_id') ) {
1791        require MT::Entry;
1792        my $entry = MT::Entry->load($entry_id);
1793        return undef unless $entry;
1794        $app->{_blog} = $entry->blog if $entry;
1795    }
1796    return $app->{_blog};
1797}
1798
17991;
1800__END__
1801
1802=head1 NAME
1803
1804MT::App::Comments
1805
1806=head1 SYNOPSIS
1807
1808The application-level callbacks of the C<MT::App::Comments> application
1809are documented here.
1810
1811=head1 METHODS
1812
1813=head2 $app->init
1814
1815Initializes the application and defines the serviceable modes.
1816
1817=head2 $app->init_request
1818
1819Initializes the application to service the request.
1820
1821=head2 $app->do_preview($cgi[, $err])
1822
1823Handles the comment preview request and displays the preview using
1824the Comment Preview blog template. If C<$err> is specified, the
1825error message is relayed to the user using the Comment Error blog
1826template.
1827
1828=head2 $app->blog
1829
1830Returns the L<MT::Blog> object related to the entry being commented on.
1831
1832=head2 $app->eval_comment
1833
1834Evaluates the comment being posted in a variety of ways and an L<MT::Comment>
1835object is returned. If the comment request is rejected due to throttling,
1836no object is returned and the Comment Pending blog template is displayed.
1837
1838=head2 $app->handle_error
1839
1840Returns an error message to the user using the Comment Error blog template.
1841
1842=head1 APPLICATION MODES
1843
1844=head2 $app->commenter_name_js
1845
1846Returns some JavaScript code that sets the 'commenter_name' variable
1847based on the 'tk_commenter' cookie that is accessible to the comments
1848CGI script.
1849
1850=head2 $app->do_red
1851
1852Handles a commenter URL redirect, where the comment_id points to a
1853L<MT::Comment> object with a URL. The response redirects the user to
1854that URL. The comment must be approved and published.
1855
1856Note: This behavior has been deprecated in favor of using the 'nofollow'
1857plugin.
1858
1859=head2 $app->handle_sign_in
1860
1861Handles the sign-in process for a sign-in request handled by external
1862such authentication APIs as TypeKey and OpenID.
1863
1864=head2 $app->post
1865
1866Mode that handles posting of a new comment.
1867
1868=head2 $app->preview
1869
1870Mode for previewing a comment before posting.
1871
1872=head1 CALLBACKS
1873
1874=over 4
1875
1876=item CommentThrottleFilter
1877
1878Called as soon as a new comment has been received. The callback must
1879return a boolean value. If the return value is false, the incoming
1880comment data will be discarded and the app will output an error page
1881about throttling. A CommentThrottleFilter callback has the following
1882signature:
1883
1884    sub comment_throttle_filter($cb, $app, $entry)
1885    {
1886        ...
1887    }
1888
1889I<$app> is the C<MT::App::Comments> object, whose interface is documented
1890in L<MT::App::Comments>, and I<$entry> is the entry on which the
1891comment is to be placed.
1892
1893Note that no comment object is passed, because it has not yet been
1894built. As such, this callback can be used to tell the application to
1895exit early from a comment attempt, before much processing takes place.
1896
1897When more than one CommentThrottleFilter is installed, the data is
1898discarded unless all callbacks return true.
1899
1900=item CommentFilter
1901
1902Called once the comment object has been constructed, but before saving
1903it. If any CommentFilter callback returns false, the comment will not
1904be saved. The callback has the following signature:
1905
1906    sub comment_filter($cb, $app, $comment)
1907    {
1908        ...
1909    }
1910
1911=head1 SPAM PROTECTION
1912
1913Spam filtering (or "Junk" filtering in MT terminology) is handled using
1914the L<MT::JunkFilter> package and plugins that implement them. Please
1915refer to that module for further documentation.
1916
1917=head1 AUTHOR & COPYRIGHT
1918
1919Please see the I<MT> manpage for author, copyright, and license information.
1920
1921=back
Note: See TracBrowser for help on using the browser.