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

Revision 2403, 63.2 kB (checked in by bchoate, 19 months ago)

Updates to fix magic token parameter for edit profile. BugId:79822

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