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

Revision 2749, 59.6 kB (checked in by bchoate, 17 months ago)

Fixed test for blog 'allow_commenter_regist' when externalusermanagement is enabled. Case # 80055

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