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

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

Reload session object by ID (could be a comment session; could be a user session).

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