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