Refactoring usage of $url_escape_re into a function named url_escape_url_path_and_fn.
[pkg/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
199 use vars qw!
200     $version
201     $blog_title
202     $blog_description
203     $blog_language
204     $blog_encoding
205     $datadir
206     $url
207     %template
208     $template
209     $depth
210     $num_entries
211     $file_extension
212     $default_flavour
213     $static_or_dynamic
214     $config_dir
215     $plugin_list
216     $plugin_path
217     $plugin_dir
218     $plugin_state_dir
219     @plugins
220     %plugins
221     $static_dir
222     $static_password
223     @static_flavours
224     $static_entries
225     $path_info_full
226     $path_info
227     $path_info_yr
228     $path_info_mo
229     $path_info_da
230     $path_info_mo_num
231     $flavour
232     $static_or_dynamic
233     %month2num
234     @num2month
235     $interpolate
236     $entries
237     $output
238     $header
239     $show_future_entries
240     %files
241     %indexes
242     %others
243     $encode_xml_entities
244     $encode_8bit_chars
245     $url_escape_re
246     $content_type
247 !;
248
249 use strict;
250 use FileHandle;
251 use File::Find;
252 use File::stat;
253 use Time::Local;
254 use CGI qw/:standard :netscape/;
255
256 $version = "2.1.2+dev";
257
258 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
259 my $blosxom_config;
260 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
261     $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
262     ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
263 }
264 else {
265     for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
266         '/etc' )
267     {
268         if ( -r "$blosxom_config_dir/blosxom.conf" ) {
269             $config_dir     = $blosxom_config_dir;
270             $blosxom_config = "$blosxom_config_dir/blosxom.conf";
271             last;
272         }
273     }
274 }
275
276 # Load $blosxom_config
277 if ($blosxom_config) {
278     if ( -r $blosxom_config ) {
279         eval { require $blosxom_config }
280             or warn "Error reading blosxom config file '$blosxom_config'"
281             . ( $@ ? ": $@" : '' );
282     }
283     else {
284         warn "Cannot find or read blosxom config file '$blosxom_config'";
285     }
286 }
287
288 my $fh = new FileHandle;
289
290 %month2num = (
291     nil => '00',
292     Jan => '01',
293     Feb => '02',
294     Mar => '03',
295     Apr => '04',
296     May => '05',
297     Jun => '06',
298     Jul => '07',
299     Aug => '08',
300     Sep => '09',
301     Oct => '10',
302     Nov => '11',
303     Dec => '12'
304 );
305 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
306
307 # Use the stated preferred URL or figure it out automatically. Set
308 # $url manually in the config section above if CGI.pm doesn't guess
309 # the base URL correctly, e.g. when called from a Server Side Includes
310 # document or so.
311 unless ($url) {
312     $url = url();
313
314     # Unescape %XX hex codes (from URI::Escape::uri_unescape)
315     $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;      
316
317     # Support being called from inside a SSI document
318     $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
319
320     # Remove PATH_INFO if it is set but not removed by CGI.pm. This
321     # seems to happen when used with Apache's Alias directive or if
322     # called from inside a Server Side Include document. If that
323     # doesn't help either, set $url manually in the configuration.
324     $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
325
326     # NOTE:
327     #
328     # There is one case where this code does more than necessary, too:
329     # If the URL requested is e.g. http://example.org/blog/blog and
330     # the base URL is correctly determined as http://example.org/blog
331     # by CGI.pm, then this code will incorrectly normalize the base
332     # URL down to http://example.org, because the same string as
333     # PATH_INFO is part of the base URL, too. But this is such a
334     # seldom case and can be fixed by setting $url in the config file,
335     # too.
336 }
337
338 # The only modification done to a manually set base URL is to strip
339 # a trailing slash if present.
340
341 $url =~ s!/$!!;
342
343 # Drop ending any / from dir settings
344 $datadir    =~ s!/$!!;
345 $plugin_dir =~ s!/$!!;
346 $static_dir =~ s!/$!!;
347
348 # Fix depth to take into account datadir's path
349 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
350
351 if (    !$ENV{GATEWAY_INTERFACE}
352     and param('-password')
353     and $static_password
354     and param('-password') eq $static_password )
355 {
356     $static_or_dynamic = 'static';
357 }
358 else {
359     $static_or_dynamic = 'dynamic';
360     param( -name => '-quiet', -value => 1 );
361 }
362
363 # Path Info Magic
364 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
365 my @path_info = split m{/}, path_info() || param('path');
366 $path_info_full = join '/', @path_info;      # Equivalent to $ENV{PATH_INFO}
367 shift @path_info;
368
369 # Flavour specified by ?flav={flav} or index.{flav}
370 $flavour = '';
371 if (! ($flavour = param('flav'))) {
372     if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
373        $flavour = $2;
374         pop @path_info if $1 eq 'index';
375     }
376 }
377 $flavour ||= $default_flavour;
378
379 # Fix XSS in flavour name (CVE-2008-2236)
380 $flavour = blosxom_html_escape($flavour);
381
382 sub blosxom_html_escape {
383   my $string = shift;
384   my %escape = (
385                 '<' => '&lt;',
386                 '>' => '&gt;',
387                 '&' => '&amp;',
388                 '"' => '&quot;',
389                 "'" => '&apos;'
390                 );
391   my $escape_re = join '|' => keys %escape;
392   $string =~ s/($escape_re)/$escape{$1}/g;
393   $string;
394 }
395
396 # Global variable to be used in head/foot.{flavour} templates
397 $path_info = '';
398 # Add all @path_info elements to $path_info till we come to one that could be a year
399 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
400     $path_info .= '/' . shift @path_info;
401 }
402
403 # Pull date elements out of path
404 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
405   $path_info_yr = shift @path_info;
406   if ($path_info[0] && 
407      ($path_info[0] =~ /^(0\d|1[012])$/ || 
408       exists $month2num{ ucfirst lc $path_info_mo })) {
409     $path_info_mo = shift @path_info;
410     # Map path_info_mo to numeric $path_info_mo_num
411     $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
412       ? $path_info_mo
413       : $month2num{ ucfirst lc $path_info_mo };
414     if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
415       $path_info_da = shift @path_info;
416     }
417   }
418 }
419
420 # Add remaining path elements to $path_info
421 $path_info .= '/' . join('/', @path_info);
422
423 # Strip spurious slashes
424 $path_info =~ s!(^/*)|(/*$)!!g;
425
426 # Define standard template subroutine, plugin-overridable at Plugins: Template
427 $template = sub {
428     my ( $path, $chunk, $flavour ) = @_;
429
430     do {
431         return join '', <$fh>
432             if $fh->open("< $datadir/$path/$chunk.$flavour");
433     } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
434
435     # Check for definedness, since flavour can be the empty string
436     if ( defined $template{$flavour}{$chunk} ) {
437         return $template{$flavour}{$chunk};
438     }
439     elsif ( defined $template{error}{$chunk} ) {
440         return $template{error}{$chunk};
441     }
442     else {
443         return '';
444     }
445 };
446
447 # Bring in the templates
448 %template = ();
449 while (<DATA>) {
450     last if /^(__END__)$/;
451     my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
452     $txt =~ s/\\n/\n/mg;
453     $template{$ct}{$comp} .= $txt . "\n";
454 }
455
456 # Plugins: Start
457 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
458 my @plugin_dirs = split /$path_sep/, $plugin_path;
459 unshift @plugin_dirs, $plugin_dir;
460 my @plugin_list = ();
461 my %plugin_hash = ();
462
463 # If $plugin_list is set, read plugins to use from that file
464 if ( $plugin_list ) {
465     if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
466         @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
467         $fh->close;
468     }
469     else {
470         warn "unable to read or open plugin_list '$plugin_list': $!";
471         $plugin_list = '';
472     }
473 }
474
475 # Otherwise walk @plugin_dirs to get list of plugins to use
476 if ( ! @plugin_list && @plugin_dirs ) {
477     for my $plugin_dir (@plugin_dirs) {
478         next unless -d $plugin_dir;
479         if ( opendir PLUGINS, $plugin_dir ) {
480             for my $plugin (
481                 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
482                 readdir(PLUGINS) )
483             {
484
485                 # Ignore duplicates
486                 next if $plugin_hash{$plugin};
487
488                 # Add to @plugin_list and %plugin_hash
489                 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
490                 push @plugin_list, $plugin;
491             }
492             closedir PLUGINS;
493         }
494     }
495     @plugin_list = sort @plugin_list;
496 }
497
498 # Load all plugins in @plugin_list
499 unshift @INC, @plugin_dirs;
500 foreach my $plugin (@plugin_list) {
501     my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
502     my $plugin_file = $plugin_list ? $plugin_name : $plugin;
503     my $on_off = $off eq '_' ? -1 : 1;
504
505     # Allow perl module plugins
506     # The -z test is a hack to allow a zero-length placeholder file in a 
507     #   $plugin_path directory to indicate an @INC module should be loaded
508     if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
509
510      # For Blosxom::Plugin::Foo style plugins, we need to use a string require
511         eval "require $plugin_file";
512     }
513     else
514     { # we try first to load from $plugin_dir before attempting from $plugin_path
515         eval        { require "$plugin_dir/$plugin_file" }
516             or eval { require $plugin_file };
517     }
518
519     if ($@) {
520         warn "error finding or loading blosxom plugin '$plugin_name': $@";
521         next;
522     }
523     if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
524         push @plugins, $plugin_name;
525     }
526
527 }
528 shift @INC foreach @plugin_dirs;
529
530 # Plugins: Template
531 # Allow for the first encountered plugin::template subroutine to override the
532 # default built-in template subroutine
533 foreach my $plugin (@plugins) {
534     if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
535         if ( my $tmp = $plugin->template() ) {
536             $template = $tmp;
537             last;
538         }
539     }
540 }
541
542 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
543 sub load_template {
544     return &$template(@_);
545 }
546
547 # Define default entries subroutine
548 $entries = sub {
549     my ( %files, %indexes, %others );
550     find(
551         sub {
552             my $d;
553             my $curr_depth = $File::Find::dir =~ tr[/][];
554             return if $depth and $curr_depth > $depth;
555
556             if (
557
558                 # a match
559                 $File::Find::name
560                 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
561
562                 # not an index, .file, and is readable
563                 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
564                 )
565             {
566
567                 # read modification time
568                 my $mtime = stat($File::Find::name)->mtime or return;
569
570                 # to show or not to show future entries
571                 return unless ( $show_future_entries or $mtime < time );
572
573                 # add the file and its associated mtime to the list of files
574                 $files{$File::Find::name} = $mtime;
575
576                 # static rendering bits
577                 my $static_file
578                     = "$static_dir/$1/index." . $static_flavours[0];
579                 if (   param('-all')
580                     or !-f $static_file
581                     or stat($static_file)->mtime < $mtime )
582                 {
583                     $indexes{$1} = 1;
584                     $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
585                     $indexes{$d} = $d;
586                     $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
587                         if $static_entries;
588                 }
589             }
590
591             # not an entries match
592             elsif ( !-d $File::Find::name and -r $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 (    !$ENV{GATEWAY_INTERFACE}
619     and param('-password')
620     and $static_password
621     and param('-password') eq $static_password )
622 {
623
624     param('-quiet') or print "Blosxom is generating static index pages...\n";
625
626     # Home Page and Directory Indexes
627     my %done;
628     foreach my $path ( sort keys %indexes ) {
629         my $p = '';
630         foreach ( ( '', split /\//, $path ) ) {
631             $p .= "/$_";
632             $p =~ s!^/!!;
633             next if $done{$p}++;
634             mkdir "$static_dir/$p", 0755
635                 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
636             foreach $flavour (@static_flavours) {
637                 $content_type
638                     = ( &$template( $p, 'content_type', $flavour ) );
639                 $content_type =~ s!\n.*!!s;
640                 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
641                 param('-quiet') or print "$fn.$flavour\n";
642                 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
643                     or die "Couldn't open $static_dir/$p for writing: $!";
644                 $output = '';
645                 if ( $indexes{$path} == 1 ) {
646
647                     # category
648                     $path_info = $p;
649
650                     # individual story
651                     $path_info =~ s!\.$file_extension$!\.$flavour!;
652                     print $fh_w &generate( 'static', $path_info, '', $flavour,
653                         $content_type );
654                 }
655                 else {
656
657                     # date
658                     local (
659                         $path_info_yr, $path_info_mo,
660                         $path_info_da, $path_info
661                     ) = split /\//, $p, 4;
662                     unless ( defined $path_info ) { $path_info = "" }
663                     print $fh_w &generate( 'static', '', $p, $flavour,
664                         $content_type );
665                 }
666                 $fh_w->close;
667             }
668         }
669     }
670 }
671
672 # Dynamic
673 else {
674     $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
675     $content_type =~ s!\n.*!!s;
676
677     $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
678     $header = { -type => $content_type };
679
680     print generate( 'dynamic', $path_info,
681         "$path_info_yr/$path_info_mo_num/$path_info_da",
682         $flavour, $content_type );
683 }
684
685 # Plugins: End
686 foreach my $plugin (@plugins) {
687     if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
688         $entries = $plugin->end();
689     }
690 }
691
692 # Generate
693 sub generate {
694     my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
695         = @_;
696
697     %files = %$files;
698     %others = ref $others ? %$others : ();
699
700     # Plugins: Filter
701     foreach my $plugin (@plugins) {
702         if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
703             $entries = $plugin->filter( \%files, \%others );
704         }
705     }
706
707     my %f = %files;
708
709     # Plugins: Skip
710     # Allow plugins to decide if we can cut short story generation
711     my $skip;
712     foreach my $plugin (@plugins) {
713         if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
714             if ( my $tmp = $plugin->skip() ) {
715                 $skip = $tmp;
716                 last;
717             }
718         }
719     }
720
721     # Define default interpolation subroutine
722     $interpolate = sub {
723         package blosxom;
724         my $template = shift;
725         # Interpolate scalars, namespaced scalars, and hash/hashref scalars
726         $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{([\'\"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
727         return $template;
728     };
729
730     unless ( defined($skip) and $skip ) {
731
732         # Plugins: Interpolate
733         # Allow for the first encountered plugin::interpolate subroutine to
734         # override the default built-in interpolate subroutine
735         foreach my $plugin (@plugins) {
736             if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
737                 if ( my $tmp = $plugin->interpolate() ) {
738                     $interpolate = $tmp;
739                     last;
740                 }
741             }
742         }
743
744         # Head
745         my $head = ( &$template( $currentdir, 'head', $flavour ) );
746
747         # Plugins: Head
748         foreach my $plugin (@plugins) {
749             if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
750                 $entries = $plugin->head( $currentdir, \$head );
751             }
752         }
753
754         $head = &$interpolate($head);
755
756         $output .= $head;
757
758         # Stories
759         my $curdate = '';
760         my $ne      = $num_entries;
761
762         if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
763             $currentdir = "$1$2.$file_extension";
764             %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
765                 if $files{"$datadir/$currentdir"};
766         }
767         else {
768             $currentdir =~ s!/index\..+$!!;
769         }
770
771         # Define a default sort subroutine
772         my $sort = sub {
773             my ($files_ref) = @_;
774             return
775                 sort { $files_ref->{$b} <=> $files_ref->{$a} }
776                 keys %$files_ref;
777         };
778
779      # Plugins: Sort
780      # Allow for the first encountered plugin::sort subroutine to override the
781      # default built-in sort subroutine
782         foreach my $plugin (@plugins) {
783             if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
784                 if ( my $tmp = $plugin->sort() ) {
785                     $sort = $tmp;
786                     last;
787                 }
788             }
789         }
790
791         foreach my $path_file ( &$sort( \%f, \%others ) ) {
792             last if $ne <= 0 && $date !~ /\d/;
793             use vars qw/ $path $fn /;
794             ( $path, $fn )
795                 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
796
797             # Only stories in the right hierarchy
798             $path =~ /^$currentdir/
799                 or $path_file eq "$datadir/$currentdir"
800                 or next;
801
802             # Prepend a slash for use in templates only if a path exists
803             $path &&= "/$path";
804
805             # Date fiddling for by-{year,month,day} archive views
806             use vars
807                 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
808             ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
809                 = nice_date( $files{"$path_file"} );
810             ( $hr, $min ) = split /:/, $ti;
811             ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
812             $hr12 =~ s/^0//;
813             if ( $hr12 == 0 ) { $hr12 = 12 }
814
815             # Only stories from the right date
816             my ( $path_info_yr, $path_info_mo_num, $path_info_da )
817                 = split /\//, $date;
818             next if $path_info_yr     && $yr != $path_info_yr;
819             last if $path_info_yr     && $yr < $path_info_yr;
820             next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
821             next if $path_info_da     && $da != $path_info_da;
822             last if $path_info_da     && $da < $path_info_da;
823
824             # Date
825             my $date = ( &$template( $path, 'date', $flavour ) );
826
827             # Plugins: Date
828             foreach my $plugin (@plugins) {
829                 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
830                     $entries
831                         = $plugin->date( $currentdir, \$date,
832                         $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
833                         $yr );
834                 }
835             }
836
837             $date = &$interpolate($date);
838
839             if ( $date && $curdate ne $date ) {
840                 $curdate = $date;
841                 $output .= $date;
842             }
843
844             use vars qw/ $title $body $raw /;
845             if ( -f "$path_file" && $fh->open("< $path_file") ) {
846                 chomp( $title = <$fh> );
847                 chomp( $body = join '', <$fh> );
848                 $fh->close;
849                 $raw = "$title\n$body";
850             }
851             my $story = ( &$template( $path, 'story', $flavour ) );
852
853             # Plugins: Story
854             foreach my $plugin (@plugins) {
855                 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
856                     $entries = $plugin->story( $path, $fn, \$story, \$title,
857                         \$body );
858                 }
859             }
860
861             # Save unescaped versions and allow them to be used in
862             # flavour templates.
863             use vars qw/$url_unesc $path_unesc $fn_unesc/;
864             $url_unesc  = $url;
865             $path_unesc = $path;
866             $fn_unesc   = $fn;
867
868             # Fix special characters in links inside XML content
869             if ( $encode_xml_entities &&
870                  $content_type =~ m{\bxml\b} &&
871                  $content_type !~ m{\bxhtml\b} ) {
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