root/branches/release-32/lib/MT/App/Comments.pm @ 1635

Revision 1635, 60.5 kB (checked in by fumiakiy, 20 months ago)

Merged enzo@r1555 to release-32. BugId:70191

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