root/tagwire/tags/0.22/tagwire.pl

Revision 100, 16.0 kB (checked in by ogawa, 3 years ago)

A fix for the case when no tags are provided to MT-XSearch.
A fix for adapting to MT3.2 plugin interface.
And a little modification of the document.

  • Property svn:keywords set to Id Author Date Rev
Line 
1# Tagwire Plugin (aka AllKeywords Plugin)
2# a plugin for listing and handling "tags"
3#
4# $Id$
5#
6# This software is provided as-is. You may use it for commercial or
7# personal use. If you distribute it, please keep this notice intact.
8#
9# Copyright (c) 2005 Hirotaka Ogawa
10
11package MT::Plugin::Tagwire;
12use strict;
13use MT::Template::Context;
14use MT::Request;
15
16# DEBUG
17my $FORCE_PD_REFRESH = 0;
18my $ENABLE_PD_INDEXES = 1;
19my $ENABLE_REQ_CACHE = 1;
20
21my $plugin;
22eval {
23    require MT::Plugin;
24    $plugin = new MT::Plugin({
25        name => 'Tagwire Plugin',
26        description => 'A plugin for listing and handling blog-wide tags and entry tags.',
27        doc_link => 'http://as-is.net/hacks/2005/06/tagwire_plugin.html',
28        author_name => 'Hirotaka Ogawa',
29        author_link => 'http://profile.typekey.com/ogawa/',
30        version => '0.22'
31        });
32    MT->add_plugin($plugin);
33};
34
35if (MT->can('add_callback')) {
36    my $mt = MT->instance;
37    MT->add_callback((ref $mt eq 'MT::App::CMS' ? 'AppPostEntrySave' : 'MT::Entry::post_save'),
38                     10, $plugin, \&update_pd_indexes);
39}
40
41sub update_pd_indexes {
42    return unless $ENABLE_PD_INDEXES && $plugin;
43    my ($eh, $app, $entry) = @_;
44    require MT::Entry;
45    my $blog_id = $entry->blog_id;
46    require MT::PluginData;
47    my $pd = MT::PluginData->load({ plugin => $plugin->name,
48                                    key => $blog_id });
49    my (%eindex, %tindex);
50    my $data;
51    if (!$pd || $FORCE_PD_REFRESH) {
52        $pd = new MT::PluginData();
53        $pd->plugin($plugin->name);
54        $pd->key($blog_id);
55        $data = $pd->data() || {};
56        my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
57                                          status => MT::Entry::RELEASE() });
58        while (my $e = $iter->()) {
59            my @tags = split_tags($e->keywords, 1) or next;
60            $eindex{$e->id} = { tags => \@tags,
61                                created_on => $e->created_on };
62        }
63    } else {
64        $data = $pd->data() || {};
65        my $entry_id = $entry->id;
66        %eindex = %{$data->{eindex}};
67        delete $eindex{$entry_id} if exists $eindex{$entry_id};
68        if ($entry->status == MT::Entry::RELEASE()) {
69            my @tags = split_tags($entry->keywords, 1);
70            $eindex{$entry_id} = { tags => \@tags,
71                                   created_on => $entry->created_on };
72        }
73    }
74    foreach my $eid (keys %eindex) {
75        map { push @{$tindex{$_}}, $eid } @{$eindex{$eid}->{tags}};
76    }
77    $data->{eindex} = \%eindex;
78    $data->{tindex} = \%tindex;
79    $pd->data($data);
80    $pd->save or die $pd->errstr;
81    if ($ENABLE_REQ_CACHE) {
82        my $r = MT::Request->instance;
83        my $cname = 'Tagwire::Cache::' . $blog_id;
84        $r->cache($cname, undef);
85    }
86}
87
88MT::Template::Context->add_container_tag('Tags' => \&tags);
89MT::Template::Context->add_container_tag('EntryTags' => \&entry_tags);
90MT::Template::Context->add_tag('Tag' => \&tag);
91MT::Template::Context->add_tag('TagCount' => \&tag_count);
92MT::Template::Context->add_tag('TagsTotal' => \&tags_total);
93MT::Template::Context->add_tag('TagsTotalSum' => \&tags_total_sum);
94MT::Template::Context->add_container_tag('EntriesWithTags' => \&entries);
95MT::Template::Context->add_container_tag('MostRelatedEntries' => \&most_related_entries);
96MT::Template::Context->add_global_filter('encode_urlplus' => \&encode_urlplus);
97
98# For compatibility (this plugin was formerly named 'AllKeywords')
99MT::Template::Context->add_container_tag('AllKeywords' => \&tags);
100MT::Template::Context->add_container_tag('EntryAllKeywords' => \&entry_tags);
101MT::Template::Context->add_tag('AllKeyword' => \&tag);
102MT::Template::Context->add_tag('AllKeywordCount' => \&tag_count);
103MT::Template::Context->add_tag('AllKeywordsTotal' => \&tags_total);
104MT::Template::Context->add_tag('AllKeywordsTotalSum' => \&tags_total_sum);
105MT::Template::Context->add_container_tag('EntriesWithKeywords' => \&entries);
106
107sub split_args {
108    my ($string, $delimiter, $case_sensitive) = @_;
109    return unless $string;
110    my @tags;
111    $string =~ s/(^\s+|\s+$)//g;
112    $string = lc $string unless $case_sensitive;
113
114    return split(/\s+/, $string) unless $delimiter;
115
116    foreach my $tag (split($delimiter, $string)) {
117        $tag =~ s/(^\s+|\s+$)//g;
118        push @tags, $tag if $tag;
119    }
120    @tags;
121}
122
123sub split_tags {
124    my ($string, $case_sensitive) = @_;
125    return unless $string;
126    my @tags;
127    $string =~ s/(^\s+|\s+$)//g;
128    $string = lc $string unless $case_sensitive;
129#    $string =~ s/\[[^[]+\]//g; # uncomment this to discard [short title]
130
131    if ($string =~ m/[;,|]/) {
132        # tags separated by non-whitespaces
133        while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^;,|]+)/g) {
134            my $tag = $1;
135            $tag =~ s/(^[\["'\s;,|]+|[\]"'\s;,|]+$)//g;
136            push @tags, $tag if $tag;
137        }
138    } else {
139        # tags separated by whitespaces
140        while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^\s]+)/g) {
141            my $tag = $1;
142            $tag =~ s/(^[\["'\s]+|[\]"'\s]+$)//g;
143            push @tags, $tag if $tag;
144        }
145    }
146    @tags;
147}
148
149sub get_pd_indexes {
150    return unless $ENABLE_PD_INDEXES && $plugin;
151    my $blog_id = $_[0] or return;
152    my ($r, $cname);
153    if ($ENABLE_REQ_CACHE) {
154        $r = MT::Request->instance;
155        $cname = 'Tagwire::Cache::' . $blog_id;
156        return $r->cache($cname) if defined $r->cache($cname);
157        $r->cache($cname, undef);
158    }
159    my $data;
160    eval {
161        require MT::PluginData;
162        my $pd = MT::PluginData->load({ plugin => $plugin->name,
163                                        key => $blog_id });
164        $data = $pd->data() if $pd;
165    };
166    $r->cache($cname, $data) if $ENABLE_REQ_CACHE && $data;
167    $data;
168}
169
170sub get_db_indexes {
171    my $blog_id = $_[0] or return;
172    my ($r, $cname);
173    if ($ENABLE_REQ_CACHE) {
174        $r = MT::Request->instance;
175        $cname = 'Tagwire::Cache::' . $blog_id;
176        return $r->cache($cname) if defined $r->cache($cname);
177        $r->cache($cname, undef);
178    }
179    my $data;
180    my (%eindex, %tindex);
181    my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
182                                      status => MT::Entry::RELEASE() });
183    while (my $e = $iter->()) {
184        my @tags = split_tags($e->keywords, 1) or next;
185        $eindex{$e->id} = { tags => \@tags,
186                            created_on => $e->created_on };
187    }
188    foreach my $eid (keys %eindex) {
189        map { push @{$tindex{$_}}, $eid } @{$eindex{$eid}->{tags}};
190    }
191    $data->{eindex} = \%eindex;
192    $data->{tindex} = \%tindex;
193    $r->cache($cname, $data) if $ENABLE_REQ_CACHE;
194    $data;
195}
196
197sub tags {
198    my ($ctx, $args, $cond) = @_;
199
200    # sort_by (tag/tag-case/count, default = tag)
201    my $sort_by = $args->{sort_by} || 'tag';
202    # sort_order (ascend/descend, default = ascend)
203    my $sort_order = $args->{sort_order} || 'ascend';
204    # lastn (default = 0, no cutoff)
205    my $lastn = $args->{lastn} || 0;
206    # case_sensitive (0/1, default = 1)
207    my $case_sensitive = defined $args->{case_sensitive} ?
208        $args->{case_sensitive} : 1;
209
210    my $blog_id = $ctx->stash('blog_id');
211    my %tags = ();
212
213    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
214        or return '';
215    my %tindex = %{$data->{tindex}};
216    if ($case_sensitive) {
217        map { $tags{$_} = scalar(@{$tindex{$_}}) } keys %tindex;
218    } else {
219        map { $tags{lc $_} += scalar(@{$tindex{$_}}) } keys %tindex;
220    }
221
222    my @list;
223    if ($sort_by eq 'tag' || $sort_by eq 'keyword' ) {
224        @list = $sort_order eq 'ascend' ?
225            sort { lc $a cmp lc $b } keys %tags :
226            sort { lc $b cmp lc $a } keys %tags;
227    } elsif ($sort_by eq 'tag-case' || $sort_by eq 'keyword-case') {
228        @list = $sort_order eq 'ascend' ?
229            sort keys %tags :
230            sort reverse keys %tags;
231    } else {
232        @list = $sort_order eq 'ascend' ?
233            sort { $tags{$a} <=> $tags{$b} } keys %tags :
234            sort { $tags{$b} <=> $tags{$a} } keys %tags;
235    }
236
237    $ctx->stash('Tagwire::tags_total', scalar @list);
238
239    my $total_sum = 0;
240    foreach (@list) {
241        $total_sum += $tags{$_};
242    }
243    $ctx->stash('Tagwire::tags_total_sum', $total_sum);
244
245    my @res;
246    my $builder = $ctx->stash('builder');
247    my $tokens = $ctx->stash('tokens');
248    my $i = 0;
249    foreach (@list) {
250        last if $lastn && $i >= $lastn;
251        $ctx->stash('Tagwire::tag', $case_sensitive ? $_ : ucfirst $_);
252        $ctx->stash('Tagwire::tag_count', $tags{$_});
253        defined(my $out = $builder->build($ctx, $tokens))
254            or return $ctx->error($ctx->errstr);
255        push @res, $out;
256        $i++;
257    }
258    my $glue = $args->{glue} || '';
259    join $glue, @res;
260}
261
262sub entry_tags {
263    my ($ctx, $args, $cond) = @_;
264    my $e = $ctx->stash('entry')
265        or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
266    return '' unless $e->keywords;
267
268    # case_sensitive (0/1, default = 1)
269    my $case_sensitive = defined $args->{case_sensitive} ?
270        $args->{case_sensitive} : 1;
271
272    my @tags = split_tags($e->keywords, $case_sensitive);
273    my $total = scalar(@tags);
274    $ctx->stash('Tagwire::tags_total', $total);
275    $ctx->stash('Tagwire::tags_total_sum', $total);
276
277    my @res;
278    my $builder = $ctx->stash('builder');
279    my $tokens = $ctx->stash('tokens');
280    foreach (@tags) {
281        $ctx->stash('Tagwire::tag', $case_sensitive ? $_ : ucfirst $_);
282        $ctx->stash('Tagwire::tag_count', 1);
283        defined(my $out = $builder->build($ctx, $tokens))
284            or return $ctx->error($ctx->errstr);
285        push @res, $out;
286    }
287    my $glue = $args->{glue} || '';
288    join $glue, @res;
289}
290
291sub tag {
292    $_[0]->stash('Tagwire::tag');
293}
294
295sub tag_count {
296    $_[0]->stash('Tagwire::tag_count');
297}
298
299sub tags_total {
300    $_[0]->stash('Tagwire::tags_total');
301}
302
303sub tags_total_sum {
304    $_[0]->stash('Tagwire::tags_total_sum');
305}
306
307sub entries {
308    my ($ctx, $args, $cond) = @_;
309
310    # tags(keywords) (REQUIRED)
311    my $search = $args->{tags} || $args->{keywords} or return '';
312    # delimiter for "tags" argument (default = space characters)
313    my $delimiter = $args->{delimiter} || '';
314    # case_sensitive (0/1, default = 1)
315    my $case_sensitive = defined $args->{case_sensitive} ?
316        $args->{case_sensitive} : 1;
317    # sort_by (created_on, default = created_on)
318    my $sort_by = $args->{sort_by} || 'created_on';
319    # sort_order (ascend/descend, default = descend)
320    my $sort_order = $args->{sort_order} || 'descend';
321    # lastn (default = 0, no cutoff)
322    my $lastn = $args->{lastn} || 0;
323
324    my @patterns = split_args($search, $delimiter, $case_sensitive)
325        or return '';
326
327    my $blog_id = $ctx->stash('blog_id');
328    my @entries;
329
330    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
331        or return '';
332    my %tindex = %{$data->{tindex}};
333    my %eindex = %{$data->{eindex}};
334    my %match;
335    if ($case_sensitive) {
336        foreach my $tag (@patterns) {
337            foreach my $eid (@{$tindex{$tag}}) {
338                $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
339            }
340        }
341    } else {
342        foreach my $tag (@patterns) {
343            foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
344                foreach my $eid (@{$tindex{$mtag}}) {
345                    $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
346                }
347            }
348        }
349    }
350    my $count = scalar @patterns;
351    my @eids = grep { $match{$_} == $count } keys %match or return;
352    @eids = sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids;
353    @eids = $sort_order eq 'descend' ?
354        sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
355        sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
356    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);
357    require MT::Entry;
358    map { push @entries, MT::Entry->load($_) } @eids;
359
360    my $res = '';
361    my $tokens = $ctx->stash('tokens');
362    my $builder = $ctx->stash('builder');
363    my $i = 0;
364    for my $e (@entries) {
365        local $ctx->{__stash}{entry} = $e;
366        local $ctx->{current_timestamp} = $e->created_on;
367        local $ctx->{modification_timestamp} = $e->modified_on;
368        my $out = $builder->build($ctx, $tokens, {
369            %$cond,
370            EntryIfExtended => $e->text_more ? 1 : 0,
371            EntryIfAllowComments => $e->allow_comments,
372            EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
373            EntryIfAllowPings => $e->allow_pings,
374            EntriesHeader => !$i,
375            EntriesFooter => !defined $entries[$i+1]
376            });
377        return $ctx->error($ctx->errstr) unless defined $out;
378        $res .= $out;
379        $i++;
380    }
381    $res;
382}
383
384sub most_related_entries {
385    my ($ctx, $args, $cond) = @_;
386    my $entry = $ctx->stash('entry')
387        or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
388    return '' unless $entry->keywords;
389
390    # case_sensitive (0/1, default = 1)
391    my $case_sensitive = defined $args->{case_sensitive} ?
392        $args->{case_sensitive} : 1;
393    # sort_order (ascend/descend, default = descend)
394    my $sort_order = $args->{sort_order} || 'descend';
395    # lastn (default = 0, no cutoff)
396    my $lastn = $args->{lastn} || 0;
397
398    my @patterns = split_tags($entry->keywords, $case_sensitive)
399        or return '';
400
401    my $blog_id = $ctx->stash('blog_id');
402    my @entries;
403
404    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
405        or return '';
406    my %tindex = %{$data->{tindex}};
407    my %eindex = %{$data->{eindex}};
408    my %match;
409    if ($case_sensitive) {
410        foreach my $tag (@patterns) {
411            foreach my $eid (@{$tindex{$tag}}) {
412                next if $eid == $entry->id;
413                $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
414            }
415        }
416    } else {
417        foreach my $tag (@patterns) {
418            foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
419                foreach my $eid (@{$tindex{$mtag}}) {
420                    next if $eid == $entry->id;
421                    $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
422                }
423            }
424        }
425    }
426    my @eids = keys %match or return '';
427    @eids = $sort_order eq 'descend' ?
428        sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
429        sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
430    @eids = sort { $match{$b} <=> $match{$a} } @eids;
431    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);
432    require MT::Entry;
433    map { push @entries, MT::Entry->load($_) } @eids;
434    return '' unless @entries;
435
436    my $res = '';
437    my $tokens = $ctx->stash('tokens');
438    my $builder = $ctx->stash('builder');
439    my $i = 0;
440    for my $e (@entries) {
441        local $ctx->{__stash}{entry} = $e;
442        local $ctx->{current_timestamp} = $e->created_on;
443        local $ctx->{modification_timestamp} = $e->modified_on;
444        my $out = $builder->build($ctx, $tokens, {
445            %$cond,
446            EntryIfExtended => $e->text_more ? 1 : 0,
447            EntryIfAllowComments => $e->allow_comments,
448            EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
449            EntryIfAllowPings => $e->allow_pings,
450            EntriesHeader => !$i,
451            EntriesFooter => !defined $entries[$i+1]
452            });
453        return $ctx->error($ctx->errstr) unless defined $out;
454        $res .= $out;
455        $i++;
456    }
457    $res;
458}
459
460sub encode_urlplus {
461    my $s = $_[0];
462    return $s unless $_[1];
463    require MT::Util;
464    $s =~ tr/ /+/;
465    MT::Util::encode_url($s);
466}
467
468eval {
469    require MT::XSearch;
470    MT::XSearch->add_search_plugin('Tagwire', {
471        label => 'Tag(Keyword) Search',
472        description => 'Tag(Keyword) Search plugin for MT-XSearch',
473        on_execute => \&xsearch_on_execute,
474        on_stash => \&xsearch_on_stash });
475};
476
477sub xsearch_on_stash {
478    $_[0]->stash('entry', $_[1]);
479    $_[0]->{current_timestamp} = $_[1]->created_on;
480    $_[0]->{modification_timestamp} = $_[1]->modified_on;
481}
482
483sub xsearch_on_execute {
484    my $args = shift;
485    require MT::Entry;
486    my $blog_id = $args->{blog_id} or MT->error('Blog ID is required.');
487    my $sort_by = $args->{sort_by} || 'created_on';
488    my $sort_order = $args->{sort_order} || 'descend';
489    my $delimiter = $args->{delimiter} || '';
490    my $case_sensitive = defined $args->{case_sensitive} ?
491        $args->{case_sensitive} : 1;
492
493    my @results;
494    my @patterns = split_args($args->{search}, $delimiter, $case_sensitive)
495        or return \@results;
496
497    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
498        or return \@results;
499    my %tindex = %{$data->{tindex}};
500    my %eindex = %{$data->{eindex}};
501    my %match;
502    if ($case_sensitive) {
503        foreach my $tag (@patterns) {
504            foreach my $eid (@{$tindex{$tag}}) {
505                $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
506            }
507        }
508    } else {
509        foreach my $tag (@patterns) {
510            foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
511                foreach my $eid (@{$tindex{$mtag}}) {
512                    $match{$eid} = exists $match{$eid} ? $match{$eid} + 1 : 1;
513                }
514            }
515        }
516    }
517    my $count = scalar @patterns;
518    my @eids = grep { $match{$_} == $count } keys %match
519        or return \@results;
520    @eids = $sort_order eq 'descend' ?
521        sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
522        sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
523    require MT::Entry;
524    map { push @results, MT::Entry->load($_) } @eids;
525    \@results;
526}
527
5281;
Note: See TracBrowser for help on using the browser.