root/branches/release-32/lib/MT/App/Comments.pm @ 1561

Revision 1561, 60.7 kB (checked in by bchoate, 20 months ago)

Applying comment threading support. BugId:70228

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