root/branches/release-40/lib/MT/Tag.pm @ 2632

Revision 2632, 17.7 kB (checked in by fumiakiy, 17 months ago)

Call triggers when loading the name of tags. This is mainly for enterprise drivers to post-process strings correctly. BugId:80172

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