tagging: Allow using titles in for related stories.
[matthijs/upstream/blosxom-plugins.git] / general / entries_cache_meta
1 # vim: ft=perl
2 # Blosxom Plugin: entries_cache_meta
3 # Author(s): Jason Thaxter <sseye@ahab.com>
4 # Version: 0.6
5
6 package entries_cache_meta;
7
8 # --- Configurable variables -----
9
10 #
11 # Caching variables
12 #
13
14 # How many minutes delay before entries are re-indexed?
15 # (In minutes)
16 # default: 15
17 # zero means never auto-reindex
18 $cache_period;
19
20 # reindex password via query string to force re-indexing
21 # default: none (re-index by time only)
22 $reindex_passwd;
23
24 # if true, this will warn when the cachefile can't be locked
25 # default: off
26 $write_lock_warn;
27
28 #
29 # Meta-related variables
30 #
31
32 # What prefix should I expect prepended to each meta tag?
33 # default: meta-
34 $meta_prefix;
35
36 # If set, this meta variable will enable you to set modification times
37 # e.g.:
38 #   meta-last_modified: 2003/01/12 10:33:33
39 # see pod docs re: time format
40 # default: mtime
41 $meta_mtime;
42
43 # enabled
44 # default: 1
45 # set to zero to disable this plugin
46 # good for use w/ config plugin
47 $enabled;
48
49 # --------------------------------
50
51 # defaults
52 $cache_period = 15 if not defined $cache_period;
53 $meta_prefix ||= 'meta-';
54 $meta_mtime = 'mtime' unless defined $meta_mtime;
55 $enabled    = 1       unless defined $enabled;
56
57 # standard perl modules
58 use Fcntl qw(:DEFAULT :flock);
59 use File::stat;
60 use File::Find;
61 use Data::Dumper;
62
63 # standard for blosxom
64 use CGI qw/:standard/;
65
66 # we'll use Date::Parse if we have it
67 $have_date_parse;
68 eval { require Date::Parse; };
69 if ($@) {
70     use Time::Local;
71 }
72 else {
73     $have_date_parse = 1;
74 }
75
76 %cache;    # the cache
77 %indexes = ();    # nothing - we don't do static
78 $reindex;         # reindex or not
79 *CACHE;           # for the cache file
80 $reindexed;       # flag for "save cache"
81
82 # the filename where the cache is stored
83 $cachefile = "$blosxom::plugin_state_dir/entries_cache_meta";
84 # and a temporary one while generating the cache
85 $cache_tmp = "$cachefile.TMP";
86
87 sub start {
88
89     return 0 if ( not $enabled or param('-all') );
90
91     # Read cache and reindex if failed or otherwise directed
92     (
93         (
94             not( $reindex_passwd and param('reindex') eq $reindex_passwd )
95               and not( $cache_period
96                 and -f "$cachefile"
97                 and stat($cachefile)->mtime lt( time() - $cache_period * 60 ) )
98         )
99           and ( open( MYCACHE, $cachefile )
100             and $index = join '', <MYCACHE>
101             and $index =~ /\$VAR1 = /
102             and eval($index)
103             and !$@
104             and %cache = %$VAR1
105             and close MYCACHE )
106
107       )
108       or $reindex = &lokkit( \*CACHE );
109
110     return 1;
111 }
112
113 sub entries {
114
115     # If we haven't flagged a need to re-index,
116     # copy from the cache
117     return sub {
118         my %files;
119         foreach ( keys %cache ) {
120
121             # copy into cache if the file exists
122             $files{$_} = $cache{$_}{mtime} if -f $_;
123         }
124         return ( \%files, \%indexes );
125       }
126       unless $reindex;
127
128     # otherwise, do a full reindex
129     return sub {
130
131         # Check to see if previously indexed files exist, and then rescan
132         # the datadir for any new files, while preserving the old times
133
134         for my $file ( keys %cache ) {
135             -f $file or do { $reindexed++; delete $cache{$file} };
136         }
137
138         find(
139             sub {
140
141                 # easier to read.
142                 $ffname = $File::Find::name;
143
144                 # return if we're out of our depth
145                 my $curr_depth = $File::Find::dir =~ tr[/][];
146                 if ( $blosxom::depth and $curr_depth > $blosxom::depth ) {
147                     $cache{$ffname}
148                       and delete $cache{$ffname};
149                     return;
150                 }
151
152                 # only bother for real entries
153                 return
154                   unless ( $ffname =~
155 m!^$blosxom::datadir/(?:(.*)/)?(.+)\.$blosxom::file_extension$!
156                     and $2 ne 'index'
157                     and $2 !~ /^\./
158                     and ( -r "$ffname" )
159                     and ++$reindexed );
160
161                 # process meta tags
162                 if ( -T "$ffname" and open( FF, "<$ffname" ) ) {
163                     $meta_title = <FF>;
164                     chomp($meta_title);
165                     $cache{$ffname}{title} = $meta_title;
166                     while (<FF>) {
167
168                         # get values
169                         my ( $key, $value ) = m/^$meta_prefix(\w+)\s*:\s*(.+)$/;
170                         last if not $key;
171
172                         # set values
173                         if ( $key ne "$meta_mtime" ) {
174                             $cache{$ffname}{$key} = $value;
175                         }
176
177                         # set modification time
178                         # with a double negative (hey)
179                         else {
180                             if ($have_date_parse) {
181                                 $time = Date::Parse::str2time($value);
182                             }
183                             elsif (
184                                 my ( $yr, $mo, $day, $hr, $min, $sec, $tz ) = (
185                                     $value =~
186 m! (\d{4}) [/\:\-] (\d{2}) [/\:\-] (\d{2}) \s+
187                                       (\d{2}) : (\d{2}) : (\d{2})
188                                       \s*((UTC|GMT)*)!x
189                                 )
190                               )
191                             {
192                                 if ($tz) {
193                                     $time =
194                                       timegm( $sec, $min, $hr, $day, --$mo,
195                                         $yr );
196                                 }
197                                 else {
198                                     $time =
199                                       timelocal( $sec, $min, $hr, $day, --$mo,
200                                         $yr );
201                                 }
202                             }
203                             else {
204                                 warn "unparseable time in $ffname: $value";
205                             }
206                             $cache{$ffname}{mtime} = $time;
207                         }
208                     }
209                     close FF;
210                 }
211                 else {
212                     warn "couldn't open entry ($ffname): $!";
213                 }
214
215                 # If we have no meta mtime and no cached time,
216                 # stat the file and store it.
217                 if ( $cache{$ffname}{mtime} ) {
218                     $files{$ffname} = $cache{$ffname}{mtime};
219                 }
220                 else {    # make sure blosxom behaves normally
221                     $files{$ffname} = stat($ffname)->mtime;
222                     $cache{$ffname}{mtime} = $files{$ffname};
223                 }
224
225                 # show or not to show future entries
226                 if ( not $blosxom::show_future_entries
227                     and $files{$ffname} > time() )
228                 {
229                     delete $files{$ffname} and return;
230                 }
231
232             },
233             $blosxom::datadir
234         );
235
236         return ( \%files, \%indexes );    # indexes is bogus: we don't do static
237       }
238 }
239
240 # put the stuff into $meta:: namespace for story rendering
241 # and trim meta tags out of our story
242 sub story {
243     my ( $pkg, $path, $filename, $story_ref, $title_ref, $body_ref ) = @_;
244
245     # first, load up our cached meta values
246     foreach my $key ( keys %{ $cache{$filename} } ) {
247         ${"meta::$key"} = $cache{$filename}{$key};
248     }
249
250     # next, remove any meta tags from the body
251     my ( $body, $in_body );
252     foreach ( split /\n/, $$body_ref ) {
253         if ($in_body) {
254             $body .= "$_\n";
255         }
256         elsif (/^$meta_prefix(\w+)\s*:\s*(.+)$/) {
257             ;
258         }
259         else {
260             $in_body = 1;
261             $body .= "$_\n";
262         }
263     }
264
265     $$body_ref = $body;
266
267     return 1;
268 }
269
270 # save cache - as late as possible
271 # this gives other plugins a chance to alter values
272 # or, forbid us from caching them!
273 sub end {
274     &stuffit( \*CACHE, Dumper \%cache ) if ($reindexed);
275     1;
276 }
277
278 #####################################################################
279 # Internal subs
280 #
281
282 # Reserve file for writing
283 sub lokkit {
284     local *FH = shift;
285
286     # open it
287     my $umask = umask 002;    # group-write is useful
288     sysopen( FH, "$cache_tmp", O_RDWR | O_CREAT )
289       or ( warn "can't open file $cache_tmp: $!" and return );
290     umask $umask;             # restore so it doesn't affect other plugins
291
292     # lock file
293     flock( FH, LOCK_EX | LOCK_NB )
294       or (  close FH
295         and ( not $write_lock_warn or warn "can't lock file: $!" )
296         and return );
297
298     return 1;
299 }
300
301 # Write and release file
302 sub stuffit {
303     local *FH       = shift;
304     local $contents = shift;
305
306     my $err;
307     print FH $contents or $err = "can't write cache $cache_tmp: $!";
308     flock( FH, LOCK_UN ) or $err = "can't unlock cache $cache_tmp: $!";
309     close FH or $err = "can't close cache $cache_tmp: $!";
310
311     if ( not $err ) {
312         rename "$cache_tmp", $cachefile
313           or warn "Error installing cache $cache_tmp: $!";
314     }
315     else {
316         warn $err;
317         unlink "$cache_tmp";
318     }
319 }
320
321 1;
322
323 __END__
324
325 =head1 NAME - entries_cache_meta
326
327 Blosxom Plug-in: entries_cache_meta
328
329 =head1 DESCRIPTION
330
331 The entries_cache_meta plugin is Yet Another Caching Plugin, with some
332 additional features, notably meta-tagging.
333
334 By combining the meta-tag functionality with the entries cache, it becomes
335 possible to write or use plugins that access meta-values outside the C<story>
336 hook. For example, you could use this plugin to write another one showing the
337 most recent entry for each author defined in meta tags.
338
339 Additionally, it makes use of file locking to avoid from re-indexing if another
340 request already has re-indexing in progress. Simultaneous re-indexing could
341 corrupt the cache file, or bog down the server with redundant, expensive tasks;
342 the larger and busier the site, the more likely these problems. For added
343 security, you must supply a password if you want to force re-indexing via query
344 string; this prevents any Blosxom-savvy user from forcing a re-index on your
345 site.
346
347 =head1 USAGE
348
349 A properly permissioned $plugin_state_dir is required. Configuration is
350 optional, but there are a few useful configuration variables.
351
352 =over 4
353
354 =item $cache_period
355
356 How often, in minutes, to reindex automatically. If set to zero, re-indexing will never
357 be automatic.
358
359 =item $reindex_passwd
360
361 What query string password forces reindex.  You can force a scan by appending a
362 query string, e.g.  C<?reindex=mypassword> to the end of the url.  This will
363 also cause meta tags to be updated. However, for security's sake, you must
364 set the password in the configuration to use this.
365
366 =item $write_lock_warn
367
368 Send a warning to the error log when it can't get a write-lock on the cache
369 file. This is mostly useful for verifying that write-locking actually does
370 something.
371
372 =item $meta_prefix
373
374 What prefix marks meta variable names. Defaults to C<meta->. An empty string
375 should be permitted, but currently it isn't.
376
377 =item $meta_mtime
378
379 The name of the meta-property for file modification times. If this property is
380 present in a given entry, it will override the modification time of the file as
381 the entry's time. The default value is "mtime".
382
383 The format of the time is obviously important: it can be either
384
385         meta-mtime: YYYY/MM/DD HH:MM:SS [UTC|GMT]
386
387 or, if Date::Parse is present, any format parseable by that module. Either way,
388 unparseable dates are simply ignored as if the property was not present. This
389 feature may be disabled by setting it to an empty string.
390
391 =back
392
393 =head1 VERSION
394
395 Version 0.6
396
397 =head1 AUTHOR
398
399 Jason Thaxter <sseye@ahab.com>
400
401 This plugin is now maintained by the Blosxom Sourceforge Team,
402 <blosxom-devel@lists.sourceforge.net>.
403
404 =head1 SEE ALSO
405
406 Blosxom Home/Docs/Licensing: http://blosxom.sourceforge.net
407
408 Blosxom Plugin Docs: http://blosxom.sourceforge.net/documentation/users/plugins.html
409
410 =head1 BUGS
411
412 It doesn't currently work in static mode, in part because the premise of static
413 mode is to be a performance benefit all on its own. Also because I don't use
414 static mode and I'm lazy.
415
416 Please send bug reports and feedback to the Blosxom development mailing list 
417 <blosxom-devel@lists.sourceforge.net>.
418
419 =head1 LICENSE
420
421 entries_cache_meta plugin
422 Copyright 2004, Jason Thaxter
423
424 Permission is hereby granted, free of charge, to any person obtaining a
425 copy of this software and associated documentation files (the "Software"),
426 to deal in the Software without restriction, including without limitation
427 the rights to use, copy, modify, merge, publish, distribute, sublicense,
428 and/or sell copies of the Software, and to permit persons to whom the
429 Software is furnished to do so, subject to the following conditions:
430
431 The above copyright notice and this permission notice shall be included
432 in all copies or substantial portions of the Software.
433
434 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
435 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
436 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
437 THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
438 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
439 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
440 OTHER DEALINGS IN THE SOFTWARE.