root/branches/release-27/lib/MT/Tag.pm @ 1208

Revision 1208, 17.6 kB (checked in by bchoate, 23 months ago)

Added require for MT::Session prior to use in clear_cache method. BugId:64083

  • Property svn:keywords set to 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::Tag;
8
9use strict;
10use base qw( MT::Object );
11
12__PACKAGE__->install_properties({
13    column_defs => {
14        'id' => 'integer not null auto_increment primary key',
15        'name' => 'string(255) not null',
16        'n8d_id' => 'integer',
17        'is_private' => 'boolean'
18    },
19    indexes => {
20        name => 1,
21        is_private => 1,
22        n8d_id => 1,
23    },
24    defaults => {
25        n8d_id => 0,
26        is_private => 0,
27    },
28    child_classes => ['MT::ObjectTag'],
29    datasource => 'tag',
30    primary_key => 'id',
31});
32
33sub class_label {
34    return MT->translate('Tag');
35}
36
37sub class_label_plural {
38    return MT->translate('Tags');
39}
40
41sub save {
42    my $tag = shift;
43    my $name = $tag->name;
44    return $tag->error(MT->translate("Tag must have a valid name"))
45        unless defined($name) && length($name);
46    my $n8d = $tag->normalize;
47    return $tag->error(MT->translate("Tag must have a valid name"))
48        unless defined($n8d) && length($n8d);
49    if ($n8d ne $name) {
50        my $n8d_tag = MT::Tag->load({ name => $n8d });
51        if (!$n8d_tag) {
52            $n8d_tag = new MT::Tag;
53            $n8d_tag->name($n8d);
54            $n8d_tag->save;
55        }
56        if (!$tag->n8d_id || ($tag->n8d_id != $n8d_tag->id)) {
57            $tag->n8d_id($n8d_tag->id);
58        }
59    } else {
60        $tag->n8d_id(0) if $tag->n8d_id;
61    }
62    # maintain the private flag...
63    $tag->is_private( $name =~ m/^@/ ? 1 : 0 );
64    $tag->SUPER::save();
65}
66
67sub normalize {
68    my $tag = shift;
69    my ($str) = @_;
70    if (!@_ && !(ref $tag)) {
71        $str = $tag;
72    } elsif (!$str && (ref $tag)) {
73        $str = $tag->name;
74    }
75
76    my $private = $str =~ m/^@/;
77    $str = MT::I18N::encode_text( $str, MT->instance->config->PublishCharset, 'utf-8' );
78    $str =~ s/[@!`\\<>\*&#\/~\?'"\.\,=\(\)\${}\[\];:\ \+\-\r\n]+//gs;
79    $str = lc $str;
80    $str = '@' . $str if $private;
81    $str = MT::I18N::encode_text( $str, 'utf-8', MT->instance->config->PublishCharset );
82    $str;
83}
84
85sub remove {
86    my $tag = shift;
87    my $n8d_tag;
88    if (ref $tag) {
89        if (!$tag->n8d_id) {
90            # normalized tag! we can't delete if others reference us
91            my $child_tags = MT::Tag->count({n8d_id => $tag->id});
92            return $tag->error(MT->translate("This tag is referenced by others."))
93                if $child_tags;
94        } else {
95            $n8d_tag = MT::Tag->load($tag->n8d_id);
96        }
97    }
98    $tag->remove_children({key => 'tag_id'});
99    $tag->SUPER::remove(@_)
100        or return $tag->error($tag->errstr);
101    # check for an orphaned normalized tag and delete if necessary
102    if ($n8d_tag) {
103        # Normalized tag, no longer referenced by other tags...
104        if (!MT::Tag->count({n8d_id => $n8d_tag->id})) {
105            # Noramlized tag that no longer has any object tag associations
106            require MT::ObjectTag;
107            if (!MT::ObjectTag->count({tag_id => $n8d_tag->id})) {
108                $n8d_tag->remove
109                    or return $tag->error($n8d_tag->errstr);
110            }
111        }
112    }
113    1;
114}
115
116sub split {
117    my $pkg = shift;
118    my ($delim, $str) = @_;
119    $delim = quotemeta($delim);
120    my @tags;
121    $str =~ s/(^\s+|\s+$)//gs;
122    while (length($str) && ($str =~ m/^(((['"])(.*?)\3[^$delim]*?|.*?)($delim\s*|$))/s)) {
123        $str = substr($str, length($1));
124        my $tag = defined $4 ? $4 : $2;
125        #$tag =~ s/(^[\s,]+|[\s,]+$)//gs;
126        $tag =~ s/(^\s+|\s+$)//gs;
127        $tag =~ s/\s+/ /gs;
128        my $n8d_tag = MT::Tag->normalize($tag);
129        next if $n8d_tag eq '';
130        push @tags, $tag if $tag ne '';
131    }
132    @tags;
133}
134
135sub join {
136    my $obj = shift;
137    my ($delim, @tags) = @_;
138    my $tags = '';
139    foreach (@tags) {
140        $tags .= $delim . ($delim eq ' ' ? '' : ' ') if $tags ne '';
141        if (m/\Q$delim\E/) {
142            if (m/"/) {
143                $tags .= "'" . $_ . "'";
144            } else {
145                $tags .= '"' . $_ . '"';
146            }
147        } else {
148            $tags .= $_;
149        }
150    }
151    $tags;
152}
153
154sub load_by_datasource {
155    my $pkg = shift;
156    my ($datasource, $terms, $args) = @_;
157    $args ||= {};
158    $args->{'sort'} ||= 'name';
159    my $blog_id;
160    my %jargs;
161    if ($terms->{blog_id}) {
162        $blog_id = $terms->{blog_id};
163        delete $terms->{blog_id};
164        if ($args->{not} && $args->{not}{blog_id}) {
165            $jargs{not}{blog_id} = 1;
166        }
167    }
168    $args->{'join'} ||= MT::ObjectTag->join_on('tag_id', {
169        $blog_id ? (blog_id => $blog_id) : (),
170        object_datasource => $datasource
171    }, { unique => 1, %jargs });
172    my @tags = MT::Tag->load($terms, $args);
173    @tags;
174}
175
176# static method for tag cache control
177sub cache_obj {
178    my $pkg = shift;
179    my (%param) = @_;
180    my $blog_id = $param{blog_id};
181    my $ds = $param{datasource};
182
183    require MT::Session;
184    # Clear any tag cache if tags were modified upon saving
185    my $sess_id = ($blog_id ? 'blog:' . $blog_id . ';' : '') . 'datasource:' . $ds . ($param{private} ? ';private' : '');
186    my $tag_cache = MT::Session::get_unexpired_value(60 * 60, {
187        kind => 'TC',  # tag cache
188        id => $sess_id
189    });
190    if (!$tag_cache) {
191        $tag_cache = new MT::Session;
192        $tag_cache->kind('TC');
193        $tag_cache->id($sess_id);
194        $tag_cache->start(time);
195    }
196    $tag_cache;
197}
198
199sub clear_cache {
200    my $pkg = shift;
201    my (%param) = @_;
202    my $blog_id = $param{blog_id};
203    my $ds = $param{datasource};
204
205    my $tag_cache;
206    my $sess_id = ($blog_id ? 'blog:' . $blog_id . ';' : '') . 'datasource:' . $ds;
207    require MT::Session;
208    $tag_cache = MT::Session->load({
209        kind => 'TC',
210        id => $sess_id});
211    $tag_cache->remove if $tag_cache;
212
213    $sess_id = ($blog_id ? 'blog:' . $blog_id . ';' : '') . 'datasource:' . $ds . ';private';
214    $tag_cache = MT::Session->load({
215        kind => 'TC',
216        id => $sess_id});
217    $tag_cache->remove if $tag_cache;
218}
219
220sub cache {
221    my $pkg = shift;
222    my (%param) = @_;
223    my $blog_id = $param{blog_id};
224    my $class = $param{class};
225    if (ref($class) eq 'SCALAR') {
226        $class = eval "use $class;";
227        if (my $err = $@) {
228            $class = eval 'use MT::Entry;';
229        }
230    }
231    my $ds = $class->datasource;
232    $param{datasource} = $ds;
233
234    my $tag_cache = $pkg->cache_obj(%param);
235    my $data = $tag_cache->get('tag_cache');
236    if (!$data) {
237        my $tag_count;
238        require MT::ObjectTag;
239        $tag_count = {};
240        my $tag_count_iter =
241            MT::ObjectTag->count_group_by({
242                ($blog_id ? (blog_id => $blog_id) : ()),
243                object_datasource => $ds
244            }, { group => ['tag_id']});
245        while (my ($count, $tag_id) = $tag_count_iter->()) {
246            $tag_count->{$tag_id} = $count;
247        }
248        my $iter = $class->load_iter(undef,
249            { 'join' => ['MT::ObjectTag', 'object_id',
250            { ($blog_id ? (blog_id => $blog_id) : ()), object_datasource => $ds },
251            { unique => 1 } ],
252              sort => 'modified_on', direction => 'descend' 
253        });
254        my @tags;
255        my %tags_seen;
256        my $limit = MT->config->MaxTagAutoCompletionItems;
257        while (my $entry = $iter->()) {
258            my @etags = $entry->tags;
259            @etags = grep /^[^@]/, @etags unless $param{private};
260            @etags = grep { !exists($tags_seen{$_}) } @etags;
261            next if 0 == scalar(@etags);
262
263            $tags_seen{$_} = 1 for @etags;
264
265            my @ttags = MT::Tag->load({ 'name' => \@etags },
266                { 'join' => ['MT::ObjectTag', 'tag_id',
267                { ($blog_id ? (blog_id => $blog_id) : ()), object_datasource => $ds },
268                { unique => 1 } ]
269            });
270            if (scalar(@tags) + scalar(@ttags) <= $limit) {
271                push @tags, @ttags;
272            } else {
273                for (0..($limit - scalar(@tags)) - 1) {
274                    push @tags, $ttags[$_];
275                }
276            }
277            last if ($limit <= scalar(@tags));
278        }
279        $data = {};
280        if (@tags) {
281            foreach my $tag (@tags) {
282                $data->{$tag->name} = $tag_count->{$tag->id} || 1;
283            }
284            $tag_cache->set('tag_cache', $data);
285            $tag_cache->save;
286        }
287    }
288    $data || {};
289}
290
291# An interface for any MT::Object that wishes to utilize tags themselves
292
293package MT::Taggable;
294
295use constant TAG_CACHE_TIME => 7 * 24 * 60 * 60;  ## 1 week
296
297sub install_properties {
298    my $pkg = shift;
299    my ($class) = @_;
300
301    # synchronize tags if necessary
302    $class->add_trigger( post_save => \&post_save_tags );
303    $class->add_trigger( pre_remove => \&pre_remove_tags );
304}
305
306# post_save trigger for MT::Taggable objects to synchronize tags upon save.
307sub post_save_tags {
308    my $class = shift;
309    my ($obj) = @_;
310    $obj->save_tags;
311}
312
313sub pre_remove_tags {
314    my $class = shift;
315    my ($obj) = @_;
316    $obj->remove_tags if ref $obj;
317}
318
319sub tag_cache_key {
320    my $obj = shift;
321    return undef unless $obj->id;
322    return sprintf "%stags-%d", $obj->datasource, $obj->id;
323}
324
325sub __load_tags {
326    my $obj = shift;
327    if (!$obj->id) {
328        $obj->{__tags} = [];
329        return $obj->{__tag_objects} = [];
330    }
331    return if exists $obj->{__tag_objects};
332
333    require MT::Memcached;
334    my $cache = MT::Memcached->instance;
335    my $memkey = $obj->tag_cache_key;
336    my @tags;
337    if (my $tag_ids = $cache->get($memkey)) {
338        @tags = grep { defined } @{ MT::Tag->lookup_multi($tag_ids) };
339    } else {
340        require MT::ObjectTag;
341        @tags = MT::Tag->search(undef, { 
342            sort => 'name', 
343            join => [ 'MT::ObjectTag', 'tag_id', { object_id => $obj->id,
344                object_datasource => $obj->datasource }, { unique => 1 } ],       
345        });
346        $cache->set($memkey, [ map { $_->id } @tags ], TAG_CACHE_TIME);
347    }
348    $obj->{__tags} = [ map { $_->name } @tags ];
349    $obj->{__tag_objects} = \@tags;
350}
351
352sub get_tags {
353    my $obj = shift;
354    $obj->__load_tags unless $obj->{__tags} && @{ $obj->{__tags} };
355    return @{ $obj->{__tags} };
356}
357
358sub get_tag_objects {
359    my $obj = shift;
360    $obj->__load_tags;
361    return $obj->{__tag_objects};
362}
363
364sub set_tags {
365    my $obj = shift;
366    $obj->{__tags} = [ sort @_ ];
367    $obj->{__save_tags} = 1;
368}
369
370sub save_tags {
371    my $obj = shift;
372    return 1 unless $obj->{__save_tags};
373    require MT::ObjectTag;
374    my $clear_cache = 0;
375    my @tags = @{ $obj->{__tags} };
376    return 1 unless @tags;
377    $obj->{__tag_objects} = [];
378    my $blog_id = $obj->has_column('blog_id') ? $obj->blog_id : 0;
379    my @existing_tags = MT::ObjectTag->load({object_id => $obj->id,
380        object_datasource => $obj->datasource });
381    my %existing_tags = map { $_->tag_id => $_ } @existing_tags;
382    foreach my $tag_name (@tags) {
383        my $tag = MT::Tag->load({ name => $tag_name },
384            { binary => { name => 1 } } );
385        if ($tag) {
386            if (exists $existing_tags{$tag->id}) {
387                $existing_tags{$tag->id} = 0;
388                push @{ $obj->{__tag_objects} }, $tag;
389                next;
390            }
391        } else {
392            # new tag
393            $tag = new MT::Tag;
394            $tag->name($tag_name);
395            $tag->save or next;
396            $clear_cache = 1;
397        }
398        my $otag = new MT::ObjectTag;
399        $otag->blog_id($blog_id);
400        $otag->tag_id($tag->id);
401        $otag->object_id($obj->id);
402        $otag->object_datasource($obj->datasource);
403        $otag->save or return $obj->error($otag->errstr);
404        $existing_tags{$tag->id} = 0;
405        $clear_cache = 1;
406
407        push @{ $obj->{__tag_objects} }, $tag;
408    }
409
410    foreach my $otag (values %existing_tags) {
411        next unless ref $otag;
412        my $this_tag_id = $otag->tag_id;
413        $otag->remove;
414        if (! MT::ObjectTag->count({tag_id => $this_tag_id})) {
415            # no more references to this tag... just delete it now
416            if (my $tag = MT::Tag->load($this_tag_id)) {
417                $tag->remove;
418            }
419        }
420        $clear_cache = 1;
421    }
422    delete $obj->{__save_tags};
423    if ($clear_cache) {
424        MT::Tag->clear_cache(datasource => $obj->datasource, ($blog_id ? (blog_id => $blog_id) : ()));
425
426        require MT::Memcached;
427        MT::Memcached->instance->delete( $obj->tag_cache_key );
428    }
429    1;
430}
431
432sub tags {
433    my $obj = shift;
434    $obj->set_tags(@_) if @_;
435    $obj->get_tags;
436}
437
438sub add_tags {
439    my $obj = shift;
440    my (@tags) = @_;
441    my @etags = $obj->tags;
442    push @tags, @etags;
443    my %uniq;
444    @uniq{@tags} = ();
445    $obj->set_tags(keys %uniq);
446}
447
448sub remove_tags {
449    my $obj = shift;
450    my (@tags) = @_;
451    if (@tags) {
452        my @etags = $obj->tags;
453        my %uniq;
454        @uniq{@etags} = ();
455        delete $uniq{$_} for @tags;
456        if (keys %uniq) {
457            $obj->set_tags(keys %uniq);
458            return;
459        }
460    }
461    require MT::ObjectTag;
462    my @et = MT::ObjectTag->load({ object_id => $obj->id,
463                                   object_datasource => $obj->datasource });
464    $_->remove for @et;
465    $obj->{__tags} = [];
466    delete $obj->{__save_tags};
467    MT::Tag->clear_cache(datasource => $obj->datasource,
468        ($obj->blog_id ? (blog_id => $obj->blog_id) : ())) if @et;
469
470    require MT::Memcached;
471    MT::Memcached->instance->delete( $obj->tag_cache_key );
472}
473
474sub has_tag {
475    my $obj = shift;
476    my ($tag) = @_;
477    # this should also check normalized versions
478    $tag = $tag->name if ref $tag;
479    my $n8d_tag = MT::Tag->normalize($tag);
480    my @tags = $obj->tags;
481    foreach (@tags) {
482        return 1 if $tag eq $_;
483        return 1 if ($tag ne $n8d_tag) && ($n8d_tag eq MT::Tag->normalize($_));
484    }
485    0;
486}
487
488# counts number of tags
489sub tag_count {
490    my $obj = shift;
491    my ($terms) = @_;
492    my $pkg = ref $obj ? ref $obj : $obj;
493    $terms ||= {};
494    my $jterms = {};
495    if (ref $obj) {
496        $terms->{object_id} = $obj->id if $obj->id;
497        $jterms->{blog_id} = $obj->blog_id if $obj->column('blog_id');
498    }
499    if ($terms->{blog_id}) {
500        $jterms->{blog_id} = $terms->{blog_id};
501        delete $terms->{blog_id};
502    }
503    $jterms->{object_datasource} = $obj->datasource;
504    my $pkg_terms = {};
505    $pkg_terms->{id} = \'=objecttag_object_id';
506    if ( $pkg->class_type eq 'entry' or $pkg->class_type eq 'page' ) {
507        $pkg_terms->{class} = $pkg->class_type;
508    }
509    require MT::ObjectTag;
510    MT::Tag->count(
511        undef,
512        {
513            join => MT::ObjectTag->join_on(
514                'tag_id', $jterms,
515                { unique => 1, join => $pkg->join_on( undef, $pkg_terms ) }
516            )
517        }
518    );
519}
520
521# counts number of objects tagged with a given tag
522sub tagged_count {
523    my $obj = shift;
524    my ($tag_id, $terms) = @_;
525    $terms ||= {};
526    my $jterms = {};
527    my $pkg = ref $obj ? ref $obj : $obj;
528    if (defined $tag_id && ($tag_id =~ m/\D/)) {
529        my $n8d_tag = MT::Tag->normalize($tag_id);
530        my $tag = MT::Tag->load({ name => [ $tag_id, $n8d_tag ] },
531            { binary => { name => 1 } });
532        return 0 unless $tag;
533        $tag_id = $tag->id;
534    }
535    if (ref $obj) {
536        $terms->{object_id} = $obj->id if $obj->id;
537        $jterms->{blog_id} = $obj->blog_id if $obj->column('blog_id');
538    } else {
539        $jterms->{blog_id} = $terms->{blog_id} if $terms->{blog_id};
540    }
541    $jterms->{object_datasource} = $pkg->datasource;
542    $jterms->{tag_id} = $tag_id if $tag_id;
543    my $args = { join => ['MT::ObjectTag', 'object_id', $jterms, { unique => 1 }] };
544    require MT::ObjectTag;
545    $pkg->count($terms, $args);
546}
547
5481;
549__END__
550
551=head1 NAME
552
553MT::Tag - Movable Type tag record and methods
554
555=head1 SYNOPSIS
556
557    use MT::Tag;
558    my $tag = MT::Tag->new;
559    $tag->name('favorite');
560    $tag->save
561        or die $tag->errstr;
562
563=head1 DATA ACCESS METHODS
564
565The I<MT::Tag> object holds the following pieces of data. These fields
566can be accessed and set using the standard data access methods described in
567the L<MT::Object> documentation.
568
569=over 4
570
571=item * id
572
573The numeric ID of the tag.
574
575=item * name
576
577The name of the tag.
578
579=item * n8d_id
580
581The ID of a "normalized" version of this tag. If undef or 0, it would
582signifiy this tag is a normalized tag name.
583
584=back
585
586=head1 DATA LOOKUP
587
588In addition to numeric ID lookup, you can look up or sort records by any
589combination of the following fields. See the I<load> documentation in
590I<MT::Object> for more information.
591
592=over 4
593
594=item * name
595
596=back
597
598=head1 OTHER METHODS
599
600=head2 cache
601
602Return the entry tags. If there are no cached tags, they are loaded
603first. If the tags have already been loaded with this method, that
604data is returned instead.
605
606=head2 cache_obj
607
608Cache the session tags.
609
610=head2 clear_cache
611
612Remove the tag cache.
613
614=head2 join($seperator, @tags)
615
616Return the given I<tags> as a string with the defined I<seperator>.
617
618=head2 split($seperator, $tags)
619
620Split-up the given I<tags> string by the given I<seperator>.
621
622=head2 normalize($tag)
623
624Sanitize the text (remove potentially characters) and lower-case the
625I<tag>. The I<tag> may be given as a string or as a tag object. In the
626case of the latter, C<$tag-E<gt>name> attribute is used.
627
628=head2 load_by_datasource($datasource, $terms, $args)
629
630Return a list of tags given by an object I<datasource> type, selection
631I<terms> and I<arguments>.
632
633=head2 $tag->save()
634
635Save the literal as well as a normalized copy if one does not exist.
636
637=head2 $tag->remove()
638
639Remove the tag and all its children unless it is referenced by another
640entry.
641
642=head1 NOTES
643
644=over 4
645
646=item *
647
648When you remove a tag using I<MT::Tag::remove>, in addition to
649removing the tag record, all of the related I<MT::ObjectTag> records are
650removed as well.
651
652=back
653
654=head1 AUTHOR & COPYRIGHT
655
656Please see L<MT/AUTHOR & COPYRIGHT>.
657
658=cut
Note: See TracBrowser for help on using the browser.