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