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

Revision 1823, 19.3 kB (checked in by takayama, 20 months ago)

Fixed BugId:67959
* Added check for result of object loading

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