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

Revision 2263, 63.3 kB (checked in by bchoate, 19 months ago)

Condition using MT user session if MT auth is not enabled for blog. BlogId:79670

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