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