#!/usr/bin/perl # Written by Nathan Wagner # Copyright disclaimed. This file is in the public domain. use strict; use warnings; use Template; use Text::Markup; use Text::MultiMarkdown; use File::Find; use POSIX qw(strftime); use Digest::SHA qw(sha256_hex); use File::Path qw(make_path); use CGI; use CGI::Carp qw(fatalsToBrowser); # TODO allow per category configuration. Can just 'do .bluefin' if it exists # for the category my $blogtitle = `pwd`; chomp($blogtitle); $blogtitle =~ s|.+/||g; $blogtitle =~ s/\.[^\.]+$//; $blogtitle =~ s/_/ /g; $blogtitle = join(' ', map { ucfirst } split(/\s+/, $blogtitle)); $blogtitle = 'Bad Data'; my $postdir = './posts'; # where should I look for posts my $commentdir = './comments'; # where to put or look for comments my $commenttime = ''; # false is all allowed, positive numeric # is seconds after article post allowed. negative numeric is seconds # after last comment allowed my $commentsneeddir = 0; # true if comment directory exists, false will create directory, if needed # can disable comments by creating a regular file where the comment directory # would be my $maxcommentlength = 2048; # if non-zero, maximum length in bytes of # a comment my $pagelimit = 10; # number of articles maximum per 'page' my $allowqstringpagelimit = 0; # allow query string to specify pagelimit # positive number is most allowed, higher will be truncated to that limit # urls like /blog/[category]/archive/[pagenum]/ will be special cased for # showing older posts, pagelimit is used for the breaks my $archivename = 'archive'; my $ignorefuture = 1; # ignore posts with timestamps in the future my $sortposts = sub { my $cmp; $cmp = $b->{'timestamp'} <=> $a->{timestamp}; if (!$cmp) { $cmp = $a->{'name'} cmp $b->{name}; } return $cmp; }; my $dateformat = '%B %d, %Y'; my $timeformat = '%H:%m:%s'; my $posttimeformat = '%c'; # get any local config -f 'bluefin.cfg' && do 'bluefin.cfg'; my $q = new CGI; my $get = $q->path_info; my $tt = Template->new({ INCLUDE_PATH => 'templates' } ); my $target = "$postdir/$get"; my $pagenumber = 0; my @files; if ($q->request_method() eq 'POST') { # handle a comment # need form fields: article, comment # remaining form fields put in metatags. if (-f $target) { my $text = $q->param('comment'); my $path = $q->path_info(); my $author = $q->param('name'); if ($maxcommentlength && length($text) > $maxcommentlength) { print $q->header(-status => 413); # TODO if -f 413.tmpl exit 0; } if (!-f $target) { print $q->header(-status => 403); exit 0; } my $article = $target; my $cd = "$commentdir/$path"; make_path($cd); my $comment; $comment .= "Article: $path\n"; $comment .= "Name: $author\n"; $comment .= "\n"; $comment .= $q->param('comment'); $comment .= "\n"; my $hash = sha256_hex($comment); open(my $fh, '>', "$cd/.$hash"); print $fh $comment; close($fh); rename("$cd/.$hash", "$cd/$hash"); print $q->redirect($q->url(-path_info => 1)); } elsif (-d $target) { # trying to post to a directory print $q->header(-status => 403); exit 0; } else { # trying to post to a non-existent file print $q->header(-status => 404); exit 0; } } #my $findposts = makefinder($target); if (-d $target or -f $target) { print $q->header; # a category. read in the files my $page = {}; # template info my @posts = (); # processed posts @files = (); # raw files find(\&findposts, $target); # find the post files @files = sort $sortposts @files; # sort them by criteria #@files = map { $_->{path} =~ s|^$target|| } @files; if ($ignorefuture) { my $ts = time; @files = grep { $_->{timestamp} <= $ts } @files; } # process the posts we're actually going to pass to the template my $start = $pagenumber * $pagelimit; my $end = $start + $pagelimit; my $postcount = @files; if ($end > @files) { $end = @files; } if (@files >= $start) { my $url = $q->url(-relative => 1, -path_info => 1); $url = $q->url(); @files = splice(@files, $start, $end - $start); @posts = map { readpost($_->{path}, $_, $url, 1) } @files; } $page->{url} = $q->url(-rewrite => 0, -path => 0); $page->{totalposts} = $postcount; $page->{postpages} = int($postcount/$pagelimit) + $postcount % $pagelimit == 0 ? 0 : 1; $page->{blogtitle} = $blogtitle; $page->{posts} = \@posts; $tt->process('category.tmpl', $page) or die $tt->error; } elsif (-f $target) { my $page = {}; my $post = readpost($target); $page->{blogtitle} = $blogtitle; $page->{posts} = [ $post ]; # a single article $tt->process('article.tmpl', $page); } exit 0; sub readfile { my ($path) = @_; if (!-f $path) { return (); } my $page = {}; $page->{'timestamp'} = (stat($path))[9]; my @timeinfo = localtime($page->{'timestamp'}); $page->{'timeinfo'} = \@timeinfo; $page->{'postdate'} = strftime($dateformat, @timeinfo); $page->{'posttime'} = strftime($timeformat, @timeinfo); $page->{'date'} = strftime($posttimeformat, @timeinfo); my ($fh); open $fh, $path; local $/ = ""; my @lines = <$fh>; if ($lines[0] and $lines[0] =~ m/^.+:\s*.+/m) { my %hdrinfo = ($lines[0] =~ m/^(.+):\s*(.+)/mg); $page->{'meta'} = \%hdrinfo; $page->{'title'} = $page->{'meta'}{'Title'}; shift @lines; } my $lines = join('', @lines); my $md = Text::MultiMarkdown->new(); my $html = $md->markdown($lines); close $fh; $page->{'content'} = $html; return $page; } sub find_files { my @paths = @_; my @files = (); my $asof = time; my $find = sub { push @files, $File::Find::name if ( (-f $_) && (!m/^\./) )}; my $get = sub { wantarray ? @files : [ @files ] }; find($find, @paths); return $get->(); } sub readpost { my ($path, $xinfo, $url, $comments) = @_; my %page; my $page = readfile($path); return () unless $page; $page->{'url'} = $url . '/' . $xinfo->{lpath}; if (!$page->{'title'}) { my $t = $xinfo->{name}; $t =~ s/\.[^\.]+$//; $t =~ s/_/ /g; $t = join(' ', map { ucfirst } split(/\s+/, $t)); $page->{'title'} = $t; } my $cd = "$commentdir/" . $xinfo->{lpath}; if ($comments) { if (-d $cd) { my @comments = map { readfile($_) } find_files($cd); @comments = sort { my $cmp; $cmp = $a->{'timestamp'} <=> $b->{timestamp}; if (!$cmp) { $cmp = $a->{'name'} <=> $b->{name}; } return $cmp; } @comments; $page->{comments} = \@comments; } else { $page->{comments} = []; } } $page->{'xinfo'} = $xinfo; my @catpath = split(m|/|, $xinfo->{lpath}); pop @catpath; $page->{'catpath'} = \@catpath; my $caturl = $q->url(-rewrite => 0) . '/../'; my @caturls = (); foreach my $cat (@catpath) { $caturl .= "$cat/"; push @caturls, { name => $cat, url => $caturl }; } $page->{'categories'} = \@caturls; return $page; } sub findposts { my %fileinfo; return unless -f; return if m/^\./; # skip dotfiles $fileinfo{'stat'} = [ stat $_ ]; $fileinfo{'timestamp'} = (stat _)[9]; $fileinfo{'name'} = $_; $fileinfo{'dir'} = $File::Find::dir; $fileinfo{'path'} = $File::Find::name; $fileinfo{'lpath'} = $File::Find::name; $fileinfo{'lpath'} =~ s|^$postdir/*||; push @files, \%fileinfo; } __END__ my $parser = Text::Markup->new( default_format => 'multimarkdown', default_encoding => 'UTF-8', ); #my $html = $parser->parse(file => $file); print $html;