root/branches/release-35/lib/MT/App/Trackback.pm @ 1941

Revision 1941, 19.0 kB (checked in by bchoate, 20 months ago)

Prevent updating existing pings. BugId:70216

  • 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();
290        }
291    }
292
293    if ( !$ping ) {
294        $ping ||= MT::TBPing->new;
295        $ping->blog_id( $tb->blog_id );
296        $ping->tb_id($tb_id);
297        $ping->source_url($url);
298        $ping->ip( $app->remote_ip || '' );
299        $ping->visible(1);
300    }
301    my $excerpt_max_len = const('LENGTH_ENTRY_PING_EXCERPT');
302    if ($excerpt) {
303        if ( length_text($excerpt) > $excerpt_max_len ) {
304            $excerpt = substr_text( $excerpt, 0, $excerpt_max_len - 3 ) . '...';
305        }
306        $title =
307          first_n_text( $excerpt, const('LENGTH_ENTRY_PING_TITLE_FROM_TEXT') )
308          unless defined $title;
309        $ping->excerpt($excerpt);
310    }
311    $ping->title( defined $title && $title ne '' ? $title : $url );
312    $ping->blog_name($blog_name);
313
314    # strip of any null characters (done after junk checks so they can
315    # monitor for that kind of activity)
316    for my $field (qw(title excerpt source_url blog_name)) {
317        my $val = $ping->column($field);
318        if ( $val =~ m/\x00/ ) {
319            $val =~ tr/\x00//d;
320            $ping->column( $field, $val );
321        }
322    }
323
324    if ( !MT->run_callbacks( 'TBPingFilter', $app, $ping ) ) {
325        return $app->_response( Error => "", Code => 403 );
326    }
327
328    if ( !$ping->is_junk ) {
329        MT::JunkFilter->filter($ping);
330    }
331
332    if ( !$ping->is_junk && $ping->visible && $blog->moderate_pings ) {
333        $ping->visible(0);
334    }
335
336    $ping->save
337      or return $app->_response( Error => "An internal error occured" );
338    if ( $ping->id && !$ping->is_junk ) {
339        my $msg = 'New TrackBack received.';
340        if ($entry) {
341            $msg = $app->translate( 'TrackBack on "[_1]" from "[_2]".',
342                $entry->title, $ping->blog_name );
343        }
344        elsif ($cat) {
345            $msg = $app->translate( "TrackBack on category '[_1]' (ID:[_2]).",
346                $cat->label, $cat->id );
347        }
348        require MT::Log;
349        $app->log(
350            {
351                message  => $msg,
352                class    => 'ping',
353                category => 'new',
354                blog_id  => $blog_id,
355                metadata => $ping->id,
356            }
357        );
358    }
359
360    if ( !$ping->is_junk ) {
361        if ( !$ping->visible ) {
362            $app->_send_ping_notification( $blog, $entry, $cat, $ping );
363        }
364        else {
365            start_background_task(
366                sub {
367                    ## If this is a trackback item for a particular entry, we need to
368                    ## rebuild the indexes in case the <$MTEntryTrackbackCount$> tag
369                    ## is being used. We also want to place the RSS files inside of the
370                    ## Local Site Path.
371                    $app->rebuild_indexes( Blog => $blog )
372                      or return $app->_response(
373                        Error => $app->translate(
374                            "Publish failed: [_1]",
375                            $app->errstr
376                        )
377                      );
378
379                    if ( $tb->entry_id ) {
380                        $app->rebuild_entry(
381                            Entry             => $entry->id,
382                            Blog              => $blog,
383                            BuildDependencies => 1
384                        );
385                    }
386                    if ( $tb->category_id ) {
387                        $app->publisher->_rebuild_entry_archive_type(
388                            Entry       => undef,
389                            Blog        => $blog,
390                            Category    => $cat,
391                            ArchiveType => 'Category'
392                        );
393                    }
394
395                    if ( $app->config('GenerateTrackBackRSS') ) {
396                        ## Now generate RSS feed for this trackback item.
397                        my $rss  = _generate_rss( $tb, 10 );
398                        my $base = $blog->archive_path;
399                        my $feed = File::Spec->catfile( $base,
400                            $tb->rss_file || $tb->id . '.xml' );
401                        my $fmgr = $blog->file_mgr;
402                        $fmgr->put_data( $rss, $feed )
403                          or return $app->_response(
404                            Error => $app->translate(
405                                "Can't create RSS feed '[_1]': ", $feed,
406                                $fmgr->errstr
407                            )
408                          );
409                    }
410                    $app->_send_ping_notification( $blog, $entry, $cat, $ping );
411                }
412            );
413        }
414    }
415    else {
416        $app->run_tasks('JunkExpiration');
417    }
418
419    return $app->_response;
420}
421
422# one of $entry or $cat must be passed.
423sub _send_ping_notification {
424    my $app = shift;
425    my ( $blog, $entry, $cat, $ping ) = @_;
426
427    return unless $blog->email_new_pings;
428
429    my $attn_reqd = $ping->is_moderated();
430    if ( $blog->email_attn_reqd_pings && !$attn_reqd ) {
431        return;
432    }
433
434    require MT::Mail;
435
436    my ( $author, $subj );
437    if ($entry) {
438        $author = $entry->author;
439    }
440    elsif ($cat) {
441        require MT::Author;
442        $author = MT::Author->load( $cat->author_id ) if $cat->author_id;
443    }
444    $app->set_language( $author->preferred_language )
445      if $author && $author->preferred_language;
446
447    if ( $author && $author->email ) {
448        if ($entry) {
449            $subj = $app->translate( 'New TrackBack Ping to Entry [_1] ([_2])',
450                $entry->id, $entry->title );
451        }
452        elsif ($cat) {
453            $subj =
454              $app->translate( 'New TrackBack Ping to Category [_1] ([_2])',
455                $cat->id, $cat->label );
456        }
457        my %head = (
458            id   => 'new_ping',
459            To   => $author->email,
460            From => $app->config('EmailAddressMain')
461              || (
462                  $author->nickname
463                ? $author->nickname . ' <' . $author->email . '>'
464                : $author->email
465              ),
466            Subject => '[' . $blog->name . '] ' . $subj
467        );
468        my $base;
469        {
470            local $app->{is_admin} = 1;
471            $base = $app->base . $app->mt_uri;
472        }
473        if ( $base =~ m!^/! ) {
474            my ($blog_domain) = $blog->site_url =~ m|(.+://[^/]+)|;
475            $base = $blog_domain . $base;
476        }
477        my $nonce =
478          MT::Util::perl_sha1_digest_hex( $ping->id
479              . $ping->created_on
480              . $blog->id
481              . $app->config->SecretToken );
482        my $approve_link = $base
483          . $app->uri_params(
484            'mode' => 'approve_item',
485            args   => {
486                blog_id => $blog->id,
487                '_type' => 'ping',
488                id      => $ping->id,
489                nonce   => $nonce
490            }
491          );
492        my $spam_link = $base
493          . $app->uri_params(
494            'mode' => 'handle_junk',
495            args   => {
496                blog_id => $blog->id,
497                '_type' => 'ping',
498                id      => $ping->id,
499                nonce   => $nonce
500            }
501          );
502        my $edit_link = $base
503          . $app->uri_params(
504            'mode' => 'view',
505            args => { blog_id => $blog->id, '_type' => 'ping', id => $ping->id }
506          );
507        my $ban_link = $base
508          . $app->uri_params(
509            'mode' => 'save',
510            args   => {
511                '_type' => 'banlist',
512                blog_id => $blog->id,
513                ip      => $ping->ip
514            }
515          );
516        my %param = (
517            blog           => $blog,
518            approve_url    => $approve_link,
519            spam_url       => $spam_link,
520            edit_url       => $edit_link,
521            ban_url        => $ban_link,
522            ping           => $ping,
523            unapproved     => !$ping->visible(),
524            state_editable => (
525                $author->is_superuser()
526                  || ( $author->permissions( $blog->id )->can_manage_feedback
527                    || $author->permissions( $blog->id )->can_publish_post )
528              ) ? 1 : 0,
529        );
530        $param{entry}    = $entry if $entry;
531        $param{category} = $cat   if $cat;
532
533        my $charset = $app->config('MailEncoding') || $app->charset;
534        $head{'Content-Type'} = qq(text/plain; charset="$charset");
535        my $body = MT->build_email( 'new-ping.tmpl', \%param );
536        MT::Mail->send( \%head, $body );
537    }
538}
539
540sub rss {
541    my $app = shift;
542    my ( $tb_id, $pass ) = $app->_get_params;
543    my $tb = MT::Trackback->load($tb_id)
544      or return $app->_response(
545        Error => $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) );
546    if ( my $eid = $tb->entry_id ) {
547        my $entry = $app->model('entry')->load($eid);
548        return $app->_response(
549            Error => $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) )
550          unless $entry && ( MT::Entry::RELEASE() == $entry->status );
551    }
552    elsif ( my $cid = $tb->category_id ) {
553        my $exist = $app->model('entry')->exist(
554            { status => MT::Entry::RELEASE() },
555            {
556                join =>
557                  MT::Placement->join_on( 'entry_id', { category_id => $cid } )
558            }
559        );
560        return $app->_response(
561            Error => $app->translate( "Invalid TrackBack ID '[_1]'", $tb_id ) )
562          unless $exist;
563    }
564    my $rss = _generate_rss($tb);
565    $app->_response( RSS => $rss );
566}
567
568sub _generate_rss {
569    my ( $tb, $lastn ) = @_;
570    my $lang = MT->config->DefaultLanguage || 'en-us';
571    my $rss = <<RSS;
572<rss version="0.91"><channel>
573<title>@{[ $tb->title ]}</title>
574<link>@{[ $tb->url || '' ]}</link>
575<description>@{[ $tb->description || '' ]}</description>
576<language>$lang</language>
577RSS
578    my %arg;
579    if ($lastn) {
580        %arg = (
581            'sort'    => 'created_on',
582            direction => 'descend',
583            limit     => $lastn
584        );
585    }
586    my $iter = MT::TBPing->load_iter(
587      {
588        tb_id       => $tb->id,
589        junk_status => MT::TBPing::NOT_JUNK(),
590        visible     => 1
591      },
592      \%arg
593    );
594    while ( my $ping = $iter->() ) {
595        $rss .= sprintf qq(<item>\n<title>%s</title>\n<link>%s</link>\n),
596          encode_xml( $ping->title ), encode_xml( $ping->source_url );
597        if ( $ping->excerpt ) {
598            $rss .= sprintf qq(<description>%s</description>\n),
599              encode_xml( $ping->excerpt );
600        }
601        $rss .= qq(</item>\n);
602    }
603    $rss .= qq(</channel>\n</rss>);
604    my $enc = MT->config->PublishCharset || 'utf-8';
605    $rss = MT::I18N::encode_text( $rss, $enc, 'utf-8' ) if $enc ne 'utf-8';
606    $rss;
607}
608
609sub blog {
610    my $app = shift;
611    return $app->{_blog} if $app->{_blog};
612    if ( my ($tb_id) = $app->_get_params() ) {
613        require MT::Trackback;
614        my $tb = MT::Trackback->load($tb_id);
615        return undef unless $tb;
616        $app->{_blog} = MT::Blog->load( $tb->blog_id ) if $tb;
617    }
618    return $app->{_blog};
619}
620
6211;
622__END__
623
624=head1 NAME
625
626MT::App::Trackback
627
628=head1 METHODS
629
630=head2 init
631
632Call L<MT::App/init>, register the C<ping>, C<view> and C<rss>
633callbacks and set the application default_mode to C<ping>.
634
635=head2 view
636
637Build the trackback page for viewing.
638
639=head2 rss
640
641Generate and return RSS text for the trackback.
642
643=head2 blog
644
645Return the blog of the trackback.
646
647=head2 no_utf8
648
649This function removes UTF-8 from scalars.
650
651=head1 AUTHOR & COPYRIGHT
652
653Please see L<MT/AUTHOR & COPYRIGHT>.
654
655=cut
Note: See TracBrowser for help on using the browser.