root/branches/release-34/lib/MT/App/Trackback.pm @ 1866

Revision 1866, 19.3 kB (checked in by bchoate, 20 months ago)

Changes to store binary state to junk_status column. BugId:79280

  • 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::Trackback;
8
9use strict;
10use base qw( MT::App );
11
12use File::Spec;
13use MT::TBPing;
14use MT::Trackback;
15use MT::Util qw( first_n_words encode_xml is_valid_url
16  start_background_task );
17use MT::JunkFilter qw(:constants);
18use MT::I18N
19  qw( encode_text guess_encoding const length_text wrap_text substr_text first_n_text );
20
21sub id { 'tb' }
22
23sub init {
24    my $app = shift;
25    $app->SUPER::init(@_) or return;
26    $app->add_methods(
27        ping => \&ping,
28        view => \&view,
29        rss  => \&rss,
30    );
31    $app->{default_mode} = 'ping';
32    $app;
33}
34
35sub view {
36    my $app = shift;
37    my $q   = $app->param;
38    require MT::Template;
39    require MT::Template::Context;
40    require MT::Entry;
41    my $entry_id = $q->param('entry_id');
42    my $entry =
43      MT::Entry->load( { id => $entry_id, status => MT::Entry::RELEASE() } )
44      or return $app->error(
45        $app->translate( "Invalid entry ID '[_1]'", $entry_id ) );
46    my $ctx = MT::Template::Context->new;
47    $ctx->stash( 'entry', $entry );
48    $ctx->{current_timestamp} = $entry->authored_on;
49    my $tmpl = MT::Template->load(
50        {
51            type    => 'pings',
52            blog_id => $entry->blog_id
53        }
54      )
55      or return $app->error(
56        $app->translate(
57            "You must define a Ping template in order to display pings.")
58      );
59    defined( my $html = $tmpl->build($ctx) )
60      or return $app->error( $tmpl->errstr );
61    $html;
62}
63
64## The following subroutine strips the UTF8 flag from a string, thus
65## forcing it into a series of bytes. "pack 'C0'" is a magic way of
66## forcing the following string to be packed as bytes, not as UTF8.
67sub no_utf8 {
68    for (@_) {
69        next if !defined $_;
70        $_ = pack 'C0A*', $_;
71    }
72}
73
74my %map = ( '&' => '&amp;', '"' => '&quot;', '<' => '&lt;', '>' => '&gt;' );
75
76sub _response {
77    my $app   = shift;
78    my %param = @_;
79    $app->response_code( $param{Code} );
80    $app->send_http_header('text/xml; charset=utf-8');
81    $app->{no_print_body} = 1;
82
83    if ( my $err = $param{Error} ) {
84        my $re = join '|', keys %map;
85        $err =~ s!($re)!$map{$1}!g;
86        $err = encode_text( $err, undef, 'utf-8' );
87        print <<XML;
88<?xml version="1.0" encoding="utf-8"?>
89<response>
90<error>1</error>
91<message>$err</message>
92</response>
93XML
94    }
95    else {
96        print <<XML;
97<?xml version="1.0" encoding="utf-8"?>
98<response>
99<error>0</error>
100XML
101        if ( my $rss = $param{RSS} ) {
102            $rss = encode_text( $rss, undef, 'utf-8' );
103            print $rss;
104        }
105        print <<XML;
106</response>
107XML
108    }
109
110    1;
111}
112
113sub _get_params {
114    my $app = shift;
115    my ( $tb_id, $pass );
116    if ( $tb_id = $app->param('tb_id') ) {
117        $pass = $app->param('pass');
118    }
119    else {
120        if ( my $pi = $app->path_info ) {
121            $pi =~ s!^/!!;
122            my $tbscript = $app->config('TrackbackScript');
123            $pi =~ s!.*\Q$tbscript\E/!!;
124            ( $tb_id, $pass ) = split /\//, $pi;
125        }
126    }
127    ( $tb_id, $pass );
128}
129
130sub _builtin_throttle {
131    my ( $eh, $app, $tb ) = @_;
132    my $user_ip = $app->remote_ip;
133    use MT::Util qw(offset_time_list);
134    my @ts = offset_time_list( time - 3600, $tb->blog_id );
135    my $from = sprintf(
136        "%04d%02d%02d%02d%02d%02d",
137        $ts[5] + 1900,
138        $ts[4] + 1,
139        @ts[ 3, 2, 1, 0 ]
140    );
141    require MT::TBPing;
142    if (
143        $app->config('OneHourMaxPings') <= MT::TBPing->count(
144            {
145                blog_id    => $tb->blog_id,
146                created_on => [$from]
147            },
148            { range => { created_on => 1 } }
149        )
150      )
151    {
152        return 0;
153    }
154
155    @ts = offset_time_list( time - $app->config('ThrottleSeconds') * 4000 - 1,
156        $tb->blog_id );
157    $from = sprintf(
158        "%04d%02d%02d%02d%02d%02d",
159        $ts[5] + 1900,
160        $ts[4] + 1,
161        @ts[ 3, 2, 1, 0 ]
162    );
163    my $terms = {
164        blog_id    => $tb->blog_id,
165        created_on => [$from]
166    };
167    my $count = MT::TBPing->count( $terms, { range => { created_on => 1 } } );
168    if ( $count >= $app->config('OneDayMaxPings') ) {
169        return 0;
170    }
171    return 1;
172}
173
174sub ping {
175    my $app = shift;
176    my $q   = $app->param;
177
178    return $app->_response(
179        Error => $app->translate("Trackback pings must use HTTP POST") )
180      if $app->request_method() ne 'POST';
181
182    my ( $tb_id, $pass ) = $app->_get_params;
183    return $app->_response(
184        Error => $app->translate("Need a TrackBack ID (tb_id).") )
185      unless $tb_id;
186
187    require MT::Trackback;
188    my $tb = MT::Trackback->load($tb_id)
189      or return $app->_response(
190        Error => $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) );
191
192    my $user_ip = $app->remote_ip;
193
194    ## Check if this user has been banned from sending TrackBack pings.
195    require MT::IPBanList;
196    my $iter = MT::IPBanList->load_iter( { blog_id => $tb->blog_id } );
197    while ( my $ban = $iter->() ) {
198        my $banned_ip = $ban->ip;
199        if ( $user_ip =~ /$banned_ip/ ) {
200            return $app->_response(
201                Error => $app->translate(
202                    "You are not allowed to send TrackBack pings.")
203            );
204        }
205    }
206
207    my ( $blog_id, $entry, $cat );
208    if ( $tb->entry_id ) {
209        require MT::Entry;
210        $entry = MT::Entry->load(
211            { id => $tb->entry_id, status => MT::Entry::RELEASE() } );
212        if ( !$entry ) {
213            return $app->_response( Error =>
214                  $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) );
215        }
216    }
217    elsif ( $tb->category_id ) {
218        require MT::Category;
219        $cat = MT::Category->load( $tb->category_id );
220    }
221    $blog_id = $tb->blog_id;
222
223    MT->add_callback( 'TBPingThrottleFilter', 1, undef,
224        \&MT::App::Trackback::_builtin_throttle );
225
226    my $passed_filter = MT->run_callbacks( 'TBPingThrottleFilter', $app, $tb );
227    if ( !$passed_filter ) {
228        return $app->_response(
229            Error => $app->translate(
230"You are pinging trackbacks too quickly. Please try again later."
231            ),
232            Code => "403 Throttled"
233        );
234    }
235
236    my ( $title, $excerpt, $url, $blog_name, $enc ) = map scalar $q->param($_),
237      qw( title excerpt url blog_name charset);
238
239    unless ($enc) {
240        my $content_type = $q->content_type();
241        if ( $content_type =~ m/;[ ]+charset=(.+)/i ) {
242            $enc = lc $1;
243            $enc =~ s/^\s+|\s+$//gs;
244        }
245    }
246
247    no_utf8( $tb_id, $title, $excerpt, $url, $blog_name );
248
249    # guess encoding as possible
250    $enc = MT::I18N::guess_encoding( $excerpt . $title . $blog_name )
251      unless $enc;
252    ( $title, $excerpt, $blog_name ) =
253      map { encode_text( $_, $enc ) } ( $title, $excerpt, $blog_name );
254
255    return $app->_response(
256        Error => $app->translate("Need a Source URL (url).") )
257      unless $url;
258
259    if ( my $fixed = MT::Util::is_valid_url( $url || "" ) ) {
260        $url = $fixed;
261    }
262    else {
263        return $app->_response(
264            Error => $app->translate( "Invalid URL '[_1]'", $url ) );
265    }
266
267    require MT::TBPing;
268    require MT::Blog;
269    my $blog = MT::Blog->load( $tb->blog_id );
270    my $cfg  = $app->config;
271
272    return $app->_response(
273        Error => $app->translate("This TrackBack item is disabled.") )
274      if $tb->is_disabled || !$cfg->AllowPings || !$blog || !$blog->allow_pings;
275
276    if ( $tb->passphrase && ( !$pass || $pass ne $tb->passphrase ) ) {
277        return $app->_response(
278            Error => $app->translate(
279                "This TrackBack item is protected by a passphrase.")
280        );
281    }
282
283    my $ping;
284
285    # Check for duplicates...
286    my @pings = MT::TBPing->load( { tb_id => $tb->id } );
287    foreach (@pings) {
288        if ( $_->source_url eq $url ) {
289            return $app->_response() if $_->is_junk;
290            if ( $app->remote_ip eq $_->ip ) {
291                $ping = $_;
292                last;
293            }
294            else {
295
296                # return success to quiet this pinger
297                return $app->_response();
298            }
299        }
300    }
301
302    if ( !$ping ) {
303        $ping ||= MT::TBPing->new;
304        $ping->blog_id( $tb->blog_id );
305        $ping->tb_id($tb_id);
306        $ping->source_url($url);
307        $ping->ip( $app->remote_ip || '' );
308        $ping->visible(1);
309    }
310    my $excerpt_max_len = const('LENGTH_ENTRY_PING_EXCERPT');
311    if ($excerpt) {
312        if ( length_text($excerpt) > $excerpt_max_len ) {
313            $excerpt = substr_text( $excerpt, 0, $excerpt_max_len - 3 ) . '...';
314        }
315        $title =
316          first_n_text( $excerpt, const('LENGTH_ENTRY_PING_TITLE_FROM_TEXT') )
317          unless defined $title;
318        $ping->excerpt($excerpt);
319    }
320    $ping->title( defined $title && $title ne '' ? $title : $url );
321    $ping->blog_name($blog_name);
322
323    # strip of any null characters (done after junk checks so they can
324    # monitor for that kind of activity)
325    for my $field (qw(title excerpt source_url blog_name)) {
326        my $val = $ping->column($field);
327        if ( $val =~ m/\x00/ ) {
328            $val =~ tr/\x00//d;
329            $ping->column( $field, $val );
330        }
331    }
332
333    if ( !MT->run_callbacks( 'TBPingFilter', $app, $ping ) ) {
334        return $app->_response( Error => "", Code => 403 );
335    }
336
337    if ( !$ping->is_junk ) {
338        MT::JunkFilter->filter($ping);
339    }
340
341    if ( !$ping->is_junk && $ping->visible && $blog->moderate_pings ) {
342        $ping->visible(0);
343    }
344
345    $ping->save
346      or return $app->_response( Error => "An internal error occured" );
347    if ( $ping->id && !$ping->is_junk ) {
348        my $msg = 'New TrackBack received.';
349        if ($entry) {
350            $msg = $app->translate( 'TrackBack on "[_1]" from "[_2]".',
351                $entry->title, $ping->blog_name );
352        }
353        elsif ($cat) {
354            $msg = $app->translate( "TrackBack on category '[_1]' (ID:[_2]).",
355                $cat->label, $cat->id );
356        }
357        require MT::Log;
358        $app->log(
359            {
360                message  => $msg,
361                class    => 'ping',
362                category => 'new',
363                blog_id  => $blog_id,
364                metadata => $ping->id,
365            }
366        );
367    }
368
369    if ( !$ping->is_junk ) {
370        if ( !$ping->visible ) {
371            $app->_send_ping_notification( $blog, $entry, $cat, $ping );
372        }
373        else {
374            start_background_task(
375                sub {
376                    ## If this is a trackback item for a particular entry, we need to
377                    ## rebuild the indexes in case the <$MTEntryTrackbackCount$> tag
378                    ## is being used. We also want to place the RSS files inside of the
379                    ## Local Site Path.
380                    $app->rebuild_indexes( Blog => $blog )
381                      or return $app->_response(
382                        Error => $app->translate(
383                            "Publish failed: [_1]",
384                            $app->errstr
385                        )
386                      );
387
388                    if ( $tb->entry_id ) {
389                        $app->rebuild_entry(
390                            Entry             => $entry->id,
391                            Blog              => $blog,
392                            BuildDependencies => 1
393                        );
394                    }
395                    if ( $tb->category_id ) {
396                        $app->publisher->_rebuild_entry_archive_type(
397                            Entry       => undef,
398                            Blog        => $blog,
399                            Category    => $cat,
400                            ArchiveType => 'Category'
401                        );
402                    }
403
404                    if ( $app->config('GenerateTrackBackRSS') ) {
405                        ## Now generate RSS feed for this trackback item.
406                        my $rss  = _generate_rss( $tb, 10 );
407                        my $base = $blog->archive_path;
408                        my $feed = File::Spec->catfile( $base,
409                            $tb->rss_file || $tb->id . '.xml' );
410                        my $fmgr = $blog->file_mgr;
411                        $fmgr->put_data( $rss, $feed )
412                          or return $app->_response(
413                            Error => $app->translate(
414                                "Can't create RSS feed '[_1]': ", $feed,
415                                $fmgr->errstr
416                            )
417                          );
418                    }
419                    $app->_send_ping_notification( $blog, $entry, $cat, $ping );
420                }
421            );
422        }
423    }
424    else {
425        $app->run_tasks('JunkExpiration');
426    }
427
428    return $app->_response;
429}
430
431# one of $entry or $cat must be passed.
432sub _send_ping_notification {
433    my $app = shift;
434    my ( $blog, $entry, $cat, $ping ) = @_;
435
436    return unless $blog->email_new_pings;
437
438    my $attn_reqd = $ping->is_moderated();
439    if ( $blog->email_attn_reqd_pings && !$attn_reqd ) {
440        return;
441    }
442
443    require MT::Mail;
444
445    my ( $author, $subj );
446    if ($entry) {
447        $author = $entry->author;
448    }
449    elsif ($cat) {
450        require MT::Author;
451        $author = MT::Author->load( $cat->author_id ) if $cat->author_id;
452    }
453    $app->set_language( $author->preferred_language )
454      if $author && $author->preferred_language;
455
456    if ( $author && $author->email ) {
457        if ($entry) {
458            $subj = $app->translate( 'New TrackBack Ping to Entry [_1] ([_2])',
459                $entry->id, $entry->title );
460        }
461        elsif ($cat) {
462            $subj =
463              $app->translate( 'New TrackBack Ping to Category [_1] ([_2])',
464                $cat->id, $cat->label );
465        }
466        my %head = (
467            id   => 'new_ping',
468            To   => $author->email,
469            From => $app->config('EmailAddressMain')
470              || (
471                  $author->nickname
472                ? $author->nickname . ' <' . $author->email . '>'
473                : $author->email
474              ),
475            Subject => '[' . $blog->name . '] ' . $subj
476        );
477        my $base;
478        {
479            local $app->{is_admin} = 1;
480            $base = $app->base . $app->mt_uri;
481        }
482        if ( $base =~ m!^/! ) {
483            my ($blog_domain) = $blog->site_url =~ m|(.+://[^/]+)|;
484            $base = $blog_domain . $base;
485        }
486        my $nonce =
487          MT::Util::perl_sha1_digest_hex( $ping->id
488              . $ping->created_on
489              . $blog->id
490              . $app->config->SecretToken );
491        my $approve_link = $base
492          . $app->uri_params(
493            'mode' => 'approve_item',
494            args   => {
495                blog_id => $blog->id,
496                '_type' => 'ping',
497                id      => $ping->id,
498                nonce   => $nonce
499            }
500          );
501        my $spam_link = $base
502          . $app->uri_params(
503            'mode' => 'handle_junk',
504            args   => {
505                blog_id => $blog->id,
506                '_type' => 'ping',
507                id      => $ping->id,
508                nonce   => $nonce
509            }
510          );
511        my $edit_link = $base
512          . $app->uri_params(
513            'mode' => 'view',
514            args => { blog_id => $blog->id, '_type' => 'ping', id => $ping->id }
515          );
516        my $ban_link = $base
517          . $app->uri_params(
518            'mode' => 'save',
519            args   => {
520                '_type' => 'banlist',
521                blog_id => $blog->id,
522                ip      => $ping->ip
523            }
524          );
525        my %param = (
526            blog           => $blog,
527            approve_url    => $approve_link,
528            spam_url       => $spam_link,
529            edit_url       => $edit_link,
530            ban_url        => $ban_link,
531            ping           => $ping,
532            unapproved     => !$ping->visible(),
533            state_editable => (
534                $author->is_superuser()
535                  || ( $author->permissions( $blog->id )->can_manage_feedback
536                    || $author->permissions( $blog->id )->can_publish_post )
537              ) ? 1 : 0,
538        );
539        $param{entry}    = $entry if $entry;
540        $param{category} = $cat   if $cat;
541
542        my $charset = $app->config('MailEncoding') || $app->charset;
543        $head{'Content-Type'} = qq(text/plain; charset="$charset");
544        my $body = MT->build_email( 'new-ping.tmpl', \%param );
545        MT::Mail->send( \%head, $body );
546    }
547}
548
549sub rss {
550    my $app = shift;
551    my ( $tb_id, $pass ) = $app->_get_params;
552    my $tb = MT::Trackback->load($tb_id)
553      or return $app->_response(
554        Error => $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) );
555    if ( my $eid = $tb->entry_id ) {
556        my $entry = $app->model('entry')->load($eid);
557        return $app->_response(
558            Error => $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) )
559          unless $entry && ( MT::Entry::RELEASE() == $entry->status );
560    }
561    elsif ( my $cid = $tb->category_id ) {
562        my $count = $app->model('entry')->count(
563            { status => MT::Entry::RELEASE() },
564            {
565                join =>
566                  MT::Placement->join_on( 'entry_id', { category_id => $cid } )
567            }
568        );
569        return $app->_response(
570            Error => $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) )
571          if $count <= 0;
572    }
573    my $rss = _generate_rss($tb);
574    $app->_response( RSS => $rss );
575}
576
577sub _generate_rss {
578    my ( $tb, $lastn ) = @_;
579    my $lang = MT->config->DefaultLanguage || 'en-us';
580    my $rss = <<RSS;
581<rss version="0.91"><channel>
582<title>@{[ $tb->title ]}</title>
583<link>@{[ $tb->url || '' ]}</link>
584<description>@{[ $tb->description || '' ]}</description>
585<language>$lang</language>
586RSS
587    my %arg;
588    if ($lastn) {
589        %arg = (
590            'sort'    => 'created_on',
591            direction => 'descend',
592            limit     => $lastn
593        );
594    }
595    my $iter = MT::TBPing->load_iter(
596      {
597        tb_id       => $tb->id,
598        junk_status => MT::TBPing::NOT_JUNK(),
599        visible     => 1
600      },
601      \%arg
602    );
603    while ( my $ping = $iter->() ) {
604        $rss .= sprintf qq(<item>\n<title>%s</title>\n<link>%s</link>\n),
605          encode_xml( $ping->title ), encode_xml( $ping->source_url );
606        if ( $ping->excerpt ) {
607            $rss .= sprintf qq(<description>%s</description>\n),
608              encode_xml( $ping->excerpt );
609        }
610        $rss .= qq(</item>\n);
611    }
612    $rss .= qq(</channel>\n</rss>);
613    my $enc = MT->config->PublishCharset || 'utf-8';
614    $rss = MT::I18N::encode_text( $rss, $enc, 'utf-8' ) if $enc ne 'utf-8';
615    $rss;
616}
617
618sub blog {
619    my $app = shift;
620    return $app->{_blog} if $app->{_blog};
621    if ( my ($tb_id) = $app->_get_params() ) {
622        require MT::Trackback;
623        my $tb = MT::Trackback->load($tb_id);
624        return undef unless $tb;
625        $app->{_blog} = MT::Blog->load( $tb->blog_id ) if $tb;
626    }
627    return $app->{_blog};
628}
629
6301;
631__END__
632
633=head1 NAME
634
635MT::App::Trackback
636
637=head1 METHODS
638
639=head2 init
640
641Call L<MT::App/init>, register the C<ping>, C<view> and C<rss>
642callbacks and set the application default_mode to C<ping>.
643
644=head2 view
645
646Build the trackback page for viewing.
647
648=head2 rss
649
650Generate and return RSS text for the trackback.
651
652=head2 blog
653
654Return the blog of the trackback.
655
656=head2 no_utf8
657
658This function removes UTF-8 from scalars.
659
660=head1 AUTHOR & COPYRIGHT
661
662Please see L<MT/AUTHOR & COPYRIGHT>.
663
664=cut
Note: See TracBrowser for help on using the browser.