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

Revision 2350, 63.4 kB (checked in by bchoate, 19 months ago)

Fix for async publishing so individual archive for comment is published when received (other pages are left to publish based on established rules).

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