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