Refactoring usage of $url_escape_re into a function named url_escape_url_path_and_fn.
[blosxom.git] / blosxom.cgi
blob590e0f2b4adfd76dc2f0220daecd96449266bfb7
1 #!/usr/bin/perl
3 # Blosxom
4 # Author: Rael Dornfest (2002-2003), The Blosxom Development Team (2005-2009)
5 # Version: 2.1.2 ($Id: blosxom.cgi,v 1.98 2009/07/19 17:18:37 xtaran Exp $)
6 # Home/Docs/Licensing: http://blosxom.sourceforge.net/
7 # Development/Downloads: http://sourceforge.net/projects/blosxom
9 package blosxom;
11 =head1 NAME
13 blosxom - A lightweight yet feature-packed weblog
15 =head1 SYNOPSIS
17 B<blosxom> is a simple web log (blog) CGI script written in perl.
19 =head1 DESCRIPTION
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.
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.
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.
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.
42 =head1 USAGE
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:
49 First Blosxom Post!
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>.
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.
59 =cut
61 # --- Configurable variables -----
63 # What's this blog's title?
64 $blog_title = "My Weblog";
66 # What's this blog's description (for outgoing RSS feed)?
67 $blog_description = "Yet another Blosxom weblog.";
69 # What's this blog's primary language (for outgoing RSS feed)?
70 $blog_language = "en";
72 # What's this blog's text encoding ?
73 $blog_encoding = "UTF-8";
75 # Where are this blog's entries kept?
76 $datadir = "/Library/WebServer/Documents/blosxom";
78 # What's my preferred base URL for this blog (leave blank for
79 # automatic)?
80 $url = "";
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?
85 # 0 = infinite depth (aka grab everything), 1 = datadir only,
86 # n = n levels down
88 $depth = 0;
90 # How many entries should I show on the home page?
91 $num_entries = 40;
93 # What file extension signifies a blosxom entry?
94 $file_extension = "txt";
96 # What is the default flavour?
97 $default_flavour = "html";
99 # Should I show entries from the future (i.e. dated after now)?
100 $show_future_entries = 0;
102 # --- Plugins (Optional) -----
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 = "";
108 # Where are my plugins kept?
109 $plugin_dir = "";
111 # Where should my plugins keep their state information?
112 $plugin_state_dir = "$plugin_dir/state";
114 # Additional plugins location. A list of directories, separated by ';'
115 # on windows, ':' everywhere else.
116 $plugin_path = "";
118 # --- Static Rendering -----
120 # Where are this blog's static files to be created?
121 $static_dir = "/Library/WebServer/Documents/blog";
123 # What's my administrative password (you must set this for static
124 # rendering)?
125 $static_password = "";
127 # What flavours should I generate statically?
128 @static_flavours = qw/html rss/;
130 # Should I statically generate individual entries?
131 # 0 = no, 1 = yes
132 $static_entries = 0;
134 # --- Advanced Encoding Options -----
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;
140 # Should I encode 8 bit special characters, e.g. umlauts in URLs, e.g.
141 # convert an ISO-Latin-1 \"o to %F6? (off by default for now; plugins
142 # can change this, too)
143 $encode_8bit_chars = 0;
145 # RegExp matching all characters which should be URL encoded in links.
146 # Defaults to anything but numbers, letters, slash, colon, dash,
147 # underscore and dot.
148 $url_escape_re = qr([^-/a-zA-Z0-9:._]);
150 # --------------------------------
152 =head1 ENVIRONMENT
154 =over
156 =item B<BLOSXOM_CONFIG_FILE>
158 Points to the location of the configuration file. This will be
159 considered as first option, if it's set.
162 =item B<BLOSXOM_CONFIG_DIR>
164 The here named directory will be tried unless the above mentioned
165 environment variable is set and tested for a contained blosxom.conf
166 file.
169 =back
172 =head1 FILES
174 =over
176 =item B</usr/lib/cgi-bin/blosxom>
178 The CGI script itself. Please note that the location might depend on
179 your installation.
181 =item B</etc/blosxom/blosxom.conf>
183 The default configuration file location. This is rather taken as last
184 ressort if no other configuration location is set through environment
185 variables.
187 =back
190 =head1 AUTHOR
192 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
193 development was picked up by a team of dedicated users of blosxom since
194 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
196 =cut
199 use vars qw!
200 $version
201 $blog_title
202 $blog_description
203 $blog_language
204 $blog_encoding
205 $datadir
206 $url
207 %template
208 $template
209 $depth
210 $num_entries
211 $file_extension
212 $default_flavour
213 $static_or_dynamic
214 $config_dir
215 $plugin_list
216 $plugin_path
217 $plugin_dir
218 $plugin_state_dir
219 @plugins
220 %plugins
221 $static_dir
222 $static_password
223 @static_flavours
224 $static_entries
225 $path_info_full
226 $path_info
227 $path_info_yr
228 $path_info_mo
229 $path_info_da
230 $path_info_mo_num
231 $flavour
232 $static_or_dynamic
233 %month2num
234 @num2month
235 $interpolate
236 $entries
237 $output
238 $header
239 $show_future_entries
240 %files
241 %indexes
242 %others
243 $encode_xml_entities
244 $encode_8bit_chars
245 $url_escape_re
246 $content_type
249 use strict;
250 use FileHandle;
251 use File::Find;
252 use File::stat;
253 use Time::Local;
254 use CGI qw/:standard :netscape/;
256 $version = "2.1.2+dev";
258 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
259 my $blosxom_config;
260 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
261 $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
262 ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
264 else {
265 for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
266 '/etc' )
268 if ( -r "$blosxom_config_dir/blosxom.conf" ) {
269 $config_dir = $blosxom_config_dir;
270 $blosxom_config = "$blosxom_config_dir/blosxom.conf";
271 last;
276 # Load $blosxom_config
277 if ($blosxom_config) {
278 if ( -r $blosxom_config ) {
279 eval { require $blosxom_config }
280 or warn "Error reading blosxom config file '$blosxom_config'"
281 . ( $@ ? ": $@" : '' );
283 else {
284 warn "Cannot find or read blosxom config file '$blosxom_config'";
288 my $fh = new FileHandle;
290 %month2num = (
291 nil => '00',
292 Jan => '01',
293 Feb => '02',
294 Mar => '03',
295 Apr => '04',
296 May => '05',
297 Jun => '06',
298 Jul => '07',
299 Aug => '08',
300 Sep => '09',
301 Oct => '10',
302 Nov => '11',
303 Dec => '12'
305 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
307 # Use the stated preferred URL or figure it out automatically. Set
308 # $url manually in the config section above if CGI.pm doesn't guess
309 # the base URL correctly, e.g. when called from a Server Side Includes
310 # document or so.
311 unless ($url) {
312 $url = url();
314 # Unescape %XX hex codes (from URI::Escape::uri_unescape)
315 $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
317 # Support being called from inside a SSI document
318 $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
320 # Remove PATH_INFO if it is set but not removed by CGI.pm. This
321 # seems to happen when used with Apache's Alias directive or if
322 # called from inside a Server Side Include document. If that
323 # doesn't help either, set $url manually in the configuration.
324 $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
326 # NOTE:
328 # There is one case where this code does more than necessary, too:
329 # If the URL requested is e.g. http://example.org/blog/blog and
330 # the base URL is correctly determined as http://example.org/blog
331 # by CGI.pm, then this code will incorrectly normalize the base
332 # URL down to http://example.org, because the same string as
333 # PATH_INFO is part of the base URL, too. But this is such a
334 # seldom case and can be fixed by setting $url in the config file,
335 # too.
338 # The only modification done to a manually set base URL is to strip
339 # a trailing slash if present.
341 $url =~ s!/$!!;
343 # Drop ending any / from dir settings
344 $datadir =~ s!/$!!;
345 $plugin_dir =~ s!/$!!;
346 $static_dir =~ s!/$!!;
348 # Fix depth to take into account datadir's path
349 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
351 if ( !$ENV{GATEWAY_INTERFACE}
352 and param('-password')
353 and $static_password
354 and param('-password') eq $static_password )
356 $static_or_dynamic = 'static';
358 else {
359 $static_or_dynamic = 'dynamic';
360 param( -name => '-quiet', -value => 1 );
363 # Path Info Magic
364 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
365 my @path_info = split m{/}, path_info() || param('path');
366 $path_info_full = join '/', @path_info; # Equivalent to $ENV{PATH_INFO}
367 shift @path_info;
369 # Flavour specified by ?flav={flav} or index.{flav}
370 $flavour = '';
371 if (! ($flavour = param('flav'))) {
372 if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
373 $flavour = $2;
374 pop @path_info if $1 eq 'index';
377 $flavour ||= $default_flavour;
379 # Fix XSS in flavour name (CVE-2008-2236)
380 $flavour = blosxom_html_escape($flavour);
382 sub blosxom_html_escape {
383 my $string = shift;
384 my %escape = (
385 '<' => '&lt;',
386 '>' => '&gt;',
387 '&' => '&amp;',
388 '"' => '&quot;',
389 "'" => '&apos;'
391 my $escape_re = join '|' => keys %escape;
392 $string =~ s/($escape_re)/$escape{$1}/g;
393 $string;
396 # Global variable to be used in head/foot.{flavour} templates
397 $path_info = '';
398 # Add all @path_info elements to $path_info till we come to one that could be a year
399 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
400 $path_info .= '/' . shift @path_info;
403 # Pull date elements out of path
404 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
405 $path_info_yr = shift @path_info;
406 if ($path_info[0] &&
407 ($path_info[0] =~ /^(0\d|1[012])$/ ||
408 exists $month2num{ ucfirst lc $path_info_mo })) {
409 $path_info_mo = shift @path_info;
410 # Map path_info_mo to numeric $path_info_mo_num
411 $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
412 ? $path_info_mo
413 : $month2num{ ucfirst lc $path_info_mo };
414 if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
415 $path_info_da = shift @path_info;
420 # Add remaining path elements to $path_info
421 $path_info .= '/' . join('/', @path_info);
423 # Strip spurious slashes
424 $path_info =~ s!(^/*)|(/*$)!!g;
426 # Define standard template subroutine, plugin-overridable at Plugins: Template
427 $template = sub {
428 my ( $path, $chunk, $flavour ) = @_;
430 do {
431 return join '', <$fh>
432 if $fh->open("< $datadir/$path/$chunk.$flavour");
433 } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
435 # Check for definedness, since flavour can be the empty string
436 if ( defined $template{$flavour}{$chunk} ) {
437 return $template{$flavour}{$chunk};
439 elsif ( defined $template{error}{$chunk} ) {
440 return $template{error}{$chunk};
442 else {
443 return '';
447 # Bring in the templates
448 %template = ();
449 while (<DATA>) {
450 last if /^(__END__)$/;
451 my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
452 $txt =~ s/\\n/\n/mg;
453 $template{$ct}{$comp} .= $txt . "\n";
456 # Plugins: Start
457 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
458 my @plugin_dirs = split /$path_sep/, $plugin_path;
459 unshift @plugin_dirs, $plugin_dir;
460 my @plugin_list = ();
461 my %plugin_hash = ();
463 # If $plugin_list is set, read plugins to use from that file
464 if ( $plugin_list ) {
465 if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
466 @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
467 $fh->close;
469 else {
470 warn "unable to read or open plugin_list '$plugin_list': $!";
471 $plugin_list = '';
475 # Otherwise walk @plugin_dirs to get list of plugins to use
476 if ( ! @plugin_list && @plugin_dirs ) {
477 for my $plugin_dir (@plugin_dirs) {
478 next unless -d $plugin_dir;
479 if ( opendir PLUGINS, $plugin_dir ) {
480 for my $plugin (
481 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
482 readdir(PLUGINS) )
485 # Ignore duplicates
486 next if $plugin_hash{$plugin};
488 # Add to @plugin_list and %plugin_hash
489 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
490 push @plugin_list, $plugin;
492 closedir PLUGINS;
495 @plugin_list = sort @plugin_list;
498 # Load all plugins in @plugin_list
499 unshift @INC, @plugin_dirs;
500 foreach my $plugin (@plugin_list) {
501 my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
502 my $plugin_file = $plugin_list ? $plugin_name : $plugin;
503 my $on_off = $off eq '_' ? -1 : 1;
505 # Allow perl module plugins
506 # The -z test is a hack to allow a zero-length placeholder file in a
507 # $plugin_path directory to indicate an @INC module should be loaded
508 if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
510 # For Blosxom::Plugin::Foo style plugins, we need to use a string require
511 eval "require $plugin_file";
513 else
514 { # we try first to load from $plugin_dir before attempting from $plugin_path
515 eval { require "$plugin_dir/$plugin_file" }
516 or eval { require $plugin_file };
519 if ($@) {
520 warn "error finding or loading blosxom plugin '$plugin_name': $@";
521 next;
523 if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
524 push @plugins, $plugin_name;
528 shift @INC foreach @plugin_dirs;
530 # Plugins: Template
531 # Allow for the first encountered plugin::template subroutine to override the
532 # default built-in template subroutine
533 foreach my $plugin (@plugins) {
534 if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
535 if ( my $tmp = $plugin->template() ) {
536 $template = $tmp;
537 last;
542 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
543 sub load_template {
544 return &$template(@_);
547 # Define default entries subroutine
548 $entries = sub {
549 my ( %files, %indexes, %others );
550 find(
551 sub {
552 my $d;
553 my $curr_depth = $File::Find::dir =~ tr[/][];
554 return if $depth and $curr_depth > $depth;
556 if (
558 # a match
559 $File::Find::name
560 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
562 # not an index, .file, and is readable
563 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
567 # read modification time
568 my $mtime = stat($File::Find::name)->mtime or return;
570 # to show or not to show future entries
571 return unless ( $show_future_entries or $mtime < time );
573 # add the file and its associated mtime to the list of files
574 $files{$File::Find::name} = $mtime;
576 # static rendering bits
577 my $static_file
578 = "$static_dir/$1/index." . $static_flavours[0];
579 if ( param('-all')
580 or !-f $static_file
581 or stat($static_file)->mtime < $mtime )
583 $indexes{$1} = 1;
584 $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
585 $indexes{$d} = $d;
586 $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
587 if $static_entries;
591 # not an entries match
592 elsif ( !-d $File::Find::name and -r $File::Find::name ) {
593 $others{$File::Find::name} = stat($File::Find::name)->mtime;
596 $datadir
599 return ( \%files, \%indexes, \%others );
602 # Plugins: Entries
603 # Allow for the first encountered plugin::entries subroutine to override the
604 # default built-in entries subroutine
605 foreach my $plugin (@plugins) {
606 if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
607 if ( my $tmp = $plugin->entries() ) {
608 $entries = $tmp;
609 last;
614 my ( $files, $indexes, $others ) = &$entries();
615 %indexes = %$indexes;
617 # Static
618 if ( !$ENV{GATEWAY_INTERFACE}
619 and param('-password')
620 and $static_password
621 and param('-password') eq $static_password )
624 param('-quiet') or print "Blosxom is generating static index pages...\n";
626 # Home Page and Directory Indexes
627 my %done;
628 foreach my $path ( sort keys %indexes ) {
629 my $p = '';
630 foreach ( ( '', split /\//, $path ) ) {
631 $p .= "/$_";
632 $p =~ s!^/!!;
633 next if $done{$p}++;
634 mkdir "$static_dir/$p", 0755
635 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
636 foreach $flavour (@static_flavours) {
637 $content_type
638 = ( &$template( $p, 'content_type', $flavour ) );
639 $content_type =~ s!\n.*!!s;
640 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
641 param('-quiet') or print "$fn.$flavour\n";
642 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
643 or die "Couldn't open $static_dir/$p for writing: $!";
644 $output = '';
645 if ( $indexes{$path} == 1 ) {
647 # category
648 $path_info = $p;
650 # individual story
651 $path_info =~ s!\.$file_extension$!\.$flavour!;
652 print $fh_w &generate( 'static', $path_info, '', $flavour,
653 $content_type );
655 else {
657 # date
658 local (
659 $path_info_yr, $path_info_mo,
660 $path_info_da, $path_info
661 ) = split /\//, $p, 4;
662 unless ( defined $path_info ) { $path_info = "" }
663 print $fh_w &generate( 'static', '', $p, $flavour,
664 $content_type );
666 $fh_w->close;
672 # Dynamic
673 else {
674 $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
675 $content_type =~ s!\n.*!!s;
677 $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
678 $header = { -type => $content_type };
680 print generate( 'dynamic', $path_info,
681 "$path_info_yr/$path_info_mo_num/$path_info_da",
682 $flavour, $content_type );
685 # Plugins: End
686 foreach my $plugin (@plugins) {
687 if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
688 $entries = $plugin->end();
692 # Generate
693 sub generate {
694 my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
695 = @_;
697 %files = %$files;
698 %others = ref $others ? %$others : ();
700 # Plugins: Filter
701 foreach my $plugin (@plugins) {
702 if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
703 $entries = $plugin->filter( \%files, \%others );
707 my %f = %files;
709 # Plugins: Skip
710 # Allow plugins to decide if we can cut short story generation
711 my $skip;
712 foreach my $plugin (@plugins) {
713 if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
714 if ( my $tmp = $plugin->skip() ) {
715 $skip = $tmp;
716 last;
721 # Define default interpolation subroutine
722 $interpolate = sub {
723 package blosxom;
724 my $template = shift;
725 # Interpolate scalars, namespaced scalars, and hash/hashref scalars
726 $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{([\'\"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
727 return $template;
730 unless ( defined($skip) and $skip ) {
732 # Plugins: Interpolate
733 # Allow for the first encountered plugin::interpolate subroutine to
734 # override the default built-in interpolate subroutine
735 foreach my $plugin (@plugins) {
736 if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
737 if ( my $tmp = $plugin->interpolate() ) {
738 $interpolate = $tmp;
739 last;
744 # Head
745 my $head = ( &$template( $currentdir, 'head', $flavour ) );
747 # Plugins: Head
748 foreach my $plugin (@plugins) {
749 if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
750 $entries = $plugin->head( $currentdir, \$head );
754 $head = &$interpolate($head);
756 $output .= $head;
758 # Stories
759 my $curdate = '';
760 my $ne = $num_entries;
762 if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
763 $currentdir = "$1$2.$file_extension";
764 %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
765 if $files{"$datadir/$currentdir"};
767 else {
768 $currentdir =~ s!/index\..+$!!;
771 # Define a default sort subroutine
772 my $sort = sub {
773 my ($files_ref) = @_;
774 return
775 sort { $files_ref->{$b} <=> $files_ref->{$a} }
776 keys %$files_ref;
779 # Plugins: Sort
780 # Allow for the first encountered plugin::sort subroutine to override the
781 # default built-in sort subroutine
782 foreach my $plugin (@plugins) {
783 if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
784 if ( my $tmp = $plugin->sort() ) {
785 $sort = $tmp;
786 last;
791 foreach my $path_file ( &$sort( \%f, \%others ) ) {
792 last if $ne <= 0 && $date !~ /\d/;
793 use vars qw/ $path $fn /;
794 ( $path, $fn )
795 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
797 # Only stories in the right hierarchy
798 $path =~ /^$currentdir/
799 or $path_file eq "$datadir/$currentdir"
800 or next;
802 # Prepend a slash for use in templates only if a path exists
803 $path &&= "/$path";
805 # Date fiddling for by-{year,month,day} archive views
806 use vars
807 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
808 ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
809 = nice_date( $files{"$path_file"} );
810 ( $hr, $min ) = split /:/, $ti;
811 ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
812 $hr12 =~ s/^0//;
813 if ( $hr12 == 0 ) { $hr12 = 12 }
815 # Only stories from the right date
816 my ( $path_info_yr, $path_info_mo_num, $path_info_da )
817 = split /\//, $date;
818 next if $path_info_yr && $yr != $path_info_yr;
819 last if $path_info_yr && $yr < $path_info_yr;
820 next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
821 next if $path_info_da && $da != $path_info_da;
822 last if $path_info_da && $da < $path_info_da;
824 # Date
825 my $date = ( &$template( $path, 'date', $flavour ) );
827 # Plugins: Date
828 foreach my $plugin (@plugins) {
829 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
830 $entries
831 = $plugin->date( $currentdir, \$date,
832 $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
833 $yr );
837 $date = &$interpolate($date);
839 if ( $date && $curdate ne $date ) {
840 $curdate = $date;
841 $output .= $date;
844 use vars qw/ $title $body $raw /;
845 if ( -f "$path_file" && $fh->open("< $path_file") ) {
846 chomp( $title = <$fh> );
847 chomp( $body = join '', <$fh> );
848 $fh->close;
849 $raw = "$title\n$body";
851 my $story = ( &$template( $path, 'story', $flavour ) );
853 # Plugins: Story
854 foreach my $plugin (@plugins) {
855 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
856 $entries = $plugin->story( $path, $fn, \$story, \$title,
857 \$body );
861 # Save unescaped versions and allow them to be used in
862 # flavour templates.
863 use vars qw/$url_unesc $path_unesc $fn_unesc/;
864 $url_unesc = $url;
865 $path_unesc = $path;
866 $fn_unesc = $fn;
868 # Fix special characters in links inside XML content
869 if ( $encode_xml_entities &&
870 $content_type =~ m{\bxml\b} &&
871 $content_type !~ m{\bxhtml\b} ) {
872 # Escape special characters inside the <link> container
874 &url_escape_url_path_and_fn();
876 # Escape <, >, and &, and to produce valid RSS
877 $title = blosxom_html_escape($title);
878 $body = blosxom_html_escape($body);
879 $url = blosxom_html_escape($url);
880 $path = blosxom_html_escape($path);
881 $fn = blosxom_html_escape($fn);
884 # Fix special characters in links inside XML content
885 if ($encode_8bit_chars) {
886 &url_escape_url_path_and_fn();
889 $story = &$interpolate($story);
891 $output .= $story;
892 $fh->close;
894 $ne--;
897 # Foot
898 my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
900 # Plugins: Foot
901 foreach my $plugin (@plugins) {
902 if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
903 $entries = $plugin->foot( $currentdir, \$foot );
907 $foot = &$interpolate($foot);
908 $output .= $foot;
910 # Plugins: Last
911 foreach my $plugin (@plugins) {
912 if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
913 $entries = $plugin->last();
917 } # End skip
919 # Finally, add the header, if any and running dynamically
920 $output = header($header) . $output
921 if ( $static_or_dynamic eq 'dynamic' and $header );
923 $output;
926 sub nice_date {
927 my ($unixtime) = @_;
929 my $c_time = CORE::localtime($unixtime);
930 my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
931 = ( $c_time
932 =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
934 $ti = "$hr:$min";
935 $da = sprintf( "%02d", $da );
936 my $mo_num = $month2num{$mo};
938 my $offset
939 = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
940 my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
941 . sprintf( "%02d", ( $offset % 3600 ) / 60 );
943 return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
946 sub url_escape_url_path_and_fn {
947 $url =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
948 $path =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
949 $fn =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
952 # Default HTML and RSS template bits
953 __DATA__
954 html content_type text/html; charset=$blog_encoding
956 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
957 html head <html>
958 html head <head>
959 html head <meta http-equiv="content-type" content="$content_type" >
960 html head <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
961 html head <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
962 html head </head>
963 html head <body>
964 html head <div align="center">
965 html head <h1>$blog_title</h1>
966 html head <p>$path_info_da $path_info_mo $path_info_yr</p>
967 html head </div>
969 html story <div>
970 html story <h3><a name="$fn">$title</a></h3>
971 html story <div>$body</div>
972 html story <p>posted at: $ti | path: <a href="$url$path">$path</a> | <a href="$url/$yr/$mo_num/$da#$fn">permanent link to this entry</a></p>
973 html story </div>
975 html date <h2>$dw, $da $mo $yr</h2>
977 html foot
978 html foot <div align="center">
979 html foot <a href="http://blosxom.sourceforge.net/"><img src="http://blosxom.sourceforge.net/images/pb_blosxom.gif" alt="powered by blosxom" border="0" width="90" height="33" ></a>
980 html foot </div>
981 html foot </body>
982 html foot </html>
984 rss content_type text/xml; charset=$blog_encoding
986 rss head <?xml version="1.0" encoding="$blog_encoding"?>
987 rss head <rss version="2.0">
988 rss head <channel>
989 rss head <title>$blog_title</title>
990 rss head <link>$url/$path_info</link>
991 rss head <description>$blog_description</description>
992 rss head <language>$blog_language</language>
993 rss head <docs>http://blogs.law.harvard.edu/tech/rss</docs>
994 rss head <generator>blosxom/$version</generator>
996 rss story <item>
997 rss story <title>$title</title>
998 rss story <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
999 rss story <link>$url/$yr/$mo_num/$da#$fn</link>
1000 rss story <category>$path</category>
1001 rss story <guid isPermaLink="false">$url$path/$fn</guid>
1002 rss story <description>$body</description>
1003 rss story </item>
1005 rss date
1007 rss foot </channel>
1008 rss foot </rss>
1010 error content_type text/html
1012 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
1013 error head <html>
1014 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
1015 error head <body>
1016 error head <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
1017 error head <p>I'm afraid this is the first I've heard of a "$flavour" flavoured Blosxom. Try dropping the "/+$flavour" bit from the end of the URL.</p>
1019 error story <h3>$title</h3>
1020 error story <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
1022 error date <h2>$dw, $da $mo $yr</h2>
1024 error foot </body>
1025 error foot </html>
1026 __END__