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
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 # Should date components of the path always be at the front?
103 # If this is disabled, the date components can appear anywhere in the
104 # url (but always directly after each other, in the year/month/day
105 # order). For example, /category/subcategory/2008/12/ (or even
106 # /category/2008/12/subcategory/) shows all posts in subcategory from
108 $date_first_in_url = 0;
110 # --- Plugins (Optional) -----
112 # File listing plugins blosxom should load (if empty blosxom will load
113 # all plugins in $plugin_dir and $plugin_path directories)
116 # Where are my plugins kept?
119 # Where should my plugins keep their state information?
120 $plugin_state_dir = "$plugin_dir/state";
122 # Additional plugins location. A list of directories, separated by ';'
123 # on windows, ':' everywhere else.
126 # --- Static Rendering -----
128 # Where are this blog's static files to be created?
129 $static_dir = "/Library/WebServer/Documents/blog";
131 # What's my administrative password (you must set this for static
133 $static_password = "";
135 # What flavours should I generate statically?
136 @static_flavours = qw/html rss/;
138 # Should I statically generate individual entries?
142 # --- Advanced Encoding Options -----
144 # Should I encode entities for xml content-types? (plugins can turn
145 # this off if they do it themselves)
146 $encode_xml_entities = 1;
148 # Should I encode 8 bit special characters, e.g. umlauts in URLs, e.g.
149 # convert an ISO-Latin-1 \"o to %F6? (off by default for now; plugins
150 # can change this, too)
151 $encode_8bit_chars = 0;
153 # RegExp matching all characters which should be URL encoded in links.
154 # Defaults to anything but numbers, letters, slash, colon, dash,
155 # underscore and dot.
156 $url_escape_re = qr([^-/a-zA-Z0-9:._]);
158 # --------------------------------
164 =item B<BLOSXOM_CONFIG_FILE>
166 Points to the location of the configuration file. This will be
167 considered as first option, if it's set.
170 =item B<BLOSXOM_CONFIG_DIR>
172 The here named directory will be tried unless the above mentioned
173 environment variable is set and tested for a contained blosxom.conf
184 =item B</usr/lib/cgi-bin/blosxom>
186 The CGI script itself. Please note that the location might depend on
189 =item B</etc/blosxom/blosxom.conf>
191 The default configuration file location. This is rather taken as last
192 ressort if no other configuration location is set through environment
200 Rael Dornfest <rael@oreilly.com> was the original author of blosxom. The
201 development was picked up by a team of dedicated users of blosxom since
202 2005. See <I<http://blosxom.sourceforge.net/>> for more information.
263 use CGI qw/:standard :netscape/;
265 $version = "2.1.2+dev";
267 # Load configuration from $ENV{BLOSXOM_CONFIG_DIR}/blosxom.conf, if it exists
269 if ( $ENV{BLOSXOM_CONFIG_FILE} && -r $ENV{BLOSXOM_CONFIG_FILE} ) {
270 $blosxom_config = $ENV{BLOSXOM_CONFIG_FILE};
271 ( $config_dir = $blosxom_config ) =~ s! / [^/]* $ !!x;
274 for my $blosxom_config_dir ( $ENV{BLOSXOM_CONFIG_DIR}, '/etc/blosxom',
277 if ( -r "$blosxom_config_dir/blosxom.conf" ) {
278 $config_dir = $blosxom_config_dir;
279 $blosxom_config = "$blosxom_config_dir/blosxom.conf";
285 # Load $blosxom_config
286 if ($blosxom_config) {
287 if ( -r $blosxom_config ) {
288 eval { require $blosxom_config }
289 or warn "Error reading blosxom config file '$blosxom_config'"
290 . ( $@ ? ": $@" : '' );
293 warn "Cannot find or read blosxom config file '$blosxom_config'";
297 my $fh = new FileHandle;
314 @num2month = sort { $month2num{$a} <=> $month2num{$b} } keys %month2num;
316 # Use the stated preferred URL or figure it out automatically. Set
317 # $url manually in the config section above if CGI.pm doesn't guess
318 # the base URL correctly, e.g. when called from a Server Side Includes
323 # Unescape %XX hex codes (from URI::Escape::uri_unescape)
324 $url =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
326 # Support being called from inside a SSI document
327 $url =~ s/^included:/http:/ if $ENV{SERVER_PROTOCOL} eq 'INCLUDED';
329 # Remove PATH_INFO if it is set but not removed by CGI.pm. This
330 # seems to happen when used with Apache's Alias directive or if
331 # called from inside a Server Side Include document. If that
332 # doesn't help either, set $url manually in the configuration.
333 $url =~ s/\Q$ENV{PATH_INFO}\E$// if defined $ENV{PATH_INFO};
337 # There is one case where this code does more than necessary, too:
338 # If the URL requested is e.g. http://example.org/blog/blog and
339 # the base URL is correctly determined as http://example.org/blog
340 # by CGI.pm, then this code will incorrectly normalize the base
341 # URL down to http://example.org, because the same string as
342 # PATH_INFO is part of the base URL, too. But this is such a
343 # seldom case and can be fixed by setting $url in the config file,
347 # The only modification done to a manually set base URL is to strip
348 # a trailing slash if present.
352 # Drop ending any / from dir settings
354 $plugin_dir =~ s!/$!!;
355 $static_dir =~ s!/$!!;
357 # Fix depth to take into account datadir's path
358 $depth += ( $datadir =~ tr[/][] ) - 1 if $depth;
360 if ( !$ENV{GATEWAY_INTERFACE}
361 and param('-password')
363 and param('-password') eq $static_password )
365 $static_or_dynamic = 'static';
368 $static_or_dynamic = 'dynamic';
369 param( -name => '-quiet', -value => 1 );
373 # Take a gander at HTTP's PATH_INFO for optional blog name, archive yr/mo/day
374 my @path_info = split m{/}, path_info() || param('path');
375 $path_info_full = join '/', @path_info; # Equivalent to $ENV{PATH_INFO}
378 # Flavour specified by ?flav={flav} or index.{flav}
380 if (! ($flavour = param('flav'))) {
381 if ( $path_info[$#path_info] =~ /(.+)\.(.+)$/ ) {
383 pop @path_info if $1 eq 'index';
386 $flavour ||= $default_flavour;
388 # Fix XSS in flavour name (CVE-2008-2236)
389 $flavour = blosxom_html_escape($flavour);
391 sub blosxom_html_escape {
400 my $escape_re = join '|' => keys %escape;
401 $string =~ s/($escape_re)/$escape{$1}/g;
405 # Global variable to be used in head/foot.{flavour} templates
408 if (!$date_first_in_url) {
409 # Add all @path_info elements to $path_info till we come to one that could be a year
410 while ( $path_info[0] && $path_info[0] !~ /^(19|20)\d{2}$/) {
411 $path_info .= '/' . shift @path_info;
415 # Pull date elements out of path
416 if ($path_info[0] && $path_info[0] =~ /^(19|20)\d{2}$/) {
417 $path_info_yr = shift @path_info;
419 ($path_info[0] =~ /^(0\d|1[012])$/ ||
420 exists $month2num{ ucfirst lc $path_info_mo })) {
421 $path_info_mo = shift @path_info;
422 # Map path_info_mo to numeric $path_info_mo_num
423 $path_info_mo_num = $path_info_mo =~ /^\d{2}$/
425 : $month2num{ ucfirst lc $path_info_mo };
426 if ($path_info[0] && $path_info[0] =~ /^[0123]\d$/) {
427 $path_info_da = shift @path_info;
432 # Add remaining path elements to $path_info
433 $path_info .= '/' . join('/', @path_info);
435 # Strip spurious slashes
436 $path_info =~ s!(^/*)|(/*$)!!g;
438 # Define standard template subroutine, plugin-overridable at Plugins: Template
440 my ( $path, $chunk, $flavour ) = @_;
443 return join '', <$fh>
444 if $fh->open("< $datadir/$path/$chunk.$flavour");
445 } while ( $path =~ s/(\/*[^\/]*)$// and $1 );
447 # Check for definedness, since flavour can be the empty string
448 if ( defined $template{$flavour}{$chunk} ) {
449 return $template{$flavour}{$chunk};
451 elsif ( defined $template{error}{$chunk} ) {
452 return $template{error}{$chunk};
459 # Bring in the templates
462 last if /^(__END__)$/;
463 my ( $ct, $comp, $txt ) = /^(\S+)\s(\S+)(?:\s(.*))?$/ or next;
465 $template{$ct}{$comp} .= $txt . "\n";
469 my $path_sep = $^O eq 'MSWin32' ? ';' : ':';
470 my @plugin_dirs = split /$path_sep/, $plugin_path;
471 unshift @plugin_dirs, $plugin_dir;
472 my @plugin_list = ();
473 my %plugin_hash = ();
475 # If $plugin_list is set, read plugins to use from that file
476 if ( $plugin_list ) {
477 if ( -r $plugin_list and $fh->open("< $plugin_list") ) {
478 @plugin_list = map { chomp $_; $_ } grep { /\S/ && !/^#/ } <$fh>;
482 warn "unable to read or open plugin_list '$plugin_list': $!";
487 # Otherwise walk @plugin_dirs to get list of plugins to use
488 if ( ! @plugin_list && @plugin_dirs ) {
489 for my $plugin_dir (@plugin_dirs) {
490 next unless -d $plugin_dir;
491 if ( opendir PLUGINS, $plugin_dir ) {
493 grep { /^[\w:]+$/ && !/~$/ && -f "$plugin_dir/$_" }
498 next if $plugin_hash{$plugin};
500 # Add to @plugin_list and %plugin_hash
501 $plugin_hash{$plugin} = "$plugin_dir/$plugin";
502 push @plugin_list, $plugin;
507 @plugin_list = sort @plugin_list;
510 # Load all plugins in @plugin_list
511 unshift @INC, @plugin_dirs;
512 foreach my $plugin (@plugin_list) {
513 my ( $plugin_name, $off ) = $plugin =~ /^\d*([\w:]+?)(_?)$/;
514 my $plugin_file = $plugin_list ? $plugin_name : $plugin;
515 my $on_off = $off eq '_' ? -1 : 1;
517 # Allow perl module plugins
518 # The -z test is a hack to allow a zero-length placeholder file in a
519 # $plugin_path directory to indicate an @INC module should be loaded
520 if ( $plugin =~ m/::/ && ( $plugin_list || -z $plugin_hash{$plugin} ) ) {
522 # For Blosxom::Plugin::Foo style plugins, we need to use a string require
523 eval "require $plugin_file";
526 { # we try first to load from $plugin_dir before attempting from $plugin_path
527 eval { require "$plugin_dir/$plugin_file" }
528 or eval { require $plugin_file };
532 warn "error finding or loading blosxom plugin '$plugin_name': $@";
535 if ( $plugin_name->start() and ( $plugins{$plugin_name} = $on_off ) ) {
536 push @plugins, $plugin_name;
540 shift @INC foreach @plugin_dirs;
543 # Allow for the first encountered plugin::template subroutine to override the
544 # default built-in template subroutine
545 foreach my $plugin (@plugins) {
546 if ( $plugins{$plugin} > 0 and $plugin->can('template') ) {
547 if ( my $tmp = $plugin->template() ) {
554 # Provide backward compatibility for Blosxom < 2.0rc1 plug-ins
556 return &$template(@_);
559 # Define default entries subroutine
561 my ( %files, %indexes, %others );
565 my $curr_depth = $File::Find::dir =~ tr[/][];
566 return if $depth and $curr_depth > $depth;
572 =~ m!^$datadir/(?:(.*)/)?(.+)\.$file_extension$!
574 # not an index, .file, and is readable
575 and $2 ne 'index' and $2 !~ /^\./ and ( -r $File::Find::name )
579 # read modification time
580 my $mtime = stat($File::Find::name)->mtime or return;
582 # to show or not to show future entries
583 return unless ( $show_future_entries or $mtime < time );
585 # add the file and its associated mtime to the list of files
586 $files{$File::Find::name} = $mtime;
588 # static rendering bits
590 = "$static_dir/$1/index." . $static_flavours[0];
593 or stat($static_file)->mtime < $mtime )
596 $d = join( '/', ( nice_date($mtime) )[ 5, 2, 3 ] );
598 $indexes{ ( $1 ? "$1/" : '' ) . "$2.$file_extension" } = 1
603 # not an entries match
604 elsif ( !-d $File::Find::name and -r $File::Find::name ) {
605 $others{$File::Find::name} = stat($File::Find::name)->mtime;
611 return ( \%files, \%indexes, \%others );
615 # Allow for the first encountered plugin::entries subroutine to override the
616 # default built-in entries subroutine
617 foreach my $plugin (@plugins) {
618 if ( $plugins{$plugin} > 0 and $plugin->can('entries') ) {
619 if ( my $tmp = $plugin->entries() ) {
626 my ( $files, $indexes, $others ) = &$entries();
627 %indexes = %$indexes;
630 if ( !$ENV{GATEWAY_INTERFACE}
631 and param('-password')
633 and param('-password') eq $static_password )
636 param('-quiet') or print "Blosxom is generating static index pages...\n";
638 # Home Page and Directory Indexes
640 foreach my $path ( sort keys %indexes ) {
642 foreach ( ( '', split /\//, $path ) ) {
646 mkdir "$static_dir/$p", 0755
647 unless ( -d "$static_dir/$p" or $p =~ /\.$file_extension$/ );
648 foreach $flavour (@static_flavours) {
650 = ( &$template( $p, 'content_type', $flavour ) );
651 $content_type =~ s!\n.*!!s;
652 my $fn = $p =~ m!^(.+)\.$file_extension$! ? $1 : "$p/index";
653 param('-quiet') or print "$fn.$flavour\n";
654 my $fh_w = new FileHandle "> $static_dir/$fn.$flavour"
655 or die "Couldn't open $static_dir/$p for writing: $!";
657 if ( $indexes{$path} == 1 ) {
663 $path_info =~ s!\.$file_extension$!\.$flavour!;
664 print $fh_w &generate( 'static', $path_info, '', $flavour,
671 $path_info_yr, $path_info_mo,
672 $path_info_da, $path_info
673 ) = split /\//, $p, 4;
674 unless ( defined $path_info ) { $path_info = "" }
675 print $fh_w &generate( 'static', '', $p, $flavour,
686 $content_type = ( &$template( $path_info, 'content_type', $flavour ) );
687 $content_type =~ s!\n.*!!s;
689 $content_type =~ s/(\$\w+(?:::\w+)*)/"defined $1 ? $1 : ''"/gee;
690 $header = { -type => $content_type };
692 print generate( 'dynamic', $path_info,
693 "$path_info_yr/$path_info_mo_num/$path_info_da",
694 $flavour, $content_type );
698 foreach my $plugin (@plugins) {
699 if ( $plugins{$plugin} > 0 and $plugin->can('end') ) {
700 $entries = $plugin->end();
706 my ( $static_or_dynamic, $currentdir, $date, $flavour, $content_type )
710 %others = ref $others ? %$others : ();
713 foreach my $plugin (@plugins) {
714 if ( $plugins{$plugin} > 0 and $plugin->can('filter') ) {
715 $entries = $plugin->filter( \%files, \%others );
722 # Allow plugins to decide if we can cut short story generation
724 foreach my $plugin (@plugins) {
725 if ( $plugins{$plugin} > 0 and $plugin->can('skip') ) {
726 if ( my $tmp = $plugin->skip() ) {
733 # Define default interpolation subroutine
736 my $template = shift;
737 # Interpolate scalars, namespaced scalars, and hash/hashref scalars
738 $template =~ s/(\$\w+(?:::\w+)*(?:(?:->)?{([\'\"]?)[-\w]+\2})?)/"defined $1 ? $1 : ''"/gee;
742 unless ( defined($skip) and $skip ) {
744 # Plugins: Interpolate
745 # Allow for the first encountered plugin::interpolate subroutine to
746 # override the default built-in interpolate subroutine
747 foreach my $plugin (@plugins) {
748 if ( $plugins{$plugin} > 0 and $plugin->can('interpolate') ) {
749 if ( my $tmp = $plugin->interpolate() ) {
757 my $head = ( &$template( $currentdir, 'head', $flavour ) );
760 foreach my $plugin (@plugins) {
761 if ( $plugins{$plugin} > 0 and $plugin->can('head') ) {
762 $entries = $plugin->head( $currentdir, \$head );
766 $head = &$interpolate($head);
772 my $ne = $num_entries;
774 if ( $currentdir =~ /(.*?)([^\/]+)\.(.+)$/ and $2 ne 'index' ) {
775 $currentdir = "$1$2.$file_extension";
776 %f = ( "$datadir/$currentdir" => $files{"$datadir/$currentdir"} )
777 if $files{"$datadir/$currentdir"};
780 $currentdir =~ s!/index\..+$!!;
783 # Define a default sort subroutine
785 my ($files_ref) = @_;
787 sort { $files_ref->{$b} <=> $files_ref->{$a} }
792 # Allow for the first encountered plugin::sort subroutine to override the
793 # default built-in sort subroutine
794 foreach my $plugin (@plugins) {
795 if ( $plugins{$plugin} > 0 and $plugin->can('sort') ) {
796 if ( my $tmp = $plugin->sort() ) {
803 foreach my $path_file ( &$sort( \%f, \%others ) ) {
804 last if $ne <= 0 && $date !~ /\d/;
805 use vars qw/ $path $fn /;
807 = $path_file =~ m!^$datadir/(?:(.*)/)?(.*)\.$file_extension!;
809 # Only stories in the right hierarchy
810 $path =~ /^$currentdir/
811 or $path_file eq "$datadir/$currentdir"
814 # Prepend a slash for use in templates only if a path exists
817 # Date fiddling for by-{year,month,day} archive views
819 qw/ $dw $mo $mo_num $da $ti $yr $hr $min $hr12 $ampm $utc_offset/;
820 ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset )
821 = nice_date( $files{"$path_file"} );
822 ( $hr, $min ) = split /:/, $ti;
823 ( $hr12, $ampm ) = $hr >= 12 ? ( $hr - 12, 'pm' ) : ( $hr, 'am' );
825 if ( $hr12 == 0 ) { $hr12 = 12 }
827 # Only stories from the right date
828 my ( $path_info_yr, $path_info_mo_num, $path_info_da )
830 next if $path_info_yr && $yr != $path_info_yr;
831 last if $path_info_yr && $yr < $path_info_yr;
832 next if $path_info_mo_num && $mo ne $num2month[$path_info_mo_num];
833 next if $path_info_da && $da != $path_info_da;
834 last if $path_info_da && $da < $path_info_da;
837 my $date = ( &$template( $path, 'date', $flavour ) );
840 foreach my $plugin (@plugins) {
841 if ( $plugins{$plugin} > 0 and $plugin->can('date') ) {
843 = $plugin->date( $currentdir, \$date,
844 $files{$path_file}, $dw, $mo, $mo_num, $da, $ti,
849 $date = &$interpolate($date);
851 if ( $date && $curdate ne $date ) {
856 use vars qw/ $title $body $raw /;
857 if ( -f "$path_file" && $fh->open("< $path_file") ) {
858 chomp( $title = <$fh> );
859 chomp( $body = join '', <$fh> );
861 $raw = "$title\n$body";
863 my $story = ( &$template( $path, 'story', $flavour ) );
866 foreach my $plugin (@plugins) {
867 if ( $plugins{$plugin} > 0 and $plugin->can('story') ) {
868 $entries = $plugin->story( $path, $fn, \$story, \$title,
873 # Save unescaped versions and allow them to be used in
875 use vars qw/$url_unesc $path_unesc $fn_unesc/;
880 # Fix special characters in links inside XML content
881 if ( $encode_xml_entities &&
882 $content_type =~ m{\bxml\b} &&
883 $content_type !~ m{\bxhtml\b} ) {
884 # Escape special characters inside the <link> container
886 &url_escape_url_path_and_fn();
888 # Escape <, >, and &, and to produce valid RSS
889 $title = blosxom_html_escape($title);
890 $body = blosxom_html_escape($body);
891 $url = blosxom_html_escape($url);
892 $path = blosxom_html_escape($path);
893 $fn = blosxom_html_escape($fn);
896 # Fix special characters in links inside XML content
897 if ($encode_8bit_chars) {
898 &url_escape_url_path_and_fn();
901 $story = &$interpolate($story);
910 my $foot = ( &$template( $currentdir, 'foot', $flavour ) );
913 foreach my $plugin (@plugins) {
914 if ( $plugins{$plugin} > 0 and $plugin->can('foot') ) {
915 $entries = $plugin->foot( $currentdir, \$foot );
919 $foot = &$interpolate($foot);
923 foreach my $plugin (@plugins) {
924 if ( $plugins{$plugin} > 0 and $plugin->can('last') ) {
925 $entries = $plugin->last();
931 # Finally, add the header, if any and running dynamically
932 $output = header($header) . $output
933 if ( $static_or_dynamic eq 'dynamic' and $header );
941 my $c_time = CORE::localtime($unixtime);
942 my ( $dw, $mo, $da, $hr, $min, $sec, $yr )
944 =~ /(\w{3}) +(\w{3}) +(\d{1,2}) +(\d{2}):(\d{2}):(\d{2}) +(\d{4})$/
947 $da = sprintf( "%02d", $da );
948 my $mo_num = $month2num{$mo};
951 = timegm( $sec, $min, $hr, $da, $mo_num - 1, $yr - 1900 ) - $unixtime;
952 my $utc_offset = sprintf( "%+03d", int( $offset / 3600 ) )
953 . sprintf( "%02d", ( $offset % 3600 ) / 60 );
955 return ( $dw, $mo, $mo_num, $da, $ti, $yr, $utc_offset );
958 sub url_escape_url_path_and_fn {
959 $url =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
960 $path =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
961 $fn =~ s($url_escape_re)(sprintf('%%%02X', ord($&)))eg;
964 # Default HTML and RSS template bits
966 html content_type text/html; charset=$blog_encoding
968 html head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
971 html head <meta http-equiv="content-type" content="$content_type" >
972 html head <link rel="alternate" type="application/rss+xml" title="RSS" href="$url/index.rss" >
973 html head <title>$blog_title $path_info_da $path_info_mo $path_info_yr</title>
976 html head <div align="center">
977 html head <h1>$blog_title</h1>
978 html head <p>$path_info_da $path_info_mo $path_info_yr</p>
982 html story <h3><a name="$fn">$title</a></h3>
983 html story <div>$body</div>
984 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>
987 html date <h2>$dw, $da $mo $yr</h2>
990 html foot <div align="center">
991 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>
996 rss content_type text/xml; charset=$blog_encoding
998 rss head <?xml version="1.0" encoding="$blog_encoding"?>
999 rss head <rss version="2.0">
1001 rss head <title>$blog_title</title>
1002 rss head <link>$url/$path_info</link>
1003 rss head <description>$blog_description</description>
1004 rss head <language>$blog_language</language>
1005 rss head <docs>http://blogs.law.harvard.edu/tech/rss</docs>
1006 rss head <generator>blosxom/$version</generator>
1009 rss story <title>$title</title>
1010 rss story <pubDate>$dw, $da $mo $yr $ti:00 $utc_offset</pubDate>
1011 rss story <link>$url/$yr/$mo_num/$da#$fn</link>
1012 rss story <category>$path</category>
1013 rss story <guid isPermaLink="false">$url$path/$fn</guid>
1014 rss story <description>$body</description>
1022 error content_type text/html
1024 error head <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
1026 error head <head><title>Error: unknown Blosxom flavour "$flavour"</title></head>
1028 error head <h1><font color="red">Error: unknown Blosxom flavour "$flavour"</font></h1>
1029 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>
1031 error story <h3>$title</h3>
1032 error story <div>$body</div> <p><a href="$url/$yr/$mo_num/$da#fn.$default_flavour">#</a></p>
1034 error date <h2>$dw, $da $mo $yr</h2>