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

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