root/branches/release-39/lib/MT/Tag.pm @ 2542

Revision 2542, 17.8 kB (checked in by bchoate, 18 months ago)

Don't fire triggers when loading tags for tag cache.

  • 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        # FIXME: this should be a parameter; breaks MVC model
250        my $limit = MT->config->MaxTagAutoCompletionItems;
251        require MT::ObjectTag;
252        my @tags = map { $_->name } MT::Tag->load(
253            undef,
254            {
255                ( $private ? () : ( 'name' => { not_like => '@%' } ) ),
256                join => MT::ObjectTag->join_on( undef, {
257                    tag_id => \'= tag_id',
258                    ( $blog_id ? ( blog_id => $blog_id ) : () ),
259                    object_datasource => $ds,
260                }, {
261                    unique => 1,
262                    join => $class->join_on( undef, {
263                        'id' => \'= objecttag_object_id',
264                        ( $blog_id ? ( blog_id => $blog_id ) : () ),
265                        ( $class_column ? ( $class_column => $class->class_type ) : () ),
266                    }, {
267                        sort => ($class eq 'MT::Entry' ? 'authored_on' : 'modified_on'),
268                        direction => 'descend'
269                    })
270                }),
271                limit => $limit,
272                fetchonly => [ 'name' ],
273                # prevent any callbacks, as we're only fetching name column anyway
274                no_triggers => 1,
275            }
276        );
277        if (@tags) {
278            $data = \@tags;
279            $tag_cache->set('tag_cache', \@tags);
280            $tag_cache->save;
281        }
282    }
283    $data || [];
284}
285
286# An interface for any MT::Object that wishes to utilize tags themselves
287
288package MT::Taggable;
289
290use constant TAG_CACHE_TIME => 7 * 24 * 60 * 60;  ## 1 week
291
292sub install_properties {
293    my $pkg = shift;
294    my ($class) = @_;
295
296    # synchronize tags if necessary
297    $class->add_trigger( post_save => \&post_save_tags );
298    $class->add_trigger( pre_remove => \&pre_remove_tags );
299}
300
301# post_save trigger for MT::Taggable objects to synchronize tags upon save.
302sub post_save_tags {
303    my $class = shift;
304    my ($obj) = @_;
305    $obj->save_tags;
306}
307
308sub pre_remove_tags {
309    my $class = shift;
310    my ($obj) = @_;
311    $obj->remove_tags if ref $obj;
312}
313
314sub tag_cache_key {
315    my $obj = shift;
316    return undef unless $obj->id;
317    return sprintf "%stags-%d", $obj->datasource, $obj->id;
318}
319
320sub __load_tags {
321    my $obj = shift;
322    my $t = MT->get_timer;
323    $t->pause_partial if $t;
324
325    if (!$obj->id) {
326        $obj->{__tags} = [];
327        return $obj->{__tag_objects} = [];
328    }
329    return if exists $obj->{__tag_objects};
330
331    require MT::Memcached;
332    my $cache = MT::Memcached->instance;
333    my $memkey = $obj->tag_cache_key;
334    my @tags;
335    if (my $tag_ids = $cache->get($memkey)) {
336        @tags = grep { defined } @{ MT::Tag->lookup_multi($tag_ids) };
337    } else {
338        require MT::ObjectTag;
339        @tags = MT::Tag->search(undef, { 
340            sort => 'name', 
341            join => [ 'MT::ObjectTag', 'tag_id', { object_id => $obj->id,
342                object_datasource => $obj->datasource }, { unique => 1 } ],       
343        });
344        $cache->set($memkey, [ map { $_->id } @tags ], TAG_CACHE_TIME);
345    }
346    $obj->{__tags} = [ map { $_->name } @tags ];
347    $t->mark('MT::Tag::__load_tags') if $t;
348    $obj->{__tag_objects} = \@tags;
349}
350
351sub get_tags {
352    my $obj = shift;
353    $obj->__load_tags unless $obj->{__tags} && @{ $obj->{__tags} };
354    return @{ $obj->{__tags} };
355}
356
357sub get_tag_objects {
358    my $obj = shift;
359    $obj->__load_tags;
360    return $obj->{__tag_objects};
361}
362
363sub set_tags {
364    my $obj = shift;
365    $obj->{__tags} = [ sort @_ ];
366    $obj->{__save_tags} = 1;
367}
368
369sub save_tags {
370    my $obj = shift;
371    return 1 unless $obj->{__save_tags};
372    require MT::ObjectTag;
373    my $clear_cache = 0;
374    my @tags = @{ $obj->{__tags} };
375    return 1 unless @tags;
376
377    my $t = MT->get_timer;
378    $t->pause_partial if $t;
379
380    $obj->{__tag_objects} = [];
381    my $blog_id = $obj->has_column('blog_id') ? $obj->blog_id : 0;
382    my @existing_tags = MT::ObjectTag->load({object_id => $obj->id,
383        object_datasource => $obj->datasource });
384    my %existing_tags = map { $_->tag_id => $_ } @existing_tags;
385    foreach my $tag_name (@tags) {
386        my $tag = MT::Tag->load({ name => $tag_name },
387            { binary => { name => 1 } } );
388        if ($tag) {
389            if (exists $existing_tags{$tag->id}) {
390                $existing_tags{$tag->id} = 0;
391                push @{ $obj->{__tag_objects} }, $tag;
392                next;
393            }
394        } else {
395            # new tag
396            $tag = new MT::Tag;
397            $tag->name($tag_name);
398            $tag->save or next;
399            $clear_cache = 1;
400        }
401        my $otag = new MT::ObjectTag;
402        $otag->blog_id($blog_id);
403        $otag->tag_id($tag->id);
404        $otag->object_id($obj->id);
405        $otag->object_datasource($obj->datasource);
406        $otag->save or return $obj->error($otag->errstr);
407        $existing_tags{$tag->id} = 0;
408        $clear_cache = 1;
409
410        push @{ $obj->{__tag_objects} }, $tag;
411    }
412
413    foreach my $otag (values %existing_tags) {
414        next unless ref $otag;
415        my $this_tag_id = $otag->tag_id;
416        $otag->remove;
417        if (! MT::ObjectTag->exist({tag_id => $this_tag_id})) {
418            # no more references to this tag... just delete it now
419            if (my $tag = MT::Tag->load($this_tag_id)) {
420                $tag->remove;
421            }
422        }
423        $clear_cache = 1;
424    }
425    delete $obj->{__save_tags};
426    if ($clear_cache) {
427        MT::Tag->clear_cache(datasource => $obj->datasource, ($blog_id ? (blog_id => $blog_id) : ()));
428
429        require MT::Memcached;
430        MT::Memcached->instance->delete( $obj->tag_cache_key );
431    }
432    $t->mark('MT::Tag::save_tags') if $t;
433    1;
434}
435
436sub tags {
437    my $obj = shift;
438    $obj->set_tags(@_) if @_;
439    $obj->get_tags;
440}
441
442sub add_tags {
443    my $obj = shift;
444    my (@tags) = @_;
445    my @etags = $obj->tags;
446    push @tags, @etags;
447    my %uniq;
448    @uniq{@tags} = ();
449    $obj->set_tags(keys %uniq);
450}
451
452sub remove_tags {
453    my $obj = shift;
454    my (@tags) = @_;
455    if (@tags) {
456        my @etags = $obj->tags;
457        my %uniq;
458        @uniq{@etags} = ();
459        delete $uniq{$_} for @tags;
460        if (keys %uniq) {
461            $obj->set_tags(keys %uniq);
462            return;
463        }
464    }
465    require MT::ObjectTag;
466    my @et = MT::ObjectTag->load({ object_id => $obj->id,
467                                   object_datasource => $obj->datasource });
468    $_->remove for @et;
469    $obj->{__tags} = [];
470    delete $obj->{__save_tags};
471    MT::Tag->clear_cache(datasource => $obj->datasource,
472        ($obj->blog_id ? (blog_id => $obj->blog_id) : ())) if @et;
473
474    require MT::Memcached;
475    MT::Memcached->instance->delete( $obj->tag_cache_key );
476}
477
478sub has_tag {
479    my $obj = shift;
480    my ($tag) = @_;
481    # this should also check normalized versions
482    $tag = $tag->name if ref $tag;
483    my $n8d_tag = MT::Tag->normalize($tag);
484    my @tags = $obj->tags;
485    foreach (@tags) {
486        return 1 if $tag eq $_;
487        return 1 if ($tag ne $n8d_tag) && ($n8d_tag eq MT::Tag->normalize($_));
488    }
489    0;
490}
491
492# counts number of tags
493sub tag_count {
494    my $obj = shift;
495    my ($terms) = @_;
496    my $pkg = ref $obj ? ref $obj : $obj;
497    $terms ||= {};
498    my $jterms = {};
499    if (ref $obj) {
500        $terms->{object_id} = $obj->id if $obj->id;
501        $jterms->{blog_id} = $obj->blog_id if $obj->column('blog_id');
502    }
503    if ($terms->{blog_id}) {
504        $jterms->{blog_id} = $terms->{blog_id};
505        delete $terms->{blog_id};
506    }
507    $jterms->{object_datasource} = $obj->datasource;
508    my $pkg_terms = {};
509    $pkg_terms->{id} = \'=objecttag_object_id';
510    if ( $pkg->class_type eq 'entry' or $pkg->class_type eq 'page' ) {
511        $pkg_terms->{class} = $pkg->class_type;
512    }
513    require MT::ObjectTag;
514    MT::Tag->count(
515        undef,
516        {
517            join => MT::ObjectTag->join_on(
518                'tag_id', $jterms,
519                { unique => 1, join => $pkg->join_on( undef, $pkg_terms ) }
520            )
521        }
522    );
523}
524
525# counts number of objects tagged with a given tag
526sub tagged_count {
527    my $obj = shift;
528    my ($tag_id, $terms) = @_;
529    $terms ||= {};
530    my $jterms = {};
531    my $pkg = ref $obj ? ref $obj : $obj;
532    if (defined $tag_id && ($tag_id =~ m/\D/)) {
533        my $n8d_tag = MT::Tag->normalize($tag_id);
534        my $tag = MT::Tag->load({ name => [ $tag_id, $n8d_tag ] },
535            { binary => { name => 1 } });
536        return 0 unless $tag;
537        $tag_id = $tag->id;
538    }
539    if (ref $obj) {
540        $terms->{object_id} = $obj->id if $obj->id;
541        $jterms->{blog_id} = $obj->blog_id if $obj->column('blog_id');
542    } else {
543        $jterms->{blog_id} = $terms->{blog_id} if $terms->{blog_id};
544    }
545    $jterms->{object_datasource} = $pkg->datasource;
546    $jterms->{tag_id} = $tag_id if $tag_id;
547    my $args = { join => ['MT::ObjectTag', 'object_id', $jterms, { unique => 1 }] };
548    require MT::ObjectTag;
549    $pkg->count($terms, $args);
550}
551
5521;
553__END__
554
555=head1 NAME
556
557MT::Tag - Movable Type tag record and methods
558
559=head1 SYNOPSIS
560
561    use MT::Tag;
562    my $tag = MT::Tag->new;
563    $tag->name('favorite');
564    $tag->save
565        or die $tag->errstr;
566
567=head1 DATA ACCESS METHODS
568
569The I<MT::Tag> object holds the following pieces of data. These fields
570can be accessed and set using the standard data access methods described in
571the L<MT::Object> documentation.
572
573=over 4
574
575=item * id
576
577The numeric ID of the tag.
578
579=item * name
580
581The name of the tag.
582
583=item * n8d_id
584
585The ID of a "normalized" version of this tag. If undef or 0, it would
586signifiy this tag is a normalized tag name.
587
588=back
589
590=head1 DATA LOOKUP
591
592In addition to numeric ID lookup, you can look up or sort records by any
593combination of the following fields. See the I<load> documentation in
594I<MT::Object> for more information.
595
596=over 4
597
598=item * name
599
600=back
601
602=head1 OTHER METHODS
603
604=head2 cache
605
606Return the entry tags. If there are no cached tags, they are loaded
607first. If the tags have already been loaded with this method, that
608data is returned instead.
609
610=head2 cache_obj
611
612Cache the session tags.
613
614=head2 clear_cache
615
616Remove the tag cache.
617
618=head2 join($seperator, @tags)
619
620Return the given I<tags> as a string with the defined I<seperator>.
621
622=head2 split($seperator, $tags)
623
624Split-up the given I<tags> string by the given I<seperator>.
625
626=head2 normalize($tag)
627
628Sanitize the text (remove potentially characters) and lower-case the
629I<tag>. The I<tag> may be given as a string or as a tag object. In the
630case of the latter, C<$tag-E<gt>name> attribute is used.
631
632=head2 load_by_datasource($datasource, $terms, $args)
633
634Return a list of tags given by an object I<datasource> type, selection
635I<terms> and I<arguments>.
636
637=head2 $tag->save()
638
639Save the literal as well as a normalized copy if one does not exist.
640
641=head2 $tag->remove()
642
643Remove the tag and all its children unless it is referenced by another
644entry.
645
646=head1 NOTES
647
648=over 4
649
650=item *
651
652When you remove a tag using I<MT::Tag::remove>, in addition to
653removing the tag record, all of the related I<MT::ObjectTag> records are
654removed as well.
655
656=back
657
658=head1 AUTHOR & COPYRIGHT
659
660Please see L<MT/AUTHOR & COPYRIGHT>.
661
662=cut
Note: See TracBrowser for help on using the browser.