root/tagwire/tags/0.23/tagwire.pl

Revision 102, 16.0 kB (checked in by ogawa, 4 years ago)

Fix for duplicated PluginData? problem with SQLite

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