root/branches/release-41/lib/MT/App/Comments.pm @ 2821

Revision 2821, 60.3 kB (checked in by bchoate, 16 months ago)

Updates to support display of custom fields on registration form. BugId:80702

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