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