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