4 # Author: Rael Dornfest (2002-2003), The Blosxom Development Team (2005-2008)
5 # Version: 2.1.2 ($Id: blosxom.cgi,v 1.93 2009/03/08 01:14:35 xtaran Exp $)
6 # Home/Docs/Licensing: http://blosxom.sourceforge.net/
7 # Development/Downloads: http://sourceforge.net/projects/blosxom
13 blosxom - A lightweight yet feature-packed weblog
17 B<blosxom> is a simple web log (blog) CGI script written in perl.
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
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
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.
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
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,
90 # How many entries should I show on the home page?
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)
108 # Where are my plugins kept?
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.
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
125 $static_password = "";
127 # What flavours should I generate statically?
128 @static_flavours = qw/html rss/;
130 # Should I statically generate individual entries?
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 # --------------------------------
151 =item B<BLOSXOM_CONFIG_FILE>
153 Points to the location of the configuration file. This will be
154 considered as first option, if it's set.
157 =item B<BLOSXOM_CONFIG_DIR>
159 The here named directory will be tried unless the above mentioned
160 environment variable is set and tested for a contained blosxom.conf
171 =item B</usr/lib/cgi-bin/blosxom>
173 The CGI script itself. Please note that the location might depend on
176 =item B</etc/blosxom/blosxom.conf>
178 The default configuration file location. This is rather taken as last
179 ressort if no other configuration location is set through environment
187 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
188 development was picked up by a team of dedicated users of blosxom since
189 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
195 qw! $version $blog_title $blog_description $blog_language $blog_encoding $datadir $url %template $template $depth $num_entries $file_extension $default_flavour $static_or_dynamic $config_dir $plugin_list $plugin_path $plugin_dir $plugin_state_dir @plugins %plugins $static_dir $static_password @static_flavours $static_entries $path_info_full $path_info $path_info_yr $path_info_mo $path_info_da $path_info_mo_num $flavour $static_or_dynamic %month2num @num2month $interpolate $entries $output $header $show_future_entries %files %indexes %others $encode_xml_entities $encode_8bit_chars $content_type !;
202 use CGI qw/:standard :netscape/;
204 $version = "2.1.2+dev";
206 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
208 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
209 $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
210 ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
213 for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
216 if ( -r "$blosxom_config_dir/blosxom.conf" ) {
217 $config_dir = $blosxom_config_dir;
218 $blosxom_config = "$blosxom_config_dir/blosxom.conf";
224 # Load $blosxom_config
225 if ($blosxom_config) {
226 if ( -r $blosxom_config ) {
227 eval { require $blosxom_config }
228 or warn "Error reading blosxom config file '$blosxom_config'"
229 . ( $@ ? ": $@" : '' );
232 warn "Cannot find or read blosxom config file '$blosxom_config'";
236 my $fh = new FileHandle;
253 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
255 # Use the stated preferred URL or figure it out automatically. Set
256 # $url manually in the config section above if CGI.pm doesn't guess
257 # the base URL correctly, e.g. when called from a Server Side Includes
262 # Unescape %XX hex codes (from URI::Escape::uri_unescape)
263 $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
265 # Support being called from inside a SSI document
266 $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
268 # Remove PATH_INFO if it is set but not removed by CGI.pm. This
269 # seems to happen when used with Apache's Alias directive or if
270 # called from inside a Server Side Include document. If that
271 # doesn't help either, set $url manually in the configuration.
272 $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
276 # There is one case where this code does more than necessary, too:
277 # If the URL requested is e.g. http://example.org/blog/blog and
278 # the base URL is correctly determined as http://example.org/blog
279 # by CGI.pm, then this code will incorrectly normalize the base
280 # URL down to http://example.org, because the same string as
281 # PATH_INFO is part of the base URL, too. But this is such a
282 # seldom case and can be fixed by setting $url in the config file,
286 # The only modification done to a manually set base URL is to strip
287 # a trailing slash if present.
291 # Drop ending any / from dir settings
293 $plugin_dir =~ s!/$!!;
294 $static_dir =~ s!/$!!;
296 # Fix depth to take into account datadir's path
297 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
299 if ( !$ENV{GATEWAY_INTERFACE}
300 and param('-password')
302 and param('-password') eq $static_password )
304 $static_or_dynamic = 'static';
307 $static_or_dynamic = 'dynamic';
308 param( -name => '-quiet', -value => 1 );
312 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
313 my @path_info = split m{/}, path_info() || param('path');
314 $path_info_full = join '/', @path_info; # Equivalent to $ENV{PATH_INFO}
317 # Flavour specified by ?flav={flav} or index.{flav}
319 if (! ($flavour = param('flav'))) {
320 if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
322 pop @path_info if $1 eq 'index';
325 $flavour ||= $default_flavour;
327 # Fix XSS in flavour name (CVE-2008-2236)
328 $flavour = blosxom_html_escape($flavour);
330 sub blosxom_html_escape {
339 my $escape_re = join '|' => keys %escape;
340 $string =~ s/($escape_re)/$escape{$1}/g;
344 # Global variable to be used in head/foot.{flavour} templates
346 # Add all @path_info elements to $path_info till we come to one that could be a year
347 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
348 $path_info .= '/' . shift @path_info;
351 # Pull date elements out of path
352 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
353 $path_info_yr = shift @path_info;
355 ($path_info[0] =~ /^(0\d|1[012])$/ ||
356 exists $month2num{ ucfirst lc $path_info_mo })) {
357 $path_info_mo = shift @path_info;
358 # Map path_info_mo to numeric $path_info_mo_num
359 $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
361 : $month2num{ ucfirst lc $path_info_mo };
362 if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
363 $path_info_da = shift @path_info;
368 # Add remaining path elements to $path_info
369 $path_info .= '/' . join('/', @path_info);
371 # Strip spurious slashes
372 $path_info =~ s!(^/*)|(/*$)!!g;
374 # Define standard template subroutine, plugin-overridable at Plugins: Template
376 my ( $path, $chunk, $flavour ) = @_;
379 return join '', <$fh>
380 if $fh->open("< $datadir/$path/$chunk.$flavour");
381 } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
383 # Check for definedness, since flavour can be the empty string
384 if ( defined $template{$flavour}{$chunk} ) {
385 return $template{$flavour}{$chunk};
387 elsif ( defined $template{error}{$chunk} ) {
388 return $template{error}{$chunk};
395 # Bring in the templates
398 last if /^(__END__)$/;
399 my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
401 $template{$ct}{$comp} .= $txt . "\n";
405 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
406 my @plugin_dirs = split /$path_sep/, $plugin_path;
407 unshift @plugin_dirs, $plugin_dir;
408 my @plugin_list = ();
409 my %plugin_hash = ();
411 # If $plugin_list is set, read plugins to use from that file
412 if ( $plugin_list ) {
413 if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
414 @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
418 warn "unable to read or open plugin_list '$plugin_list': $!";
423 # Otherwise walk @plugin_dirs to get list of plugins to use
424 if ( ! @plugin_list && @plugin_dirs ) {
425 for my $plugin_dir (@plugin_dirs) {
426 next unless -d $plugin_dir;
427 if ( opendir PLUGINS, $plugin_dir ) {
429 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
434 next if $plugin_hash{$plugin};
436 # Add to @plugin_list and %plugin_hash
437 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
438 push @plugin_list, $plugin;
443 @plugin_list = sort @plugin_list;
446 # Load all plugins in @plugin_list
447 unshift @INC, @plugin_dirs;
448 foreach my $plugin (@plugin_list) {
449 my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
450 my $plugin_file = $plugin_list ? $plugin_name : $plugin;
451 my $on_off = $off eq '_' ? -1 : 1;
453 # Allow perl module plugins
454 # The -z test is a hack to allow a zero-length placeholder file in a
455 # $plugin_path directory to indicate an @INC module should be loaded
456 if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
458 # For Blosxom::Plugin::Foo style plugins, we need to use a string require
459 eval "require $plugin_file";
462 { # we try first to load from $plugin_dir before attempting from $plugin_path
463 eval { require "$plugin_dir/$plugin_file" }
464 or eval { require $plugin_file };
468 warn "error finding or loading blosxom plugin '$plugin_name': $@";
471 if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
472 push @plugins, $plugin_name;
476 shift @INC foreach @plugin_dirs;
479 # Allow for the first encountered plugin::template subroutine to override the
480 # default built-in template subroutine
481 foreach my $plugin (@plugins) {
482 if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
483 if ( my $tmp = $plugin->template() ) {
490 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
492 return &$template(@_);
495 # Define default entries subroutine
497 my ( %files, %indexes, %others );
501 my $curr_depth = $File::Find::dir =~ tr[/][];
502 return if $depth and $curr_depth > $depth;
508 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
510 # not an index, .file, and is readable
511 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
515 # read modification time
516 my $mtime = stat($File::Find::name)->mtime or return;
518 # to show or not to show future entries
519 return unless ( $show_future_entries or $mtime < time );
521 # add the file and its associated mtime to the list of files
522 $files{$File::Find::name} = $mtime;
524 # static rendering bits
526 = "$static_dir/$1/index." . $static_flavours[0];
529 or stat($static_file)->mtime < $mtime )
532 $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
534 $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
539 # not an entries match
540 elsif ( !-d $File::Find::name and -r $File::Find::name ) {
541 $others{$File::Find::name} = stat($File::Find::name)->mtime;
547 return ( \%files, \%indexes, \%others );
551 # Allow for the first encountered plugin::entries subroutine to override the
552 # default built-in entries subroutine
553 foreach my $plugin (@plugins) {
554 if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
555 if ( my $tmp = $plugin->entries() ) {
562 my ( $files, $indexes, $others ) = &$entries();
563 %indexes = %$indexes;
566 if ( !$ENV{GATEWAY_INTERFACE}
567 and param('-password')
569 and param('-password') eq $static_password )
572 param('-quiet') or print "Blosxom is generating static index pages...\n";
574 # Home Page and Directory Indexes
576 foreach my $path ( sort keys %indexes ) {
578 foreach ( ( '', split /\//, $path ) ) {
582 mkdir "$static_dir/$p", 0755
583 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
584 foreach $flavour (@static_flavours) {
586 = ( &$template( $p, 'content_type', $flavour ) );
587 $content_type =~ s!\n.*!!s;
588 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
589 param('-quiet') or print "$fn.$flavour\n";
590 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
591 or die "Couldn't open $static_dir/$p for writing: $!";
593 if ( $indexes{$path} == 1 ) {
599 $path_info =~ s!\.$file_extension$!\.$flavour!;
600 print $fh_w &generate( 'static', $path_info, '', $flavour,
607 $path_info_yr, $path_info_mo,
608 $path_info_da, $path_info
609 ) = split /\//, $p, 4;
610 unless ( defined $path_info ) { $path_info = "" }
611 print $fh_w &generate( 'static', '', $p, $flavour,
622 $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
623 $content_type =~ s!\n.*!!s;
625 $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
626 $header = { -type => $content_type };
628 print generate( 'dynamic', $path_info,
629 "$path_info_yr/$path_info_mo_num/$path_info_da",
630 $flavour, $content_type );
634 foreach my $plugin (@plugins) {
635 if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
636 $entries = $plugin->end();
642 my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
646 %others = ref $others ? %$others : ();
649 foreach my $plugin (@plugins) {
650 if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
651 $entries = $plugin->filter( \%files, \%others );
658 # Allow plugins to decide if we can cut short story generation
660 foreach my $plugin (@plugins) {
661 if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
662 if ( my $tmp = $plugin->skip() ) {
669 # Define default interpolation subroutine
672 my $template = shift;
673 # Interpolate scalars, namespaced scalars, and hash/hashref scalars
674 $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{([\'\"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
678 unless ( defined($skip) and $skip ) {
680 # Plugins: Interpolate
681 # Allow for the first encountered plugin::interpolate subroutine to
682 # override the default built-in interpolate subroutine
683 foreach my $plugin (@plugins) {
684 if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
685 if ( my $tmp = $plugin->interpolate() ) {
693 my $head = ( &$template( $currentdir, 'head', $flavour ) );
696 foreach my $plugin (@plugins) {
697 if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
698 $entries = $plugin->head( $currentdir, \$head );
702 $head = &$interpolate($head);
708 my $ne = $num_entries;
710 if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
711 $currentdir = "$1$2.$file_extension";
712 %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
713 if $files{"$datadir/$currentdir"};
716 $currentdir =~ s!/index\..+$!!;
719 # Define a default sort subroutine
721 my ($files_ref) = @_;
723 sort { $files_ref->{$b} <=> $files_ref->{$a} }
728 # Allow for the first encountered plugin::sort subroutine to override the
729 # default built-in sort subroutine
730 foreach my $plugin (@plugins) {
731 if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
732 if ( my $tmp = $plugin->sort() ) {
739 foreach my $path_file ( &$sort( \%f, \%others ) ) {
740 last if $ne <= 0 && $date !~ /\d/;
741 use vars qw/ $path $fn /;
743 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
745 # Only stories in the right hierarchy
746 $path =~ /^$currentdir/
747 or $path_file eq "$datadir/$currentdir"
750 # Prepend a slash for use in templates only if a path exists
753 # Date fiddling for by-{year,month,day} archive views
755 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
756 ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
757 = nice_date( $files{"$path_file"} );
758 ( $hr, $min ) = split /:/, $ti;
759 ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
761 if ( $hr12 == 0 ) { $hr12 = 12 }
763 # Only stories from the right date
764 my ( $path_info_yr, $path_info_mo_num, $path_info_da )
766 next if $path_info_yr && $yr != $path_info_yr;
767 last if $path_info_yr && $yr < $path_info_yr;
768 next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
769 next if $path_info_da && $da != $path_info_da;
770 last if $path_info_da && $da < $path_info_da;
773 my $date = ( &$template( $path, 'date', $flavour ) );
776 foreach my $plugin (@plugins) {
777 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
779 = $plugin->date( $currentdir, \$date,
780 $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
785 $date = &$interpolate($date);
787 if ( $date && $curdate ne $date ) {
792 use vars qw/ $title $body $raw /;
793 if ( -f "$path_file" && $fh->open("< $path_file") ) {
794 chomp( $title = <$fh> );
795 chomp( $body = join '', <$fh> );
797 $raw = "$title\n$body";
799 my $story = ( &$template( $path, 'story', $flavour ) );
802 foreach my $plugin (@plugins) {
803 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
804 $entries = $plugin->story( $path, $fn, \$story, \$title,
809 if ( $encode_xml_entities &&
810 $content_type =~ m{\bxml\b} &&
811 $content_type !~ m{\bxhtml\b} ) {
812 # Escape special characters inside the <link> container
814 # The following line should be moved more towards to top for
815 # performance reasons -- Axel Beckert, 2008-07-22
816 my $url_escape_re = qr([^-/a-zA-Z0-9:._]);
818 $url =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
819 $path =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
820 $fn =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
822 # Escape <, >, and &, and to produce valid RSS
823 $title = blosxom_html_escape($title);
824 $body = blosxom_html_escape($body);
825 $url = blosxom_html_escape($url);
826 $path = blosxom_html_escape($path);
827 $fn = blosxom_html_escape($fn);
830 if ($encode_8bit_chars) {
831 $url =~ s([^-a-zA-Z0-9_./:])(sprintf('%%%02X', ord($&)))ge;
832 $path =~ s([^-a-zA-Z0-9_./:])(sprintf('%%%02X', ord($&)))ge;
833 $fn =~ s([^-a-zA-Z0-9_./:])(sprintf('%%%02X', ord($&)))ge;
836 $story = &$interpolate($story);
845 my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
848 foreach my $plugin (@plugins) {
849 if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
850 $entries = $plugin->foot( $currentdir, \$foot );
854 $foot = &$interpolate($foot);
858 foreach my $plugin (@plugins) {
859 if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
860 $entries = $plugin->last();
866 # Finally, add the header, if any and running dynamically
867 $output = header($header) . $output
868 if ( $static_or_dynamic eq 'dynamic' and $header );
876 my $c_time = CORE::localtime($unixtime);
877 my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
879 =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
882 $da = sprintf( "%02d", $da );
883 my $mo_num = $month2num{$mo};
886 = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
887 my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
888 . sprintf( "%02d", ( $offset % 3600 ) / 60 );
890 return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
893 # Default HTML and RSS template bits
895 html content_type text/html; charset=$blog_encoding
897 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
900 html head <meta http-equiv="content-type" content="$content_type" >
901 html head <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
902 html head <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
905 html head <div align="center">
906 html head <h1>$blog_title</h1>
907 html head <p>$path_info_da $path_info_mo $path_info_yr</p>
911 html story <h3><a name="$fn">$title</a></h3>
912 html story <div>$body</div>
913 html story <p>posted at: $ti | path: <a href="$url$path">$path</a> | <a href="$url/$yr/$mo_num/$da#$fn">permanent link to this entry</a></p>
916 html date <h2>$dw, $da $mo $yr</h2>
919 html foot <div align="center">
920 html foot <a href="http://blosxom.sourceforge.net/"><img src="http://blosxom.sourceforge.net/images/pb_blosxom.gif" alt="powered by blosxom" border="0" width="90" height="33" ></a>
925 rss content_type text/xml; charset=$blog_encoding
927 rss head <?xml version="1.0" encoding="$blog_encoding"?>
928 rss head <rss version="2.0">
930 rss head <title>$blog_title</title>
931 rss head <link>$url/$path_info</link>
932 rss head <description>$blog_description</description>
933 rss head <language>$blog_language</language>
934 rss head <docs>http://blogs.law.harvard.edu/tech/rss</docs>
935 rss head <generator>blosxom/$version</generator>
938 rss story <title>$title</title>
939 rss story <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
940 rss story <link>$url/$yr/$mo_num/$da#$fn</link>
941 rss story <category>$path</category>
942 rss story <guid isPermaLink="false">$url$path/$fn</guid>
943 rss story <description>$body</description>
951 error content_type text/html
953 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
955 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
957 error head <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
958 error head <p>I'm afraid this is the first I've heard of a "$flavour" flavoured Blosxom. Try dropping the "/+$flavour" bit from the end of the URL.</p>
960 error story <h3>$title</h3>
961 error story <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
963 error date <h2>$dw, $da $mo $yr</h2>