root/branches/release-34/lib/MT/Tag.pm @ 1873

Revision 1873, 17.6 kB (checked in by bchoate, 20 months ago)

Applied patches from Ogawa-san to add an optimized 'exist' method for testing for existing rows. BugId:69661

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