root/branches/release-38/lib/MT/Entry.pm @ 2237

Revision 2237, 29.9 kB (checked in by bchoate, 19 months ago)

Fixed pings method. Thanks to Reed Cartwright for the report. BugId:79518

  • 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::Entry;
8
9use strict;
10
11use MT::Tag; # Holds MT::Taggable
12use base qw( MT::Object MT::Taggable MT::Scorable );
13
14use MT::Blog;
15use MT::Author;
16use MT::Category;
17use MT::Memcached;
18use MT::Placement;
19use MT::Comment;
20use MT::TBPing;
21use MT::Util qw( archive_file_for discover_tb start_end_period extract_domain
22                 extract_domains weaken );
23
24sub CATEGORY_CACHE_TIME () { 604800 } ## 7 * 24 * 60 * 60 == 1 week
25
26__PACKAGE__->install_properties({
27    column_defs => {
28        'id' => 'integer not null auto_increment',
29        'blog_id' => 'integer not null',
30        'status' => 'smallint not null',
31        'author_id' => 'integer not null',
32        'allow_comments' => 'boolean',
33        'title' => 'string(255)',
34        'excerpt' => 'text',
35        'text' => 'text',
36        'text_more' => 'text',
37        'convert_breaks' => 'string(30)',
38        'to_ping_urls' => 'text',
39        'pinged_urls' => 'text',
40        'allow_pings' => 'boolean',
41        'keywords' => 'text',
42        'tangent_cache' => 'text',
43        'basename' => 'string(255)',
44        'atom_id' => 'string(255)',
45        'authored_on' => 'datetime',
46        'week_number' => 'integer',
47        'template_id' => 'integer',
48        'comment_count' => 'integer',
49        'ping_count' => 'integer',
50## Have to keep this around for use in mt-upgrade.cgi.
51        'category_id' => 'integer',
52    },
53    indexes => {
54        status => 1,
55        author_id => 1,
56        created_on => 1,
57        modified_on => 1,
58        authored_on => 1,
59        # For lookups
60        basename => 1,
61        # Page listings are published in order by title
62        title => 1,
63        blog_author => {
64            columns => [ 'blog_id', 'class', 'author_id', 'authored_on' ],
65        },
66        # For optimizing weekly archives, selected by blog, class,
67        # status.
68        blog_week => {
69            columns => [ 'blog_id', 'class', 'status', 'week_number' ],
70        },
71        # For system-overview listings where we list all entries of
72        # a particular class by authored on date
73        class_authored => {
74            columns => [ 'class', 'authored_on' ],
75        },
76        # For most blog-level listings, where we list all entries
77        # in a blog with a particular class by authored on date.
78        blog_authored => {
79            columns => ['blog_id', 'class', 'authored_on'],
80        },
81        # For most publishing listings, where we list entries in a blog
82        # with a particular class, publish status (2) and authored on date
83        blog_stat_date => {
84            columns => ['blog_id', 'class', 'status', 'authored_on', 'id'],
85        },
86        # for tag count
87        tag_count => {
88            columns => ['status', 'class', 'blog_id', 'id'],
89        },
90    },
91    defaults => {
92        comment_count => 0,
93        ping_count => 0,
94    },
95    child_of => 'MT::Blog',
96    child_classes => ['MT::Comment','MT::Placement','MT::Trackback','MT::FileInfo'],
97    audit => 1,
98    meta => 1,
99    datasource => 'entry',
100    primary_key => 'id',
101    class_type => 'entry',
102});
103
104sub HOLD ()    { 1 }
105sub RELEASE () { 2 }
106sub REVIEW ()  { 3 }
107sub FUTURE ()  { 4 }
108
109use Exporter;
110*import = \&Exporter::import;
111use vars qw( @EXPORT_OK %EXPORT_TAGS);
112@EXPORT_OK = qw( HOLD RELEASE FUTURE );
113%EXPORT_TAGS = (constants => [ qw(HOLD RELEASE FUTURE) ]);
114
115sub class_label {
116    MT->translate("Entry");
117}
118
119sub class_label_plural {
120    MT->translate("Entries");
121}
122
123sub container_type {
124    return "category";
125}
126
127sub container_label {
128    MT->translate("Category");
129}
130
131sub cache_key {
132    my($entry_id, $key);
133    if (@_ == 3) {
134        ($entry_id, $key) = @_[1, 2];
135    } else {
136        ($entry_id, $key) = ($_[0]->id, $_[1]);
137    }
138    return sprintf "entry%s-%d", $key, $entry_id;
139}
140
141sub status_text {
142    my $s = $_[0];
143    $s == HOLD ? "Draft" :
144        $s == RELEASE ? "Publish" :
145            $s == REVIEW ? "Review" : 
146            $s == FUTURE ? "Future" : '';
147}
148
149sub status_int {
150    my $s = lc $_[0];   ## Lower-case it so that it's case-insensitive
151    $s eq 'draft' ? HOLD :
152        $s eq 'publish' ? RELEASE :
153            $s eq 'review' ? REVIEW :
154                $s eq 'future' ? FUTURE : undef;
155}
156
157sub authored_on_obj {
158    my $obj = shift;
159    return $obj->column_as_datetime('authored_on');
160}
161
162sub next {
163    my $entry = shift;
164    my($opt) = @_;
165    my $terms;
166    if (ref $opt) {
167        $terms = $opt;
168    }
169    else {
170        $terms = $opt ? { status => RELEASE } : {};
171    }
172    $entry->_nextprev('next', $terms);
173}
174
175sub previous {
176    my $entry = shift;
177    my($opt) = @_;
178    my $terms;
179    if (ref $opt) {
180        $terms = $opt;
181    }
182    else {
183        $terms = $opt ? { status => RELEASE } : {};
184    }
185    $entry->_nextprev('previous', $terms);
186}
187
188sub _nextprev {
189    my $obj = shift;
190    my $class = ref($obj);
191    my ($direction, $terms) = @_;
192    return undef unless ($direction eq 'next' || $direction eq 'previous');
193    my $next = $direction eq 'next';
194
195    $terms->{author_id} = $obj->author_id if delete $terms->{by_author};
196    if (delete $terms->{by_category}) {
197        if (my $c = $obj->category) {
198            $terms->{category_id} = $c->id;
199        }
200        else {
201            return undef;
202        }
203    }
204
205    my $label = '__' . $direction;
206    $label .= ':author='. $terms->{author_id} if exists $terms->{author_id};
207    $label .= ':category='. $terms->{category_id} if exists $terms->{category_id};
208    return $obj->{$label} if $obj->{$label};
209
210    my $args = {};
211    if (my $cat_id = delete $terms->{category_id}) {
212        my $join = MT::Placement->join_on('entry_id',
213            { category_id => $cat_id }
214        );
215        $args->{join} = $join;
216    }
217
218    my $o = $obj->nextprev(
219        direction => $direction,
220        terms     => { blog_id => $obj->blog_id, class => $obj->class, %$terms },
221        args      => $args,
222        by        => 'authored_on',
223    );
224    weaken($obj->{$label} = $o) if $o;
225    return $o;
226}
227
228sub trackback {
229    my $entry = shift;
230    $entry->cache_property('trackback', sub {
231        require MT::Trackback;
232        if ($entry->id) {
233            return scalar MT::Trackback->load({ entry_id => $entry->id });
234        }
235    }, @_);
236}
237
238sub author {
239    my $entry = shift;
240    $entry->cache_property('author', sub {
241        return undef unless $entry->author_id;
242        my $req = MT::Request->instance();
243        my $author_cache = $req->stash('author_cache');
244        my $author = $author_cache->{$entry->author_id};
245        unless ($author) {
246            require MT::Author;
247            $author = MT::Author->load($entry->author_id)
248                or return undef;
249            $author_cache->{$entry->author_id} = $author;
250            $req->stash('author_cache', $author_cache);
251        }
252        $author;
253    });
254}
255
256sub __load_category_data {
257    my $entry = shift;
258    my $t = MT->get_timer;
259    $t->pause_partial if $t;
260    my $cache = MT::Memcached->instance;
261    my $memkey = $entry->cache_key('categories');
262    my $rows;
263    unless ($rows = $cache->get($memkey)) {
264        require MT::Placement;
265        my @maps = MT::Placement->search({ entry_id => $entry->id });
266        $rows = [ map { [ $_->category_id, $_->is_primary ] } @maps ];
267        $cache->set($memkey, $rows, CATEGORY_CACHE_TIME);
268    }
269    $t->mark('MT::Entry::__load_category_data') if $t;
270    return $rows;
271}
272
273sub flush_category_cache {
274    my($copy, $place) = @_;
275    MT::Memcached->instance->delete(
276        MT::Entry->cache_key($place->entry_id, 'categories')
277    );
278}
279
280MT::Placement->add_trigger(
281    post_save   => \&flush_category_cache,
282    post_remove => \&flush_category_cache
283);
284
285sub category {
286    my $entry = shift;
287    $entry->cache_property('category', sub {
288        my $rows = $entry->__load_category_data or return;
289        my @rows = grep { $_->[1] } @$rows or return;
290        require MT::Category;
291        return MT::Category->lookup( $rows[0] );
292    });
293}
294
295sub categories {
296    my $entry = shift;
297    $entry->cache_property('categories', sub {
298        my $rows = $entry->__load_category_data or return;
299        my $cats = MT::Category->lookup_multi([ map { $_->[0] } @$rows ]);
300        my @cats = sort { $a->label cmp $b->label } @$cats;
301        return \@cats;
302    });
303}
304
305sub is_in_category {
306    my $entry = shift;
307    my($cat) = @_;
308    my $cats = $entry->categories;
309    for my $c (@$cats) {
310        return 1 if $c->id == $cat->id;
311    }
312    0;
313}
314
315sub comments {
316    my $entry = shift;
317    my ($terms, $args) = @_;
318    require MT::Comment;
319    if ($terms || $args) {
320        $terms ||= {};
321        $terms->{entry_id} = $entry->id;
322        return [ MT::Comment->load( $terms, $args ) ];
323    } else {
324        $entry->cache_property('comments', sub {
325            [ MT::Comment->load({ entry_id => $entry->id }) ];
326        });
327    }
328}
329
330sub comment_latest {
331    my $entry = shift;
332    $entry->cache_property('comment_latest', sub {
333        require MT::Comment;
334        MT::Comment->load({
335            entry_id => $entry->id,
336            visible => 1
337        }, {
338            'sort' => 'created_on',
339            direction => 'descend',
340            limit => 1,
341        });
342    });
343}
344
345MT::Comment->add_trigger(
346    post_save => sub {
347        my $comment = shift;
348        my $entry   = MT::Entry->load( $comment->entry_id )
349            or return;
350        my $count   = MT::Comment->count(
351            {
352                entry_id => $comment->entry_id,
353                visible  => 1,
354            }
355        );
356        $entry->comment_count($count);
357        $entry->save;
358    }
359);
360
361MT::Comment->add_trigger(
362    post_remove => sub {
363        my $comment = shift;
364        my $entry   = MT::Entry->load( $comment->entry_id )
365            or return;
366        if ( $comment->visible ) {
367            my $count = $entry->comment_count > 0 ? $entry->comment_count - 1 : 0;
368            $entry->comment_count($count);
369            $entry->save;
370        }
371    }
372);
373
374sub pings {
375    my $entry = shift;
376    my ($terms, $args) = @_;
377    my $tb = $entry->trackback;
378    return undef unless $tb;
379    if ($terms || $args) {
380        $terms ||= {};
381        $terms->{tb_id} = $tb->id;
382        return [ MT::TBPing->load( $terms, $args ) ];
383    } else {
384        $entry->cache_property('pings', sub {
385            [ MT::TBPing->load({ tb_id => $tb->id }) ];
386        });
387    }
388}
389
390MT::TBPing->add_trigger(
391    post_save => sub {
392        my $ping = shift;
393        require MT::Trackback;
394        if ( my $tb = MT::Trackback->load( $ping->tb_id ) ) {
395            if ( $tb->entry_id ) {
396                my $entry = MT::Entry->load( $tb->entry_id )
397                    or return;
398                my $count = MT::TBPing->count(
399                    {
400                        tb_id   => $tb->id,
401                        visible => 1,
402                    }
403                );
404                $entry->ping_count($count);
405                $entry->save;
406            }
407        }
408    }
409);
410
411MT::TBPing->add_trigger(
412    post_remove => sub {
413        my $ping = shift;
414        require MT::Trackback;
415        if ( my $tb = MT::Trackback->load( $ping->tb_id ) ) {
416            if ( $tb->entry_id && $ping->visible ) {
417                my $entry = MT::Entry->load( $tb->entry_id )
418                    or return;
419                my $count = $entry->ping_count > 0 ? $entry->ping_count - 1 : 0;
420                $entry->ping_count($count);
421                $entry->save;
422            }
423        }
424    }
425);
426
427sub archive_file {
428    my $entry = shift;
429    my($at) = @_;
430    my $blog = $entry->blog() || return $entry->error(MT->translate(
431                                                     "Load of blog failed: [_1]",
432                                                     MT::Blog->errstr));
433    unless ($at) {
434        $at = $blog->archive_type_preferred || $blog->archive_type;
435        return '' if !$at || $at eq 'None';
436        return '' if $at eq 'Page';
437        my %at = map { $_ => 1 } split /,/, $at;
438        # FIXME: should draw from list of registered archive types
439        for my $tat (qw( Individual Daily Weekly Author-Monthly Category-Monthly Monthly Category )) {
440            $at = $tat if $at{$tat};
441            last;
442        }
443    }
444    archive_file_for($entry, $blog, $at);
445}
446
447sub archive_url {
448    my $entry = shift;
449    my $blog = $entry->blog() || return $entry->error(MT->translate(
450                                                     "Load of blog failed: [_1]",
451                                                     MT::Blog->errstr));
452    my $url = $blog->archive_url || "";
453    $url .= '/' unless $url =~ m!/$!;
454    $url . $entry->archive_file(@_);
455}
456
457sub permalink {
458    my $entry = shift;
459    my $blog = $entry->blog() || return $entry->error(MT->translate(
460                                                     "Load of blog failed: [_1]",
461                                                     MT::Blog->errstr));
462    my $url = $entry->archive_url($_[0]);
463    my $effective_archive_type = ($_[0]
464        || $blog->archive_type_preferred
465        || $blog->archive_type);
466    $url .= '#' . ($_[1]->{valid_html} ? 'a' : '') . 
467        sprintf("%06d", $entry->id)
468        unless ($effective_archive_type eq 'Individual' 
469        || $_[1]->{no_anchor});
470    $url;
471}
472
473sub all_permalinks {
474    my $entry = shift;
475    my $blog = $entry->blog || return $entry->error(MT->translate(
476                                                    "Load of blog failed: [_1]",
477                                                    MT::Blog->errstr));
478    my @at = split /,/, $blog->archive_type;
479    return unless @at;
480    my @urls;
481    for my $at (@at) {
482        push @urls, $entry->permalink($at);
483    }
484    @urls;
485}
486
487sub text_filters {
488    my $entry = shift;
489    my $filters = $entry->convert_breaks;
490    if (!defined $filters) {
491        my $blog = $entry->blog() || return [];
492        $filters = $blog->convert_paras;
493    }
494    return [] unless $filters;
495    if ($filters eq '1') {
496        return [ '__default__' ];
497    } else {
498        return [ split /\s*,\s*/, $filters ];
499    }
500}
501
502sub get_excerpt {
503    my $entry = shift;
504    my($words) = @_;
505    return $entry->excerpt if $entry->excerpt;
506    my $excerpt = MT->apply_text_filters($entry->text, $entry->text_filters);
507    my $blog = $entry->blog() || return $entry->error(MT->translate(
508                                                     "Load of blog failed: [_1]",
509                                                     MT::Blog->errstr));
510    MT::I18N::first_n_text($excerpt, $words || $blog->words_in_excerpt || MT::I18N::const('DEFAULT_LENGTH_ENTRY_EXCERPT')) . '...';
511}
512
513sub pinged_url_list {
514    my $entry = shift;
515    my (%param) = @_;
516    my $include_failures = $param{Failures} || $param{OnlyFailures};
517    my $exclude_successes = $param{OnlyFailures};
518    my $urls = $entry->pinged_urls;
519    return [] unless $urls && $urls =~ /\S/;
520    my %urls = map { $_ => 1 } split /\r?\n/, $urls;
521    my %to_ping = map { $_ => 1 } @{ $entry->to_ping_url_list };
522    foreach (keys %to_ping) {
523        delete $urls{$_} if exists $urls{$_};
524    }
525    my @urls = keys %urls;
526    foreach (@urls) {
527        if (m/^([^ ]+) /) {
528            delete $urls{$_}; # remove ones with error messages
529            $urls{$1} = 1 if $include_failures;
530        } else {
531            delete $urls{$_} if $exclude_successes;
532        }
533    }
534    [ keys %urls ];
535}
536
537sub to_ping_url_list {
538    my $entry = shift;
539    my $urls = $entry->to_ping_urls;
540    return [] unless $urls && $urls =~ /\S/;
541    [ split /\r?\n/, $urls ];
542}
543
544# TBD: Write a test for this routine
545sub make_atom_id {
546    my $entry = shift;
547
548    my $blog = $entry->blog;
549    my ($host, $year, $path, $blog_id, $entry_id);
550    $blog_id = $blog->id;
551    $entry_id = $entry->id;
552    my $url = $blog->site_url || '';
553    return unless $url;
554    $url .= '/' unless $url =~ m!/$!;
555    if ($url && ($url =~ m!^https?://([^/:]+)(?::\d+)?(/.*)$!)) {
556        $host = $1;
557        $path = $2;
558    }
559    if ($entry->authored_on && ($entry->authored_on =~ m/^(\d{4})/)) {
560        $year = $1;
561    }
562    return unless $host && $year && $path && $blog_id && $entry_id;
563    qq{tag:$host,$year:$path/$blog_id.$entry_id};
564}
565
566sub discover_tb_from_entry {
567    my $entry = shift;
568    ## If we need to auto-discover TrackBack ping URLs, do that here.
569    my $cfg = MT->config;
570    my $blog = $entry->blog();
571    my $send_tb = $cfg->OutboundTrackbackLimit;
572    if ($send_tb ne 'off' && 
573        $blog && ($blog->autodiscover_links
574                  || $blog->internal_autodiscovery)) {
575        my @tb_domains;
576        if ($send_tb eq 'selected') {
577            @tb_domains = $cfg->OutboundTrackbackDomains;
578        } elsif ($send_tb eq 'local') {
579            my $iter = MT::Blog->load_iter();
580            while (my $b = $iter->()) {
581                next if $b->id == $blog->id;
582                push @tb_domains, extract_domain($b->site_url);
583            }
584        }
585        my $tb_domains;
586        if (@tb_domains) {
587            $tb_domains = '';
588            my %seen;
589            foreach (@tb_domains) {
590                next unless $_;
591                $_ = lc($_);
592                next if $seen{$_};
593                $tb_domains .= '|' if $tb_domains ne '';
594                $tb_domains .= quotemeta($_);
595                $seen{$_} = 1;
596            }
597            $tb_domains = '(' . $tb_domains . ')' if $tb_domains;
598        }
599        my $archive_domain;
600        ($archive_domain) = extract_domains($blog->archive_url);
601        my %to_ping = map { $_ => 1 } @{ $entry->to_ping_url_list };
602        my %pinged = map { $_ => 1 } @{ $entry->pinged_url_list(IncludeFailures => 1) };
603        my $body = $entry->text . ($entry->text_more || "");
604        $body = MT->apply_text_filters($body, $entry->text_filters);
605        while ($body =~ m!<a\s.*?\bhref\s*=\s*(["']?)([^'">]+)\1!gsi) {
606            my $url = $2;
607            my $url_domain;
608            ($url_domain) = extract_domains($url);
609            if ($url_domain =~ m/\Q$archive_domain\E$/i) {
610                next if !$blog->internal_autodiscovery;
611            } else {
612                next if !$blog->autodiscover_links;
613            }
614            next if $tb_domains && lc($url_domain) !~ m/$tb_domains$/;
615            if (my $item = discover_tb($url)) {
616                $to_ping{ $item->{ping_url} } = 1
617                    unless $pinged{$item->{ping_url}};
618            }
619        }
620        $entry->to_ping_urls(join "\n", keys %to_ping);
621    }
622}
623
624sub sync_assets {
625    my $entry = shift;
626    my $text = ($entry->text || '') . "\n" . ($entry->text_more || '');
627
628    require MT::ObjectAsset;
629    my @assets = MT::ObjectAsset->load({
630        object_id => $entry->id,
631        blog_id => $entry->blog_id,
632        object_ds => $entry->datasource,
633        embedded => 1,
634    });
635    my %assets = map { $_->asset_id => $_->id } @assets;
636    while ($text =~ m!<form[^>]*?\smt:asset-id=["'](\d+)["'][^>]*?>(.+?)</form>!gis) {
637        my $id = $1;
638        my $innards = $2;
639
640        # reference to an existing asset...
641        if (exists $assets{$id}) {
642            $assets{$id} = 0;
643        } else {
644            # is asset exists?
645            my $asset = MT->model('asset')->load({ id => $id }) or next;
646
647            my $map = new MT::ObjectAsset;
648            $map->blog_id($entry->blog_id);
649            $map->asset_id($id);
650            $map->object_ds($entry->datasource);
651            $map->object_id($entry->id);
652            $map->embedded(1);
653            $map->save;
654            $assets{$id} = 0;
655        }
656    }
657    if (my @old_maps = grep { $assets{$_->asset_id} } @assets) {
658        my @old_ids = map { $_->id } grep { $_->embedded } @old_maps;
659        MT::ObjectAsset->remove( { id => \@old_ids })
660            if @old_ids;
661    }
662    return 1;
663}
664
665sub save {
666    my $entry = shift;
667    my $is_new = $entry->id ? 0 : 1;
668
669    ## If there's no basename specified, create a unique basename.
670    if (!defined($entry->basename) || ($entry->basename eq '')) {
671        my $name = MT::Util::make_unique_basename($entry);
672        $entry->basename($name);
673    }
674    if (!$entry->id && !$entry->authored_on) {
675        my @ts = MT::Util::offset_time_list(time, $entry->blog_id);
676        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
677            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
678        $entry->authored_on($ts);
679    }
680    if (my $dt = $entry->authored_on_obj) {
681        my ($yr, $w) = $dt->week;
682        $entry->week_number($yr * 100 + $w);
683    }
684
685    my $sync_assets = $entry->is_changed('text')
686        || $entry->is_changed('text_more');
687
688    unless ($entry->SUPER::save(@_)) {
689        print STDERR "error during save: " . $entry->errstr . "\n";
690        die $entry->errstr;
691    }
692
693    $entry->sync_assets() if $sync_assets;
694
695    if (!$entry->atom_id && (($entry->status || 0) != HOLD)) {
696        $entry->atom_id($entry->make_atom_id());
697        $entry->SUPER::save(@_) if $entry->atom_id;
698    }
699
700    ## If pings are allowed on this entry, create or update
701    ## the corresponding TrackBack object for this entry.
702    require MT::Trackback;
703    if ($entry->allow_pings) {
704        my $tb;
705        unless ($tb = $entry->trackback) {
706            $tb = MT::Trackback->new;
707            $tb->blog_id($entry->blog_id);
708            $tb->entry_id($entry->id);
709            $tb->category_id(0);   ## category_id can't be NULL
710        }
711        $tb->title($entry->title);
712        $tb->description($entry->get_excerpt);
713        $tb->url($entry->permalink);
714        $tb->is_disabled(0);
715        $tb->save
716            or return $entry->error($tb->errstr);
717        $entry->trackback($tb);
718    } else {
719        ## If there is a TrackBack item for this entry, but
720        ## pings are now disabled, make sure that we mark the
721        ## object as disabled.
722        if (my $tb = $entry->trackback) {
723            $tb->is_disabled(1);
724            $tb->save
725                or return $entry->error($tb->errstr);
726        }
727    }
728
729    $entry->clear_cache() if $is_new;
730
731    1;
732}
733
734sub remove {
735    my $entry = shift;
736    if (ref $entry) {
737        $entry->remove_children({ key => 'entry_id' }) or return;
738
739        # Remove MT::ObjectAsset records
740        my $class = MT->model('objectasset');
741        my $iter = $class->load_iter({ object_id => $entry->id, object_ds => $entry->class_type });
742        while (my $o = $iter->()) {
743            $o->remove;
744        }
745    }
746
747
748    $entry->SUPER::remove(@_);
749}
750
751sub blog {
752    my ($entry) = @_;
753    $entry->cache_property('blog', sub {
754        my $blog_id = $entry->blog_id;
755        require MT::Blog;
756        MT::Blog->load($blog_id) or
757            $entry->error(MT->translate(
758            "Load of blog '[_1]' failed: [_2]", $blog_id, MT::Blog->errstr));
759    });
760}
761
762sub to_hash {
763    my $entry = shift;
764    my $hash = $entry->SUPER::to_hash(@_);
765
766    $hash->{'entry.text_html'} = sub { MT->apply_text_filters($entry->text, $entry->text_filters) };
767    $hash->{'entry.text_more_html'} = sub { MT->apply_text_filters($entry->text_more, $entry->text_filters) };
768    $hash->{'entry.permalink'} = $entry->permalink;
769    $hash->{'entry.status_text'} = $entry->status_text;
770    $hash->{'entry.status_is_' . $entry->status} = 1;
771    $hash->{'entry.created_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->created_on) };
772    $hash->{'entry.modified_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->modified_on) };
773    $hash->{'entry.authored_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->authored_on) };
774
775    # Populate author info
776    my $auth = $entry->author or return $hash;
777    my $auth_hash = $auth->to_hash;
778    $hash->{"entry.$_"} = $auth_hash->{$_} foreach keys %$auth_hash;
779
780    $hash;
781}
782
783#trans('Draft')
784#trans('Review')
785#trans('Future')
786
7871;
788__END__
789
790=head1 NAME
791
792MT::Entry - Movable Type entry record
793
794=head1 SYNOPSIS
795
796    use MT::Entry;
797    my $entry = MT::Entry->new;
798    $entry->blog_id($blog->id);
799    $entry->status(MT::Entry::RELEASE());
800    $entry->author_id($author->id);
801    $entry->title('My title');
802    $entry->text('Some text');
803    $entry->save
804        or die $entry->errstr;
805
806=head1 DESCRIPTION
807
808An I<MT::Entry> object represents an entry in the Movable Type system. It
809contains all of the metadata about the entry (author, status, category, etc.),
810as well as the actual body (and extended body) of the entry.
811
812=head1 USAGE
813
814As a subclass of I<MT::Object>, I<MT::Entry> inherits all of the
815data-management and -storage methods from that class; thus you should look
816at the I<MT::Object> documentation for details about creating a new object,
817loading an existing object, saving an object, etc.
818
819The following methods are unique to the I<MT::Entry> interface:
820
821=head2 $entry->next
822
823Loads and returns the next entry, where "next" is defined as the next record
824in ascending chronological order (the entry posted after the current entry).
825entry I<$entry>).
826
827Returns an I<MT::Entry> object representing this next entry; if there is not
828a next entry, returns C<undef>.
829
830Caches the return value internally so that subsequent calls will not have to
831re-query the database.
832
833=head2 $entry->previous
834
835Loads and returns the previous entry, where "previous" is defined as the
836previous record in ascending chronological order (the entry posted before the
837current entry I<$entry>).
838
839Returns an I<MT::Entry> object representing this previous entry; if there is
840not a next entry, returns C<undef>.
841
842Caches the return value internally so that subsequent calls will not have to
843re-query the database.
844
845=head2 $entry->author
846
847Returns an I<MT::Author> object representing the author of the entry
848I<$entry>. If the author record has been removed, returns C<undef>.
849
850Caches the return value internally so that subsequent calls will not have to
851re-query the database.
852
853=head2 $entry->category
854
855Returns an I<MT::Category> object representing the primary category of the
856entry I<$entry>. If a primary category has not been assigned, returns
857C<undef>.
858
859Caches the return value internally so that subsequent calls will not have to
860re-query the database.
861
862=head2 $entry->categories
863
864Returns a reference to an array of I<MT::Category> objects representing the
865categories to which the entry I<$entry> has been assigned (both primary and
866secondary categories). If the entry has not been assigned to any categories,
867returns a reference to an empty array.
868
869Caches the return value internally so that subsequent calls will not have to
870re-query the database.
871
872=head2 $entry->is_in_category($cat)
873
874Returns true if the entry I<$entry> has been assigned to entry I<$cat>, false
875otherwise.
876
877=head2 $entry->comments
878
879Returns a reference to an array of I<MT::Comment> objects representing the
880comments made on the entry I<$entry>. If no comments have been made on the
881entry, returns a reference to an empty array.
882
883Caches the return value internally so that subsequent calls will not have to
884re-query the database.
885
886=head2 $entry->archive_file([ $archive_type ])
887
888Returns the name of/path to the archive file for the entry I<$entry>. If
889I<$archive_type> is not specified, and you are using multiple archive types
890for your blog, the path is created from the preferred archive type that you
891have selected. If I<$archive_type> is specified, it should be one of the
892following values: C<Individual>, C<Daily>, C<Weekly>, C<Monthly>, and
893C<Category>.
894
895=head2 $entry->archive_url([ $archive_type ])
896
897Returns the absolute URL to the archive page for the entry I<$entry>. This
898calls I<archive_file> internally, so if I<$archive_type> is specified, it
899is merely passed through to that method. In other words, this is the
900blog Archive URL plus the results of I<archive_file>.
901
902=head2 $entry->permalink([ $archive_type ])
903
904Returns the (smart) permalink for the entry I<$entry>. Internally this calls
905I<archive_url>, which calls I<archive_file>, so I<$archive_type> (if
906specified) is merely passed through to that method. The result of this
907method is the same as I<archive_url> plus the URI fragment
908(C<#entry_id>), unless the preferred archive type is Individual, in which
909case the two methods give exactly the same results.
910
911=head2 $entry->text_filters
912
913Returns a reference to an array of text filter keynames (the short names
914that are the first argument to I<MT::add_text_filter>. This list can be
915passed directly in as the second argument to I<MT::apply_text_filters>.
916
917=head1 DATA ACCESS METHODS
918
919The I<MT::Entry> object holds the following pieces of data. These fields can
920be accessed and set using the standard data access methods described in the
921I<MT::Object> documentation.
922
923=over 4
924
925=item * id
926
927The numeric ID of the entry.
928
929=item * blog_id
930
931The numeric ID of the blog in which this entry has been posted.
932
933=item * author_id
934
935The numeric ID of the author who posted this entry.
936
937=item * status
938
939The status of the entry, either Publish (C<2>) or Draft (C<1>).
940
941=item * allow_comments
942
943An integer flag specifying whether comments are allowed on this entry. This
944setting determines whether C<E<lt>MTEntryIfAllowCommentsE<gt>> containers are
945displayed for this entry. Possible values are 0 for no comments, 1 for open
946comments and 2 for closed comments (that is, display the comments on this
947entry but do not allow new comments to be added).
948
949=item * convert_breaks
950
951A boolean flag specifying whether line and paragraph breaks should be converted
952when rebuilding this entry.
953
954=item * title
955
956The title of the entry.
957
958=item * excerpt
959
960The excerpt of the entry.
961
962=item * text
963
964The main body text of the entry.
965
966=item * text_more
967
968The extended body text of the entry.
969
970=item * created_on
971
972The timestamp denoting when the entry record was created, in the format
973C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted for the
974selected timezone.
975
976=item * modified_on
977
978The timestamp denoting when the entry record was last modified, in the
979format C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted
980for the selected timezone.
981
982=back
983
984=head1 DATA LOOKUP
985
986In addition to numeric ID lookup, you can look up or sort records by any
987combination of the following fields. See the I<load> documentation in
988I<MT::Object> for more information.
989
990=over 4
991
992=item * blog_id
993
994=item * status
995
996=item * author_id
997
998=item * created_on
999
1000=item * modified_on
1001
1002=back
1003
1004=head1 NOTES
1005
1006=over 4
1007
1008=item *
1009
1010When you remove an entry using I<MT::Entry::remove>, in addition to removing
1011the entry record, all of the comments and placements (I<MT::Comment> and
1012I<MT::Placement> records, respectively) for this entry will also be removed.
1013
1014=back
1015
1016=head1 AUTHOR & COPYRIGHTS
1017
1018Please see the I<MT> manpage for author, copyright, and license information.
1019
1020=cut
Note: See TracBrowser for help on using the browser.