root/branches/release-29/lib/MT/Comment.pm @ 1357

Revision 1357, 12.3 kB (checked in by mpaschal, 22 months ago)

Watch the sum total of visibility changes across the lifetime of a comment instance, not just what happened the last time we called visible()
Forget visibility changes once the comment was saved
BugzID: 66909

  • 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::Comment;
8
9use strict;
10use base qw( MT::Object MT::Scorable );
11
12__PACKAGE__->install_properties({
13    column_defs => {
14        'id' => 'integer not null auto_increment',
15        'blog_id' => 'integer not null',
16        'entry_id' => 'integer not null',
17        'author' => 'string(100)',
18        'commenter_id' => 'integer',
19        'visible' => 'boolean',
20        'junk_status' => 'smallint',
21        'email' => 'string(75)',
22        'url' => 'string(255)',
23        'text' => 'text',
24        'ip' => 'string(16)',
25        'last_moved_on' => 'datetime not null',
26        'junk_score' => 'float',
27        'junk_log' => 'text',
28        'parent_id' => 'integer',
29    },
30    indexes => {
31        ip => 1,
32        created_on => 1,
33        entry_id => 1,
34        blog_id => 1,
35        email => 1,
36        commenter_id => 1,
37        visible => 1,
38        junk_status => 1,
39        last_moved_on => 1,
40        junk_score => 1,
41        parent_id => 1,
42    },
43    defaults => {
44        junk_status => 0,
45        last_moved_on => '20000101000000',
46    },
47    audit => 1,
48    datasource => 'comment',
49    primary_key => 'id',
50});
51
52use constant JUNK => -1;
53use constant NOT_JUNK => 1;
54
55my %blocklists = ();
56
57sub class_label {
58    return MT->translate("Comment");
59}
60
61sub class_label_plural {
62    return MT->translate("Comments");
63}
64
65sub is_junk {
66    $_[0]->junk_status == JUNK;
67}
68
69sub is_not_junk {
70    $_[0]->junk_status != JUNK;
71}
72
73sub is_not_blocked { 
74    my ($eh, $cmt) = @_;
75   
76    my($host, @hosts);
77    # other URI schemes?
78    require MT::Util;
79    @hosts = MT::Util::extract_urls($cmt->text);
80   
81    my $not_blocked = 1;
82    my $blog_id = $cmt->blog_id;
83    if (!$blocklists{$blog_id}) {
84        require MT::Blocklist;
85        my @blocks = MT::Blocklist->load( { blog_id => $blog_id } );
86        $blocklists{$blog_id} = [ @blocks ];
87    }
88    if (@{$blocklists{$blog_id}}) {
89        for my $h (@hosts) {
90            for my $b (@{$blocklists{$blog_id}}) {
91                $not_blocked = 0 if ($h eq $b->text);
92            }
93        }
94    }
95    $not_blocked;
96}
97
98sub next {
99    my $comment = shift;
100    my($publish_only) = @_;
101    $publish_only = $publish_only ? {'visible' => 1} : {};
102    $comment->_nextprev('next', $publish_only);
103}
104
105sub previous {
106    my $comment = shift;
107    my($publish_only) = @_;
108    $publish_only = $publish_only ? {'visible' => 1} : {};
109    $comment->_nextprev('previous', $publish_only);
110}
111
112sub _nextprev {
113    my $obj = shift;
114    my $class = ref($obj);
115    my ($direction, $publish_only) = @_;
116    return undef unless ($direction eq 'next' || $direction eq 'previous');
117    my $next = $direction eq 'next';
118
119    my $label = '__' . $direction;
120    return $obj->{$label} if $obj->{$label};
121
122    # Selecting the adjacent object can be tricky since timestamps
123    # are not necessarily unique for entries. If we find that the
124    # next/previous object has a matching timestamp, keep selecting entries
125    # to select all entries with the same timestamp, then compare them using
126    # id as a secondary sort column.
127
128    my ($id, $ts) = ($obj->id, $obj->created_on);
129    my $iter = $class->load_iter({
130        blog_id => $obj->blog_id,
131        created_on => ($next ? [ $ts, undef ] : [ undef, $ts ]),
132        %{$publish_only}
133    }, {
134        'sort' => 'created_on',
135        'direction' => $next ? 'ascend' : 'descend',
136        'range_incl' => { 'created_on' => 1 },
137    });
138
139    # This selection should always succeed, but handle situation if
140    # it fails by returning undef.
141    return unless $iter;
142
143    # The 'same' array will hold any entries that have matching
144    # timestamps; we will then sort those by id to find the correct
145    # adjacent object.
146    my @same;
147    while (my $e = $iter->()) {
148        # Don't consider the object that is 'current'
149        next if $e->id == $id;
150        my $e_ts = $e->created_on;
151        if ($e_ts eq $ts) {
152            # An object with the same timestamp should only be
153            # considered if the id is in the scope we're looking for
154            # (greater than for the 'next' object; less than for
155            # the 'previous' object).
156            push @same, $e
157                if $next && $e->id > $id or !$next && $e->id < $id;
158        } else {
159            # We found an object with a timestamp different than
160            # the 'current' object.
161            if (!@same) {
162                push @same, $e;
163                # We should check to see if this new timestamped object also
164                # has entries adjacent to _it_ that have the same timestamp.
165                while (my $e = $iter->()) {
166                    push(@same, $e), next if $e->created_on eq $e_ts;
167                    $iter->('finish'), last;
168                }
169            } else {
170                $iter->('finish');
171            }
172            return $obj->{$label} = $e unless @same;
173            last;
174        }
175    }
176    if (@same) {
177        # If we only have 1 element in @same, return that.
178        return $obj->{$label} = $same[0] if @same == 1;
179        # Sort remaining elements in @same by id.
180        @same = sort { $a->id <=> $b->id } @same;
181        # Return front of list (smallest id) if selecting 'next'
182        # object. Return tail of list (largest id) if selection 'previous'.
183        return $obj->{$label} = $same[$next ? 0 : $#same];
184    }
185    return;
186}
187
188sub entry {
189    my ($comment) = @_;
190    my $entry = $comment->{__entry};
191    unless ($entry) {
192        my $entry_id = $comment->entry_id;
193        return undef unless $entry_id;
194        require MT::Entry;
195        $entry = MT::Entry->load($entry_id) or
196            return $comment->error(MT->translate(
197            "Load of entry '[_1]' failed: [_2]", $entry_id, MT::Entry->errstr));
198        $comment->{__entry} = $entry;
199    }
200    return $entry;
201}
202
203sub blog {
204    my ($comment) = @_;
205    my $blog = $comment->{__blog};
206    unless ($blog) {
207        my $blog_id = $comment->blog_id;
208        require MT::Blog;
209        $blog = MT::Blog->load($blog_id) or
210            return $comment->error(MT->translate(
211            "Load of blog '[_1]' failed: [_2]", $blog_id, MT::Blog->errstr));
212        $comment->{__blog} = $blog;
213    }
214    return $blog;
215}
216
217sub junk {
218    my ($comment) = @_;
219    if (($comment->junk_status || 0) != JUNK) {
220        require MT::Util;
221        my @ts = MT::Util::offset_time_list(time, $comment->blog_id);
222        my $ts = sprintf("%04d%02d%02d%02d%02d%02d",
223                         $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0]);
224        $comment->last_moved_on($ts);
225    }
226    $comment->junk_status(JUNK);
227    $comment->visible(0);
228}
229
230sub moderate {
231    my ($comment) = @_;
232    $comment->visible(0);
233}
234
235sub approve {
236    my ($comment) = @_;
237    $comment->visible(1);
238    $comment->junk_status(NOT_JUNK);
239}
240
241*publish = \&approve;
242
243sub all_text {
244    my $this = shift;
245    my $text = $this->column('author') || '';
246    $text .= "\n" . ($this->column('email') || '');
247    $text .= "\n" . ($this->column('url') || '');
248    $text .= "\n" . ($this->column('text') || '');
249    $text;
250}
251
252sub is_published {
253    return $_[0]->visible && !$_[0]->is_junk;
254}
255
256sub is_moderated {
257    return !$_[0]->visible() && !$_[0]->is_junk();
258}
259
260sub log {
261    # TBD: pre-load __junk_log when loading the comment
262    my $comment = shift;
263    push @{$comment->{__junk_log}}, @_;
264}
265
266sub save {
267    my $comment = shift;
268    $comment->junk_log(join "\n", @{$comment->{__junk_log}})
269        if ref $comment->{__junk_log} eq 'ARRAY';
270    my $ret = $comment->SUPER::save();
271    delete $comment->{__changed}{visibility} if $ret;
272    return $ret;
273}
274
275sub to_hash {
276    my $cmt = shift;
277    my $hash = $cmt->SUPER::to_hash();
278
279    $hash->{'comment.created_on_iso'} = sub { MT::Util::ts2iso($cmt->blog, $cmt->created_on) };
280    $hash->{'comment.modified_on_iso'} = sub { MT::Util::ts2iso($cmt->blog, $cmt->modified_on) };
281    if (my $blog = $cmt->blog) {
282        $hash->{'comment.text_html'} = sub {
283            my $txt = defined $cmt->text ? $cmt->text : '';
284            require MT::Util;
285            $txt = MT::Util::munge_comment($txt, $blog);
286            my $convert_breaks = $blog->convert_paras_comments;
287            $txt = $convert_breaks ?
288                MT->apply_text_filters($txt, $blog->comment_text_filters) :
289                $txt;
290            my $sanitize_spec = $blog->sanitize_spec ||
291                MT->config->GlobalSanitizeSpec;
292            require MT::Sanitize;
293            MT::Sanitize->sanitize($txt, $sanitize_spec);
294        }
295    }
296    if (my $entry = $cmt->entry) {
297        my $entry_hash = $entry->to_hash;
298        $hash->{"comment.$_"} = $entry_hash->{$_} foreach keys %$entry_hash;
299    }
300    if ($cmt->commenter_id) {
301        # commenter record exists... populate it
302        require MT::Author;
303        if (my $auth = MT::Author->load($cmt->commenter_id)) {
304            my $auth_hash = $auth->to_hash;
305            $hash->{"comment.$_"} = $auth_hash->{$_} foreach keys %$auth_hash;
306        }
307    }
308
309    $hash;
310}
311
312sub visible {
313    my $comment = shift;
314    return $comment->SUPER::visible unless @_;
315
316    ## Note transitions in visibility in the object, so that
317    ## other methods can act appropriately.
318    my $was_visible = $comment->SUPER::visible || 0;
319    my $is_visible = shift || 0;
320
321    my $vis_delta = 0;
322    if (!$was_visible && $is_visible) {
323        $vis_delta = 1;
324    } elsif ($was_visible && !$is_visible) {
325        $vis_delta = -1;
326    }
327    $comment->{__changed}{visibility} ||= 0;
328    $comment->{__changed}{visibility} += $vis_delta;
329
330    return $comment->SUPER::visible($is_visible);
331}
332
333sub parent {
334    my $comment = shift;
335    $comment->cache_property('parent', sub {
336        if ($comment->parent_id) {
337            return MT::Comment->load($comment->parent_id);
338        }
339    });
340}
341
3421;
343__END__
344
345=head1 NAME
346
347MT::Comment - Movable Type comment record
348
349=head1 SYNOPSIS
350
351    use MT::Comment;
352    my $comment = MT::Comment->new;
353    $comment->blog_id($entry->blog_id);
354    $comment->entry_id($entry->id);
355    $comment->author('Foo');
356    $comment->text('This is a comment.');
357    $comment->save
358        or die $comment->errstr;
359
360=head1 DESCRIPTION
361
362An I<MT::Comment> object represents a comment in the Movable Type system. It
363contains all of the metadata about the comment (author name, email address,
364homepage URL, IP address, etc.), as well as the actual body of the comment.
365
366=head1 USAGE
367
368As a subclass of I<MT::Object>, I<MT::Comment> inherits all of the
369data-management and -storage methods from that class; thus you should look
370at the I<MT::Object> documentation for details about creating a new object,
371loading an existing object, saving an object, etc.
372
373=head1 DATA ACCESS METHODS
374
375The I<MT::Comment> object holds the following pieces of data. These fields can
376be accessed and set using the standard data access methods described in the
377I<MT::Object> documentation.
378
379=over 4
380
381=item * id
382
383The numeric ID of the comment.
384
385=item * blog_id
386
387The numeric ID of the blog in which the comment is found.
388
389=item * entry_id
390
391The numeric ID of the entry on which the comment has been made.
392
393=item * author
394
395The name of the author of the comment.
396
397=item * commenter_id
398
399The author_id for the commenter; this will only be defined if the
400commenter is registered, which is only required if the blog config
401option allow_unreg_comments is false.
402
403=item * ip
404
405The IP address of the author of the comment.
406
407=item * email
408
409The email address of the author of the comment.
410
411=item * url
412
413The URL of the author of the comment.
414
415=item * text
416
417The body of the comment.
418
419=item * visible
420
421Returns a true value if the comment should be displayed. Comments can
422be hidden if comment registration is required and the commenter is
423pending approval.
424
425=item * created_on
426
427The timestamp denoting when the comment record was created, in the format
428C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted for the
429selected timezone.
430
431=item * modified_on
432
433The timestamp denoting when the comment record was last modified, in the
434format C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted
435for the selected timezone.
436
437=back
438
439=head1 DATA LOOKUP
440
441In addition to numeric ID lookup, you can look up or sort records by any
442combination of the following fields. See the I<load> documentation in
443I<MT::Object> for more information.
444
445=over 4
446
447=item * created_on
448
449=item * entry_id
450
451=item * blog_id
452
453=back
454
455=head1 AUTHOR & COPYRIGHTS
456
457Please see the I<MT> manpage for author, copyright, and license information.
458
459=cut
Note: See TracBrowser for help on using the browser.