Add $date_first_in_url configuration variable.
[matthijs/upstream/blosxom.git] / blosxom.cgi
1 #!/usr/bin/perl
2
3 # Blosxom
4 # Author: Rael Dornfest (2002-2003), The Blosxom Development Team (2005-2009)
5 # Version: 2.1.2 ($Id: blosxom.cgi,v 1.98 2009/07/19 17:18:37 xtaran Exp $)
6 # Home/Docs/Licensing: http://blosxom.sourceforge.net/
7 # Development/Downloads: http://sourceforge.net/projects/blosxom
8
9 package blosxom;
10
11 =head1 NAME
12
13 blosxom - A lightweight yet feature-packed weblog
14
15 =head1 SYNOPSIS
16
17 B<blosxom> is a simple web log (blog) CGI script written in perl.
18
19 =head1 DESCRIPTION
20
21 B<Blosxom> (pronounced "I<blossom>") is a lightweight yet feature-packed
22 weblog application designed from the ground up with simplicity,
23 usability, and interoperability in mind.
24
25 Fundamental is its reliance upon the file system, folders and files
26 as its content database. Blosxom's weblog entries are plain text
27 files like any other. Write from the comfort of your favorite text
28 editor and hit the Save button. Create, edit, rename, and delete entries
29 on the command-line, via FTP, WebDAV, or anything else you
30 might use to manipulate your files. There's no import or export; entries
31 are nothing more complex than title on the first line, body being
32 everything thereafter.
33
34 Despite its tiny footprint, Blosxom doesn't skimp on features, sporting
35 the majority of features one would find in any other Weblog application.
36
37 Blosxom is simple, straightforward, minimalist Perl affording even the
38 dabbler an opportunity for experimentation and customization. And
39 last, but not least, Blosxom is open source and free for the taking and
40 altering.
41
42 =head1 USAGE
43
44 Write a weblog entry, and place it into the main data directory. Place
45 the the title is on the first line; the body is everything afterwards.
46 For example, create a file named I<first.txt> and put in it something
47 like this:
48
49   First Blosxom Post!
50
51   I have successfully installed blosxom on this system.  For more
52   information on blosxom, see the author's <a
53   href="http://blosxom.sourceforge.net/">blosxom site</a>.
54
55 Place the file in the directory under the I<$datadir> points to. Be
56 sure to change the default location to be somewhere accessable by the
57 web server that runs blosxom as a CGI program.
58
59 =cut
60
61 # --- Configurable variables -----
62
63 # What's this blog's title?
64 $blog_title = "My Weblog";
65
66 # What's this blog's description (for outgoing RSS feed)?
67 $blog_description = "Yet another Blosxom weblog.";
68
69 # What's this blog's primary language (for outgoing RSS feed)?
70 $blog_language = "en";
71
72 # What's this blog's text encoding ?
73 $blog_encoding = "UTF-8";
74
75 # Where are this blog's entries kept?
76 $datadir = "/Library/WebServer/Documents/blosxom";
77
78 # What's my preferred base URL for this blog (leave blank for
79 # automatic)?
80 $url = "";
81
82 # Should I stick only to the datadir for items or travel down the
83 # directory hierarchy looking for items?  If so, to what depth?
84 #
85 # 0 = infinite depth (aka grab everything), 1 = datadir only,
86 # n = n levels down
87
88 $depth = 0;
89
90 # How many entries should I show on the home page?
91 $num_entries = 40;
92
93 # What file extension signifies a blosxom entry?
94 $file_extension = "txt";
95
96 # What is the default flavour?
97 $default_flavour = "html";
98
99 # Should I show entries from the future (i.e. dated after now)?
100 $show_future_entries = 0;
101
102 # Should date components of the path always be at the front?
103 # If this is disabled, the date components can appear anywhere in the
104 # url (but always directly after each other, in the year/month/day
105 # order). For example, /category/subcategory/2008/12/ (or even
106 # /category/2008/12/subcategory/) shows all posts in subcategory from
107 # December 2008.
108 $date_first_in_url = 0;
109
110 # --- Plugins (Optional) -----
111
112 # File listing plugins blosxom should load (if empty blosxom will load
113 # all plugins in $plugin_dir and $plugin_path directories)
114 $plugin_list = "";
115
116 # Where are my plugins kept?
117 $plugin_dir = "";
118
119 # Where should my plugins keep their state information?
120 $plugin_state_dir = "$plugin_dir/state";
121
122 # Additional plugins location. A list of directories, separated by ';'
123 # on windows, ':' everywhere else.
124 $plugin_path = "";
125
126 # --- Static Rendering -----
127
128 # Where are this blog's static files to be created?
129 $static_dir = "/Library/WebServer/Documents/blog";
130
131 # What's my administrative password (you must set this for static
132 # rendering)?
133 $static_password = "";
134
135 # What flavours should I generate statically?
136 @static_flavours = qw/html rss/;
137
138 # Should I statically generate individual entries?
139 # 0 = no, 1 = yes
140 $static_entries = 0;
141
142 # --- Advanced Encoding Options -----
143
144 # Should I encode entities for xml content-types? (plugins can turn
145 # this off if they do it themselves)
146 $encode_xml_entities = 1;
147
148 # Should I encode 8 bit special characters, e.g. umlauts in URLs, e.g.
149 # convert an ISO-Latin-1 \"o to %F6? (off by default for now; plugins
150 # can change this, too)
151 $encode_8bit_chars = 0;
152
153 # RegExp matching all characters which should be URL encoded in links.
154 # Defaults to anything but numbers, letters, slash, colon, dash,
155 # underscore and dot.
156 $url_escape_re = qr([^-/a-zA-Z0-9:._]);
157
158 # --------------------------------
159
160 =head1 ENVIRONMENT
161
162 =over
163
164 =item B<BLOSXOM_CONFIG_FILE>
165
166 Points to the location of the configuration file. This will be
167 considered as first option, if it's set.
168
169
170 =item B<BLOSXOM_CONFIG_DIR>
171
172 The here named directory will be tried unless the above mentioned
173 environment variable is set and tested for a contained blosxom.conf
174 file.
175
176
177 =back
178
179
180 =head1 FILES
181
182 =over
183
184 =item B</usr/lib/cgi-bin/blosxom>
185
186 The CGI script itself. Please note that the location might depend on
187 your installation.
188
189 =item B</etc/blosxom/blosxom.conf>
190
191 The default configuration file location. This is rather taken as last
192 ressort if no other configuration location is set through environment
193 variables.
194
195 =back
196
197
198 =head1 AUTHOR
199
200 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
201 development was picked up by a team of dedicated users of blosxom since
202 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
203
204 =cut
205
206
207 use vars qw!
208     $version
209     $blog_title
210     $blog_description
211     $blog_language
212     $blog_encoding
213     $datadir
214     $url
215     %template
216     $template
217     $depth
218     $num_entries
219     $file_extension
220     $default_flavour
221     $static_or_dynamic
222     $config_dir
223     $plugin_list
224     $plugin_path
225     $plugin_dir
226     $plugin_state_dir
227     @plugins
228     %plugins
229     $static_dir
230     $static_password
231     @static_flavours
232     $static_entries
233     $path_info_full
234     $path_info
235     $path_info_yr
236     $path_info_mo
237     $path_info_da
238     $path_info_mo_num
239     $flavour
240     $static_or_dynamic
241     %month2num
242     @num2month
243     $interpolate
244     $entries
245     $output
246     $header
247     $show_future_entries
248     $date_first_in_url
249     %files
250     %indexes
251     %others
252     $encode_xml_entities
253     $encode_8bit_chars
254     $url_escape_re
255     $content_type
256 !;
257
258 use strict;
259 use FileHandle;
260 use File::Find;
261 use File::stat;
262 use Time::Local;
263 use CGI qw/:standard :netscape/;
264
265 $version = "2.1.2+dev";
266
267 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
268 my $blosxom_config;
269 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
270     $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
271     ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
272 }
273 else {
274     for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
275         '/etc' )
276     {
277         if ( -r "$blosxom_config_dir/blosxom.conf" ) {
278             $config_dir     = $blosxom_config_dir;
279             $blosxom_config = "$blosxom_config_dir/blosxom.conf";
280             last;
281         }
282     }
283 }
284
285 # Load $blosxom_config
286 if ($blosxom_config) {
287     if ( -r $blosxom_config ) {
288         eval { require $blosxom_config }
289             or warn "Error reading blosxom config file '$blosxom_config'"
290             . ( $@ ? ": $@" : '' );
291     }
292     else {
293         warn "Cannot find or read blosxom config file '$blosxom_config'";
294     }
295 }
296
297 my $fh = new FileHandle;
298
299 %month2num = (
300     nil => '00',
301     Jan => '01',
302     Feb => '02',
303     Mar => '03',
304     Apr => '04',
305     May => '05',
306     Jun => '06',
307     Jul => '07',
308     Aug => '08',
309     Sep => '09',
310     Oct => '10',
311     Nov => '11',
312     Dec => '12'
313 );
314 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
315
316 # Use the stated preferred URL or figure it out automatically. Set
317 # $url manually in the config section above if CGI.pm doesn't guess
318 # the base URL correctly, e.g. when called from a Server Side Includes
319 # document or so.
320 unless ($url) {
321     $url = url();
322
323     # Unescape %XX hex codes (from URI::Escape::uri_unescape)
324     $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;      
325
326     # Support being called from inside a SSI document
327     $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
328
329     # Remove PATH_INFO if it is set but not removed by CGI.pm. This
330     # seems to happen when used with Apache's Alias directive or if
331     # called from inside a Server Side Include document. If that
332     # doesn't help either, set $url manually in the configuration.
333     $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
334
335     # NOTE:
336     #
337     # There is one case where this code does more than necessary, too:
338     # If the URL requested is e.g. http://example.org/blog/blog and
339     # the base URL is correctly determined as http://example.org/blog
340     # by CGI.pm, then this code will incorrectly normalize the base
341     # URL down to http://example.org, because the same string as
342     # PATH_INFO is part of the base URL, too. But this is such a
343     # seldom case and can be fixed by setting $url in the config file,
344     # too.
345 }
346
347 # The only modification done to a manually set base URL is to strip
348 # a trailing slash if present.
349
350 $url =~ s!/$!!;
351
352 # Drop ending any / from dir settings
353 $datadir    =~ s!/$!!;
354 $plugin_dir =~ s!/$!!;
355 $static_dir =~ s!/$!!;
356
357 # Fix depth to take into account datadir's path
358 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
359
360 if (    !$ENV{GATEWAY_INTERFACE}
361     and param('-password')
362     and $static_password
363     and param('-password') eq $static_password )
364 {
365     $static_or_dynamic = 'static';
366 }
367 else {
368     $static_or_dynamic = 'dynamic';
369     param( -name => '-quiet', -value => 1 );
370 }
371
372 # Path Info Magic
373 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
374 my @path_info = split m{/}, path_info() || param('path');
375 $path_info_full = join '/', @path_info;      # Equivalent to $ENV{PATH_INFO}
376 shift @path_info;
377
378 # Flavour specified by ?flav={flav} or index.{flav}
379 $flavour = '';
380 if (! ($flavour = param('flav'))) {
381     if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
382        $flavour = $2;
383         pop @path_info if $1 eq 'index';
384     }
385 }
386 $flavour ||= $default_flavour;
387
388 # Fix XSS in flavour name (CVE-2008-2236)
389 $flavour = blosxom_html_escape($flavour);
390
391 sub blosxom_html_escape {
392   my $string = shift;
393   my %escape = (
394                 '<' => '&lt;',
395                 '>' => '&gt;',
396                 '&' => '&amp;',
397                 '"' => '&quot;',
398                 "'" => '&apos;'
399                 );
400   my $escape_re = join '|' => keys %escape;
401   $string =~ s/($escape_re)/$escape{$1}/g;
402   $string;
403 }
404
405 # Global variable to be used in head/foot.{flavour} templates
406 $path_info = '';
407
408 if (!$date_first_in_url) {
409         # Add all @path_info elements to $path_info till we come to one that could be a year
410         while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
411                 $path_info .= '/' . shift @path_info;
412         }
413 }
414
415 # Pull date elements out of path
416 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
417   $path_info_yr = shift @path_info;
418   if ($path_info[0] && 
419      ($path_info[0] =~ /^(0\d|1[012])$/ || 
420       exists $month2num{ ucfirst lc $path_info_mo })) {
421     $path_info_mo = shift @path_info;
422     # Map path_info_mo to numeric $path_info_mo_num
423     $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
424       ? $path_info_mo
425       : $month2num{ ucfirst lc $path_info_mo };
426     if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
427       $path_info_da = shift @path_info;
428     }
429   }
430 }
431
432 # Add remaining path elements to $path_info
433 $path_info .= '/' . join('/', @path_info);
434
435 # Strip spurious slashes
436 $path_info =~ s!(^/*)|(/*$)!!g;
437
438 # Define standard template subroutine, plugin-overridable at Plugins: Template
439 $template = sub {
440     my ( $path, $chunk, $flavour ) = @_;
441
442     do {
443         return join '', <$fh>
444             if $fh->open("< $datadir/$path/$chunk.$flavour");
445     } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
446
447     # Check for definedness, since flavour can be the empty string
448     if ( defined $template{$flavour}{$chunk} ) {
449         return $template{$flavour}{$chunk};
450     }
451     elsif ( defined $template{error}{$chunk} ) {
452         return $template{error}{$chunk};
453     }
454     else {
455         return '';
456     }
457 };
458
459 # Bring in the templates
460 %template = ();
461 while (<DATA>) {
462     last if /^(__END__)$/;
463     my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
464     $txt =~ s/\\n/\n/mg;
465     $template{$ct}{$comp} .= $txt . "\n";
466 }
467
468 # Plugins: Start
469 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
470 my @plugin_dirs = split /$path_sep/, $plugin_path;
471 unshift @plugin_dirs, $plugin_dir;
472 my @plugin_list = ();
473 my %plugin_hash = ();
474
475 # If $plugin_list is set, read plugins to use from that file
476 if ( $plugin_list ) {
477     if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
478         @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
479         $fh->close;
480     }
481     else {
482         warn "unable to read or open plugin_list '$plugin_list': $!";
483         $plugin_list = '';
484     }
485 }
486
487 # Otherwise walk @plugin_dirs to get list of plugins to use
488 if ( ! @plugin_list && @plugin_dirs ) {
489     for my $plugin_dir (@plugin_dirs) {
490         next unless -d $plugin_dir;
491         if ( opendir PLUGINS, $plugin_dir ) {
492             for my $plugin (
493                 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
494                 readdir(PLUGINS) )
495             {
496
497                 # Ignore duplicates
498                 next if $plugin_hash{$plugin};
499
500                 # Add to @plugin_list and %plugin_hash
501                 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
502                 push @plugin_list, $plugin;
503             }
504             closedir PLUGINS;
505         }
506     }
507     @plugin_list = sort @plugin_list;
508 }
509
510 # Load all plugins in @plugin_list
511 unshift @INC, @plugin_dirs;
512 foreach my $plugin (@plugin_list) {
513     my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
514     my $plugin_file = $plugin_list ? $plugin_name : $plugin;
515     my $on_off = $off eq '_' ? -1 : 1;
516
517     # Allow perl module plugins
518     # The -z test is a hack to allow a zero-length placeholder file in a 
519     #   $plugin_path directory to indicate an @INC module should be loaded
520     if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
521
522      # For Blosxom::Plugin::Foo style plugins, we need to use a string require
523         eval "require $plugin_file";
524     }
525     else
526     { # we try first to load from $plugin_dir before attempting from $plugin_path
527         eval        { require "$plugin_dir/$plugin_file" }
528             or eval { require $plugin_file };
529     }
530
531     if ($@) {
532         warn "error finding or loading blosxom plugin '$plugin_name': $@";
533         next;
534     }
535     if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
536         push @plugins, $plugin_name;
537     }
538
539 }
540 shift @INC foreach @plugin_dirs;
541
542 # Plugins: Template
543 # Allow for the first encountered plugin::template subroutine to override the
544 # default built-in template subroutine
545 foreach my $plugin (@plugins) {
546     if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
547         if ( my $tmp = $plugin->template() ) {
548             $template = $tmp;
549             last;
550         }
551     }
552 }
553
554 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
555 sub load_template {
556     return &$template(@_);
557 }
558
559 # Define default entries subroutine
560 $entries = sub {
561     my ( %files, %indexes, %others );
562     find(
563         sub {
564             my $d;
565             my $curr_depth = $File::Find::dir =~ tr[/][];
566             return if $depth and $curr_depth > $depth;
567
568             if (
569
570                 # a match
571                 $File::Find::name
572                 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
573
574                 # not an index, .file, and is readable
575                 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
576                 )
577             {
578
579                 # read modification time
580                 my $mtime = stat($File::Find::name)->mtime or return;
581
582                 # to show or not to show future entries
583                 return unless ( $show_future_entries or $mtime < time );
584
585                 # add the file and its associated mtime to the list of files
586                 $files{$File::Find::name} = $mtime;
587
588                 # static rendering bits
589                 my $static_file
590                     = "$static_dir/$1/index." . $static_flavours[0];
591                 if (   param('-all')
592                     or !-f $static_file
593                     or stat($static_file)->mtime < $mtime )
594                 {
595                     $indexes{$1} = 1;
596                     $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
597                     $indexes{$d} = $d;
598                     $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
599                         if $static_entries;
600                 }
601             }
602
603             # not an entries match
604             elsif ( !-d $File::Find::name and -r $File::Find::name ) {
605                 $others{$File::Find::name} = stat($File::Find::name)->mtime;
606             }
607         },
608         $datadir
609     );
610
611     return ( \%files, \%indexes, \%others );
612 };
613
614 # Plugins: Entries
615 # Allow for the first encountered plugin::entries subroutine to override the
616 # default built-in entries subroutine
617 foreach my $plugin (@plugins) {
618     if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
619         if ( my $tmp = $plugin->entries() ) {
620             $entries = $tmp;
621             last;
622         }
623     }
624 }
625
626 my ( $files, $indexes, $others ) = &$entries();
627 %indexes = %$indexes;
628
629 # Static
630 if (    !$ENV{GATEWAY_INTERFACE}
631     and param('-password')
632     and $static_password
633     and param('-password') eq $static_password )
634 {
635
636     param('-quiet') or print "Blosxom is generating static index pages...\n";
637
638     # Home Page and Directory Indexes
639     my %done;
640     foreach my $path ( sort keys %indexes ) {
641         my $p = '';
642         foreach ( ( '', split /\//, $path ) ) {
643             $p .= "/$_";
644             $p =~ s!^/!!;
645             next if $done{$p}++;
646             mkdir "$static_dir/$p", 0755
647                 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
648             foreach $flavour (@static_flavours) {
649                 $content_type
650                     = ( &$template( $p, 'content_type', $flavour ) );
651                 $content_type =~ s!\n.*!!s;
652                 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
653                 param('-quiet') or print "$fn.$flavour\n";
654                 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
655                     or die "Couldn't open $static_dir/$p for writing: $!";
656                 $output = '';
657                 if ( $indexes{$path} == 1 ) {
658
659                     # category
660                     $path_info = $p;
661
662                     # individual story
663                     $path_info =~ s!\.$file_extension$!\.$flavour!;
664                     print $fh_w &generate( 'static', $path_info, '', $flavour,
665                         $content_type );
666                 }
667                 else {
668
669                     # date
670                     local (
671                         $path_info_yr, $path_info_mo,
672                         $path_info_da, $path_info
673                     ) = split /\//, $p, 4;
674                     unless ( defined $path_info ) { $path_info = "" }
675                     print $fh_w &generate( 'static', '', $p, $flavour,
676                         $content_type );
677                 }
678                 $fh_w->close;
679             }
680         }
681     }
682 }
683
684 # Dynamic
685 else {
686     $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
687     $content_type =~ s!\n.*!!s;
688
689     $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
690     $header = { -type => $content_type };
691
692     print generate( 'dynamic', $path_info,
693         "$path_info_yr/$path_info_mo_num/$path_info_da",
694         $flavour, $content_type );
695 }
696
697 # Plugins: End
698 foreach my $plugin (@plugins) {
699     if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
700         $entries = $plugin->end();
701     }
702 }
703
704 # Generate
705 sub generate {
706     my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
707         = @_;
708
709     %files = %$files;
710     %others = ref $others ? %$others : ();
711
712     # Plugins: Filter
713     foreach my $plugin (@plugins) {
714         if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
715             $entries = $plugin->filter( \%files, \%others );
716         }
717     }
718
719     my %f = %files;
720
721     # Plugins: Skip
722     # Allow plugins to decide if we can cut short story generation
723     my $skip;
724     foreach my $plugin (@plugins) {
725         if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
726             if ( my $tmp = $plugin->skip() ) {
727                 $skip = $tmp;
728                 last;
729             }
730         }
731     }
732
733     # Define default interpolation subroutine
734     $interpolate = sub {
735         package blosxom;
736         my $template = shift;
737         # Interpolate scalars, namespaced scalars, and hash/hashref scalars
738         $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{([\'\"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
739         return $template;
740     };
741
742     unless ( defined($skip) and $skip ) {
743
744         # Plugins: Interpolate
745         # Allow for the first encountered plugin::interpolate subroutine to
746         # override the default built-in interpolate subroutine
747         foreach my $plugin (@plugins) {
748             if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
749                 if ( my $tmp = $plugin->interpolate() ) {
750                     $interpolate = $tmp;
751                     last;
752                 }
753             }
754         }
755
756         # Head
757         my $head = ( &$template( $currentdir, 'head', $flavour ) );
758
759         # Plugins: Head
760         foreach my $plugin (@plugins) {
761             if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
762                 $entries = $plugin->head( $currentdir, \$head );
763             }
764         }
765
766         $head = &$interpolate($head);
767
768         $output .= $head;
769
770         # Stories
771         my $curdate = '';
772         my $ne      = $num_entries;
773
774         if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
775             $currentdir = "$1$2.$file_extension";
776             %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
777                 if $files{"$datadir/$currentdir"};
778         }
779         else {
780             $currentdir =~ s!/index\..+$!!;
781         }
782
783         # Define a default sort subroutine
784         my $sort = sub {
785             my ($files_ref) = @_;
786             return
787                 sort { $files_ref->{$b} <=> $files_ref->{$a} }
788                 keys %$files_ref;
789         };
790
791      # Plugins: Sort
792      # Allow for the first encountered plugin::sort subroutine to override the
793      # default built-in sort subroutine
794         foreach my $plugin (@plugins) {
795             if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
796                 if ( my $tmp = $plugin->sort() ) {
797                     $sort = $tmp;
798                     last;
799                 }
800             }
801         }
802
803         foreach my $path_file ( &$sort( \%f, \%others ) ) {
804             last if $ne <= 0 && $date !~ /\d/;
805             use vars qw/ $path $fn /;
806             ( $path, $fn )
807                 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
808
809             # Only stories in the right hierarchy
810             $path =~ /^$currentdir/
811                 or $path_file eq "$datadir/$currentdir"
812                 or next;
813
814             # Prepend a slash for use in templates only if a path exists
815             $path &&= "/$path";
816
817             # Date fiddling for by-{year,month,day} archive views
818             use vars
819                 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
820             ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
821                 = nice_date( $files{"$path_file"} );
822             ( $hr, $min ) = split /:/, $ti;
823             ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
824             $hr12 =~ s/^0//;
825             if ( $hr12 == 0 ) { $hr12 = 12 }
826
827             # Only stories from the right date
828             my ( $path_info_yr, $path_info_mo_num, $path_info_da )
829                 = split /\//, $date;
830             next if $path_info_yr     && $yr != $path_info_yr;
831             last if $path_info_yr     && $yr < $path_info_yr;
832             next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
833             next if $path_info_da     && $da != $path_info_da;
834             last if $path_info_da     && $da < $path_info_da;
835
836             # Date
837             my $date = ( &$template( $path, 'date', $flavour ) );
838
839             # Plugins: Date
840             foreach my $plugin (@plugins) {
841                 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
842                     $entries
843                         = $plugin->date( $currentdir, \$date,
844                         $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
845                         $yr );
846                 }
847             }
848
849             $date = &$interpolate($date);
850
851             if ( $date && $curdate ne $date ) {
852                 $curdate = $date;
853                 $output .= $date;
854             }
855
856             use vars qw/ $title $body $raw /;
857             if ( -f "$path_file" && $fh->open("< $path_file") ) {
858                 chomp( $title = <$fh> );
859                 chomp( $body = join '', <$fh> );
860                 $fh->close;
861                 $raw = "$title\n$body";
862             }
863             my $story = ( &$template( $path, 'story', $flavour ) );
864
865             # Plugins: Story
866             foreach my $plugin (@plugins) {
867                 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
868                     $entries = $plugin->story( $path, $fn, \$story, \$title,
869                         \$body );
870                 }
871             }
872
873             # Save unescaped versions and allow them to be used in
874             # flavour templates.
875             use vars qw/$url_unesc $path_unesc $fn_unesc/;
876             $url_unesc  = $url;
877             $path_unesc = $path;
878             $fn_unesc   = $fn;
879
880             # Fix special characters in links inside XML content
881             if ( $encode_xml_entities &&
882                  $content_type =~ m{\bxml\b} &&
883                  $content_type !~ m{\bxhtml\b} ) {
884                 # Escape special characters inside the <link> container
885
886                 &url_escape_url_path_and_fn();
887
888                 # Escape <, >, and &, and to produce valid RSS
889                 $title = blosxom_html_escape($title);
890                 $body  = blosxom_html_escape($body);
891                 $url   = blosxom_html_escape($url);
892                 $path  = blosxom_html_escape($path);
893                 $fn    = blosxom_html_escape($fn);
894             }
895
896             # Fix special characters in links inside XML content
897             if ($encode_8bit_chars) {
898                 &url_escape_url_path_and_fn();
899             }
900
901             $story = &$interpolate($story);
902
903             $output .= $story;
904             $fh->close;
905
906             $ne--;
907         }
908
909         # Foot
910         my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
911
912         # Plugins: Foot
913         foreach my $plugin (@plugins) {
914             if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
915                 $entries = $plugin->foot( $currentdir, \$foot );
916             }
917         }
918
919         $foot = &$interpolate($foot);
920         $output .= $foot;
921
922         # Plugins: Last
923         foreach my $plugin (@plugins) {
924             if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
925                 $entries = $plugin->last();
926             }
927         }
928
929     }    # End skip
930
931     # Finally, add the header, if any and running dynamically
932     $output = header($header) . $output
933         if ( $static_or_dynamic eq 'dynamic' and $header );
934
935     $output;
936 }
937
938 sub nice_date {
939     my ($unixtime) = @_;
940
941     my $c_time = CORE::localtime($unixtime);
942     my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
943         = ( $c_time
944             =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
945         );
946     $ti = "$hr:$min";
947     $da = sprintf( "%02d", $da );
948     my $mo_num = $month2num{$mo};
949
950     my $offset
951         = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
952     my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
953         . sprintf( "%02d", ( $offset % 3600 ) / 60 );
954
955     return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
956 }
957
958 sub url_escape_url_path_and_fn {
959     $url   =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
960     $path  =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
961     $fn    =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
962 }
963
964 # Default HTML and RSS template bits
965 __DATA__
966 html content_type text/html; charset=$blog_encoding
967
968 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
969 html head <html>
970 html head     <head>
971 html head         <meta http-equiv="content-type" content="$content_type" >
972 html head         <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
973 html head         <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
974 html head     </head>
975 html head     <body>
976 html head         <div align="center">
977 html head             <h1>$blog_title</h1>
978 html head             <p>$path_info_da $path_info_mo $path_info_yr</p>
979 html head         </div>
980
981 html story         <div>
982 html story             <h3><a name="$fn">$title</a></h3>
983 html story             <div>$body</div>
984 html story             <p>posted at: $ti | path: <a href="$url$path">$path</a> | <a href="$url/$yr/$mo_num/$da#$fn">permanent link to this entry</a></p>
985 html story         </div>
986
987 html date         <h2>$dw, $da $mo $yr</h2>
988
989 html foot
990 html foot         <div align="center">
991 html foot             <a href="http://blosxom.sourceforge.net/"><img src="http://blosxom.sourceforge.net/images/pb_blosxom.gif" alt="powered by blosxom" border="0" width="90" height="33" ></a>
992 html foot         </div>
993 html foot     </body>
994 html foot </html>
995
996 rss content_type text/xml; charset=$blog_encoding
997
998 rss head <?xml version="1.0" encoding="$blog_encoding"?>
999 rss head <rss version="2.0">
1000 rss head   <channel>
1001 rss head     <title>$blog_title</title>
1002 rss head     <link>$url/$path_info</link>
1003 rss head     <description>$blog_description</description>
1004 rss head     <language>$blog_language</language>
1005 rss head     <docs>http://blogs.law.harvard.edu/tech/rss</docs>
1006 rss head     <generator>blosxom/$version</generator>
1007
1008 rss story   <item>
1009 rss story     <title>$title</title>
1010 rss story     <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
1011 rss story     <link>$url/$yr/$mo_num/$da#$fn</link>
1012 rss story     <category>$path</category>
1013 rss story     <guid isPermaLink="false">$url$path/$fn</guid>
1014 rss story     <description>$body</description>
1015 rss story   </item>
1016
1017 rss date 
1018
1019 rss foot   </channel>
1020 rss foot </rss>
1021
1022 error content_type text/html
1023
1024 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
1025 error head <html>
1026 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
1027 error head     <body>
1028 error head         <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
1029 error head         <p>I'm afraid this is the first I've heard of a "$flavour" flavoured Blosxom.  Try dropping the "/+$flavour" bit from the end of the URL.</p>
1030
1031 error story        <h3>$title</h3>
1032 error story        <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
1033
1034 error date         <h2>$dw, $da $mo $yr</h2>
1035
1036 error foot     </body>
1037 error foot </html>
1038 __END__
1039