initial commit
[bluefin] / index.cgi
1 #!/usr/bin/perl
2
3 # Written by Nathan Wagner <nw@hydaspes.if.org>
4 # Copyright disclaimed.  This file is in the public domain.
5
6 use strict;
7 use warnings;
8
9 use Template;
10 use Text::Markup;
11 use Text::MultiMarkdown;
12 use File::Find;
13 use POSIX qw(strftime);
14 use Digest::SHA qw(sha256_hex);
15 use File::Path qw(make_path);
16
17 use CGI;
18 use CGI::Carp qw(fatalsToBrowser);
19
20 # TODO allow per category configuration.  Can just 'do .bluefin' if it exists
21 # for the category
22
23 my $blogtitle = `pwd`;
24 chomp($blogtitle);
25 $blogtitle =~ s|.+/||g;
26 $blogtitle =~ s/\.[^\.]+$//;
27 $blogtitle =~ s/_/ /g;
28 $blogtitle = join(' ', map { ucfirst } split(/\s+/, $blogtitle));
29
30 $blogtitle = 'Bad Data';
31
32 my $postdir = './posts'; # where should I look for posts
33
34 my $commentdir = './comments'; # where to put or look for comments
35 my $commenttime = ''; # false is all allowed, positive numeric
36 # is seconds after article post allowed.  negative numeric is seconds
37 # after last comment allowed
38 my $commentsneeddir = 0;
39 # true if comment directory exists, false will create directory, if needed
40 # can disable comments by creating a regular file where the comment directory
41 # would be
42 my $maxcommentlength = 2048; # if non-zero, maximum length in bytes of
43 # a comment
44
45 my $pagelimit = 10; # number of articles maximum per 'page'
46 my $allowqstringpagelimit = 0; # allow query string to specify pagelimit
47 # positive number is most allowed, higher will be truncated to that limit
48
49 # urls like /blog/[category]/archive/[pagenum]/ will be special cased for
50 # showing older posts, pagelimit is used for the breaks
51 my $archivename = 'archive';
52
53 my $ignorefuture = 1; # ignore posts with timestamps in the future
54
55 my $sortposts = sub {
56         my $cmp;
57         $cmp = $b->{'timestamp'} <=> $a->{timestamp};
58         if (!$cmp) {
59                 $cmp = $a->{'name'} cmp $b->{name};
60         }
61         return $cmp;
62 };
63
64 my $dateformat = '%B %d, %Y';
65 my $timeformat = '%H:%m:%s';
66 my $posttimeformat = '%c';
67
68 # get any local config
69 -f 'bluefin.cfg' && do 'bluefin.cfg';
70
71 my $q = new CGI;
72
73 my $get = $q->path_info;
74
75 my $tt = Template->new({
76                 INCLUDE_PATH => 'templates'
77         }
78 );
79
80 my $target = "$postdir/$get";
81 my $pagenumber = 0;
82 my @files;
83
84 if ($q->request_method() eq 'POST') {
85         # handle a comment
86         # need form fields: article, comment
87         # remaining form fields put in metatags.
88         if (-f $target) {
89                 my $text = $q->param('comment');
90                 my $path = $q->path_info();
91                 my $author = $q->param('name');
92                 if ($maxcommentlength && length($text) > $maxcommentlength) {
93                         print $q->header(-status => 413);
94                         # TODO if -f 413.tmpl
95                         exit 0;
96                 }
97                 if (!-f $target) {
98                         print $q->header(-status => 403);
99                         exit 0;
100                 }
101                 
102                 my $article = $target;
103                 my $cd = "$commentdir/$path";
104                 make_path($cd);
105                 my $comment;
106                 $comment .= "Article: $path\n";
107                 $comment .= "Name: $author\n";
108                 $comment .= "\n";
109                 $comment .= $q->param('comment');
110                 $comment .= "\n";
111                 my $hash = sha256_hex($comment);
112                 open(my $fh, '>', "$cd/.$hash");
113                 print $fh $comment;
114                 close($fh);
115                 rename("$cd/.$hash", "$cd/$hash");
116                 print $q->redirect($q->url(-path_info => 1));
117         } elsif (-d $target) {
118                 # trying to post to a directory
119                 print $q->header(-status => 403);
120                 exit 0;
121         } else {
122                 # trying to post to a non-existent file
123                 print $q->header(-status => 404);
124                 exit 0;
125
126         }
127 }
128
129 #my $findposts = makefinder($target);
130
131 if (-d $target or -f $target) {
132         print $q->header;
133         # a category.  read in the files
134         my $page = {}; # template info
135         my @posts = (); # processed posts
136         @files = (); # raw files
137         find(\&findposts, $target); # find the post files
138         @files = sort $sortposts @files; # sort them by criteria
139         #@files = map { $_->{path} =~ s|^$target|| } @files;
140         if ($ignorefuture) {
141                 my $ts = time;
142                 @files = grep { $_->{timestamp} <= $ts } @files;
143         }
144
145         # process the posts we're actually going to pass to the template
146         my $start = $pagenumber * $pagelimit;
147         my $end = $start + $pagelimit;
148         my $postcount = @files;
149         if ($end > @files) {
150                 $end = @files;
151         }
152         if (@files >= $start) {
153                 my $url = $q->url(-relative => 1, -path_info => 1);
154                 $url = $q->url();
155                 @files = splice(@files, $start, $end - $start);
156                 @posts = map { readpost($_->{path}, $_, $url, 1) } @files;
157         }
158
159         $page->{url} = $q->url(-rewrite => 0, -path => 0);
160         $page->{totalposts} = $postcount;
161         $page->{postpages} = int($postcount/$pagelimit) + $postcount % $pagelimit == 0 ? 0 : 1;
162         $page->{blogtitle} = $blogtitle;
163         $page->{posts} = \@posts;
164         $tt->process('category.tmpl', $page) or die $tt->error;
165 } elsif (-f $target) {
166         my $page = {};
167         my $post = readpost($target);
168         $page->{blogtitle} = $blogtitle;
169         $page->{posts} = [ $post ];
170
171         # a single article
172         $tt->process('article.tmpl', $page);
173 }
174 exit 0;
175
176 sub readfile {
177         my ($path) = @_;
178         if (!-f $path) {
179                 return ();
180         }
181         my $page = {};
182         $page->{'timestamp'} = (stat($path))[9];
183         my @timeinfo = localtime($page->{'timestamp'});
184         $page->{'timeinfo'} = \@timeinfo;
185         $page->{'postdate'} = strftime($dateformat, @timeinfo);
186         $page->{'posttime'} = strftime($timeformat, @timeinfo);
187         $page->{'date'} = strftime($posttimeformat, @timeinfo);
188
189         my ($fh);
190         open $fh, $path;
191         local $/ = "";
192         my @lines = <$fh>;
193         if ($lines[0] and $lines[0] =~ m/^.+:\s*.+/m) {
194                 my %hdrinfo = ($lines[0] =~ m/^(.+):\s*(.+)/mg);
195                 $page->{'meta'} = \%hdrinfo;
196                 $page->{'title'} = $page->{'meta'}{'Title'};
197
198                 shift @lines;
199         }
200
201         my $lines = join('', @lines);
202         my $md = Text::MultiMarkdown->new();
203         my $html = $md->markdown($lines);
204         close $fh;
205         $page->{'content'} = $html;
206
207         return $page;
208 }
209
210 sub find_files {
211         my @paths = @_;
212         my @files = ();
213         my $asof = time;
214
215         my $find = sub { push @files, $File::Find::name if ( (-f $_) && (!m/^\./) )};
216         my $get = sub { wantarray ? @files : [ @files ] };
217         find($find, @paths);
218         return $get->();
219 }
220
221 sub readpost {
222         my ($path, $xinfo, $url, $comments) = @_;
223         my %page;
224
225         my $page = readfile($path);
226         return () unless $page;
227
228         $page->{'url'} = $url . '/' . $xinfo->{lpath};
229
230         if (!$page->{'title'}) {
231                 my $t = $xinfo->{name};
232                 $t =~ s/\.[^\.]+$//;
233                 $t =~ s/_/ /g;
234                 $t = join(' ', map { ucfirst } split(/\s+/, $t));
235                 $page->{'title'} = $t;
236         }
237
238         my $cd = "$commentdir/" . $xinfo->{lpath};
239         if ($comments) {
240                 if (-d $cd) {
241                         my @comments = map { readfile($_) } find_files($cd);
242                         @comments = sort {
243                                 my $cmp;
244                                 $cmp = $a->{'timestamp'} <=> $b->{timestamp};
245                                 if (!$cmp) {
246                                         $cmp = $a->{'name'} <=> $b->{name};
247                                 }
248                                 return $cmp;
249                                 } @comments;
250
251                         $page->{comments} = \@comments;
252                 } else {
253                         $page->{comments} = [];
254                 }
255         }
256
257         $page->{'xinfo'} = $xinfo;
258
259         my @catpath = split(m|/|, $xinfo->{lpath});
260         pop @catpath;
261         $page->{'catpath'} = \@catpath;
262         my $caturl = $q->url(-rewrite => 0) . '/../';
263         my @caturls = ();
264         foreach my $cat (@catpath) {
265                 $caturl .= "$cat/";
266                 push @caturls, { name => $cat, url => $caturl };
267         }
268         
269         $page->{'categories'} = \@caturls;
270
271         return $page;
272 }
273
274 sub findposts {
275         my %fileinfo;
276         return unless -f;
277         return if m/^\./; # skip dotfiles
278         $fileinfo{'stat'} = [ stat $_ ];
279         $fileinfo{'timestamp'} = (stat _)[9];
280         $fileinfo{'name'} = $_;
281         $fileinfo{'dir'} = $File::Find::dir;
282         $fileinfo{'path'} = $File::Find::name;
283         $fileinfo{'lpath'} = $File::Find::name;
284         $fileinfo{'lpath'} =~ s|^$postdir/*||;
285         push @files, \%fileinfo;
286 }
287
288 __END__
289 my $parser = Text::Markup->new(
290         default_format   => 'multimarkdown',
291         default_encoding => 'UTF-8',
292 );
293
294 #my $html = $parser->parse(file => $file);
295
296 print $html;