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

Revision 2377, 63.8 kB (checked in by bchoate, 19 months ago)

Fix for setting user context for comment previews. BugId:79757

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