Verified Commit 52afcd48 authored by Adam Matoušek's avatar Adam Matoušek
Browse files

Rough but mostly functional blog system.

parents
/articles/
/_build/
/_deploy/
/deploy
/.gib.bin
/gib/local
[submodule "gib/bundle"]
path = gib/bundle
url = https://github.com/mornfall/gib-bundle.git
Subproject commit 40e7206b1b1c2df46fa82667b0e18a1e6446f46a
set cc /usr/bin/cc
set cflags -O2
set outdir _build
set prefix $(srcdir)/_deploy
# for gib -V dbg
set dbg
# document root; override in local gibfile to build directory
set root
set deployed-files
sub? gib/local
sub gib/bundle/boot.gib
set env /usr/bin/env SRCDIR=$(srcdir) OUTDIR=$(srcdir)/$(outdir) DOCUMENT_ROOT=$(root)
set tool $(env) $(srcdir)/tools/run
def byproduct
cmd /bin/true
def transient
cmd $(tool) chguard $(out)
out gib.manifest
cmd gib.findsrc $(srcdir) $(out)
src sources dirs gib.manifest
set articles $(sources:articles/*/meta:$1)
out article dirs
dep articles/$(articles)
cmd gib.nochange /bin/mkdir -p $(articles)
# page indices (used to generate index/archive pages)
out archives.gib
dep $(sources:articles/*/meta)
cmd $(tool) build_index
out index
dep archives.gib
use transient
# sets $(archives) with years and categories
# and $(archives.$path) with contained articles
sub archives.gib
for a $(archives)
out $(a)/index
dep index
use transient
# assemble navigation
out nav.html
dep templates/nav.tt
dep index
cmd $(tool) mknav
# assemble comments
for a $(articles)
out $(a)/comments.html
dep article dirs
dep $(sources:articles/$(a)/*.comment)
dep templates/comments.tt
cmd $(tool) mkcomments $(a)
# TODO: md2html or sth
# copy ready-made (templated) .htmls
for c $(sources:articles/*/content.html)
out $(c:articles/*.html:$1).tt.html
dep $(c)
dep article dirs
cmd /bin/cp $(srcdir)/$(c) $(out)
set images $(sources:articles/*.jpg:$1.jpg)
# link full-size images
for i $(images)
out $(i)
dep articles/$(i)
dep article dirs
cmd /bin/ln -f $(srcdir)/articles/$(i) $(out)
add deployed-files $(i)
set resized-images
# smaller image sizes (for use in articles)
for i $(images)
out resize $(i)
dep $(i)
cmd $(tool) mkminis $(i) $(i:*/*:$1)
for i $(images)
out $(i).gib
dep resize $(i)
use byproduct
for i $(images)
sub $(i).gib
set images-all
for a $(articles)
out images for $(a)
let imgs $(images:$(a))
dep $(imgs)
dep resize $(imgs)
use byproduct
add images-all $(out)
# assemble content of the article
for a $(articles)
out $(a)/content.html
dep article dirs
dep $(a)/content.tt.html
dep templates/perex.tt
cmd $(tool) mkcontent $(a)
# the previous step also produces the perex
for a $(articles)
out $(a)/perex.html
dep $(a)/content.html
use transient
set articles-html
# assemble whole articles
for a $(articles)
out $(a)/index.html
dep $(a)/content.html
dep $(a)/comments.html
dep $(srcdir)/articles/$(a)/meta
dep nav.html
dep templates/page.tt
dep templates/article.tt
cmd $(tool) mkarticle $(a)
add articles-html $(out)
add deployed-files $(out)
# archive indices
for a $(archives)
out $(a)/index.html
dep $(a)/index
dep $(archives.$a)/perex.html
dep nav.html
dep templates/page.tt
dep templates/archive.tt
cmd $(tool) mkarchive $(a)
add deployed-files $(out)
# link other resources
set resources style.css _comment.cgi
for res $(resources)
out $(res)
dep $(srcdir)/resources/$(res)
cmd /bin/ln -f $(dep) $(out)
add deployed-files $(resources) $(resized-images)
set all $(articles-html) $(images-all) $(archives)/index.html $(resources)
set install
for f $(deployed-files)
out install $(f)
dep $(f)
cmd /usr/bin/install -D -m 0644 $(dep) $(prefix)/$(f)
add install $(out)
include gib/bundle/boot.mk
#!/usr/bin/perl
# Expects a comment in the POST parameters
use strict;
use warnings;
use utf8;
use open qw/:std :encoding(utf-8)/;
use CGI::Simple;
use File::Slurper qw/read_text/;
use Data::Dumper;
use POSIX 'strftime';
use File::Temp qw/tempfile/;
$CGI::Simple::POST_MAX = 8192; # 8 Kib
$CGI::Simple::DISABLE_UPLOADS = 1;
$CGI::Simple::PARAM_UTF8 = 1;
#$CGI::Simple::DEBUG = 1;
my $q = CGI::Simple->new;
my @header = qw/-charset utf-8/;
my $dir = "_comments";
my $suffix = ".comment";
my $queue_limit = 50;
unless ( -t STDIN || ( $ENV{REQUEST_METHOD} // '' ) eq 'POST' )
{
my $status = '405 Method Not Allowed';
print $q->header( @header, -status => $status );
print_errorpage( $status );
exit 0;
}
# When no 'article' parameter has been given, try to extract the
# article id (slug) from the URI
my $article_slug = $ENV{REQUEST_URI} =~ s,/comment(\?.*)?$,,rn;
my %c = (
article => scalar $q->param( 'article' ) // $article_slug,
author => scalar $q->param( 'author' ),
body => scalar $q->param( 'body' ),
password => scalar $q->param( 'recog' ) // '',
parent => scalar $q->param( 'replyto' ) // '',
);
unless ( $c{article} && $c{article} =~ m,^[-/a-z0-9]+$,
&& $c{author} && 0 < length $c{author} < 64 && no_weird_chars( $c{author} )
&& $c{body} && 0 < length $c{body} < 8000 && no_weird_chars( $c{body} )
&& ( !$c{parent} || $c{parent} =~ /^[0-9]{14}\w{4}$/ ) )
{
my $status = '400 Bad Request';
print $q->header( @header, -status => $status );
print_errorpage( $status );
exit 0;
}
sanitise( $c{author} );
sanitise( $c{password} );
my $passed = $c{password} eq 'strasnetajneheslo'; # TODO
# Deny if the queue is full, unless the password is correct
my @ls = glob( "$dir/*$suffix" );
if ( !$passed && @ls >= $queue_limit )
{
my $status = '503 Service unavailable';
print $q->header( @header, -status => $status );
print_errorpage( $status );
exit 0;
}
$c{parent} ||= 'root'; # top-level comments
$c{body} =~ s/\v/\n/sg;
$c{body} =~ s/^\n*//s;
$c{body} =~ s/\n*$//s;
my @now = localtime;
my $date = strftime "%F %T %Z", @now;
my $slug = strftime "%Y%m%d%H%M%S", @now;
my ($fh, $filename) = tempfile( "${slug}XXXX", DIR => $dir, SUFFIX => $suffix );
my $commentid = $filename =~ s/^\Q$dir\E\/(.+)\Q$suffix\E$/$1/r;
unless ( -t STDIN ) {
my $gid = getgrgid "adamat";
chown -1, $gid, $filename;
chmod 0440, $filename;
}
my $comment = <<EOF;
Article: $c{article}
Author: $c{author}
Date: $date
Pass: @{[ $passed ? 'yes' : 'no' ]}\t$c{password}
Stamp: @{[time]}
Id: $commentid
Parent: $c{parent}
$c{body}
EOF
print $fh $comment;
close $fh;
print $q->header(
-status => '303 See Other',
-location => '/commented.html',
);
exit 0;
sub print_errorpage {
my $status = shift;
my ($code) = $status =~ /^(\d+)/;
my $file = "/var/www/errorpages/$code.html";
if ( -r $file ) {
print read_text( $file );
}
else {
print "Status $status\n";
}
}
sub no_weird_chars {
$_ = shift;
return /^[\p{Print}\n]*$/s;
}
sub sanitise {
for ( shift ) {
s/\s+/ /gs;
s/^ +//;
s/ +$//;
}
}
# vim: sts=4 et ts=8 sw=4
/* General - fonts and light theme colours */
html, body {
margin: 0;
padding: 0;
font-family: serif;
background-color: #ddd;
font-size: 12pt;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
body > *:first-child::before { content: ""; display: table; }
h1, h2, h3, h4, h5, h6 {
font-family: sans-serif;
}
a {
text-decoration: none;
color: #138;
}
a:hover {
text-decoration: underline;
}
#sidebar {
box-sizing: border-box;
margin: 0 auto;
padding: 1em;
}
#sidebar > header blockquote {
display: none;
}
#sidebar > header::after {
display: table;
content: "";
}
#navbutton {
display: block;
color: #138;
font-weight: bold;
border: 0.2em solid #138;
background-color: white;
aspect-ratio: 1;
padding: 0.5em;
box-sizing: content-box;
height: 1.6em;
float: right;
}
#navigation {
display: none;
font-family: sans-serif;
}
#navigation.open {
display: block;
}
#navigation nav {
margin-top: 3em;
}
#navigation menu {
margin-top: 0.6em;
padding: 0;
}
#navigation nav > header {
font-size: 110%;
font-weight: bold;
color: #333;
}
#navigation li {
list-style: none;
line-height: 2;
}
#navigation a {
color: #542;
}
#navigation li small {
color: #876;
font-size: 100%;
}
#content {
max-width: 48em;
margin: 0 auto;
line-height: 1.6;
}
#content > main > article {
background-color: white;
}
#content > main > header::before {
content: "";
display: table;
}
#content > main > header {
padding: 0 1em;
margin-top: -1em;
}
#content > main > header h1 {
line-height: initial;
margin-bottom: 1em;
}
main + footer {
margin-top: 1em;
padding: 1em;
background-color: rgba(0, 0, 0, 0.08);
color: #333;
}
article .banner {
display: block;
}
article .banner img {
width: 100%;
display: block;
aspect-ratio: 1.618;
object-fit: cover;
object-position: center center;
}
.article-text footer ul {
padding: 0;
}
.article-text footer li {
font-family: sans-serif;
display: block;
float: left;
}
.article-text footer li + li {
margin-left: 1em;
padding-left: 1em;
border-left: 0.1em solid #888;
}
.article-text > p,
.article-text > footer,
.article-text > header {
margin-left: 1em;
margin-right: 1em;
}
.article-text > p {
text-align: justify;
hyphens: auto;
}
.article-text footer::after {
content: "";
display: block;
clear: both;
}
.article-text .readmore a {
display: block;
padding: 0.5em 1em;
margin-top: 2em;
border-top: 0.2em dashed #ddd;
text-align: right;
font-family: sans-serif;
background-color: #f4f4f4;
}
.article-text #readmore {
padding-top: 1em;
border-bottom: 0.2em dashed #ddd;
margin-bottom: 1em;
}
.article-text p + #readmore {
margin-top: -1em;
}
/* Small (but not tiny) size */
@media screen and (min-width: 36em) {
#navigation nav {
float: left;
margin-top: 2em;
margin-bottom: 2em;
width: 33%;
padding: 2em;
box-sizing: border-box;
}
#navigation li {
line-height: 1.8;
}
#navigation::after {
content: "";
display: block;
clear: both;
}
#content > main > header,
#sidebar {
padding-left: 0;
padding-right: 0;
}
#sidebar,
#content {
width: 80%;
}
#content > main > article {
box-shadow: 0 0 1.5em rgba(0, 0, 0, 0.2);
}
main + footer {
margin-top: 2em;
}
}
/* Medium size (one side-bar) */
@media screen and (min-width: 60em) {
#sidebar {
float: left;
box-sizing: border-box;
width: 30%;
padding: 0 3em;
text-align: right;
background-color: unset;
}
#navbutton {
display: none;
}
#navigation {
display: block;
}
#navigation nav {
float: none;
margin-top: 4em;
margin-right: 0;
width: unset;
padding: 0;
}
#sidebar > header {
max-width: 20em;
margin: 0 0 0 auto;
}
#sidebar > header blockquote {
display: block;
margin-right: 0;
font-style: italic;
line-height: 1.6;
color: #333;
}
#content {
margin-top: 3em;
margin-left: 30.2%;
width: auto;
}
}
/* Large size (two side-bars) */
@media screen and (min-width: 90em) {
.article-text {
position: relative;
}
.article-text footer {
position: absolute;
top: -1em;
left: 100%;
width: 10em;
margin-right: 0;
margin-left: 1.5em;
line-height: 1.7;
}
.article-text footer a {
color: #542;
}
.article-text footer li {
display: block;
float: none;
}
.article-text footer li + li {
padding: 0;
margin: 0;
border: none;
}
}
[%- UNLESS archive.id == '.' %]
[%- SET title = "/$archive.id" %]
[%- END %]
[% WRAPPER page.tt -%]
[%- UNLESS archive.id == '.' %]
<header>
<h1>Archiv pro [% IF archive.title %][% archive.title %][% ELSE %]/[% archive.id %][% END %]</h1>
[% IF description %]<p>[% description %]</p>[% END %]
</header>
[%- END %]
[%- UNLESS articles %]
<div><p>Tato rubrika se jeví býti prosta příspěvků.</p></div>
[%- ELSE -%]
[%- FOREACH a IN articles %]
<article>
[%- IF a.banner %]
[%- SET banner_alt = "Obrázek v záhlaví" UNLESS banner_alt %]
<a class=banner href='[% root %]/[% a.id %]'>
[%- INCLUDE autoimg src="$a.id/$a.banner" alt="$a.banner_alt" -%]
</a>
[%- END %]
<div class='article-text'>
<header><h1><a href='[% root %]/[% a.id %]'>[% a.title %]</a></h1></header>
<footer>
<ul>
[% IF a.published %]<li><a href='[% root %]/[% a.id %]'>
<time datetime="[% isostamp(a.published) %]">[% datef(a.published) %]</time></a></li>[% END -%]
<li><a href='[% root %]/[% a.category %]'>/[% a.category %]</a></li>
<li><a href='[% root %]/[% a.id %]#comments'>[% IF a.ncomments %][% a.ncomments %]
komentář[%= plur(a.ncomments, '', 'e', 'ů') %][% ELSE %]bez komentáře[% END %]</a></li>
</ul>
</footer>
[% INSERT "$a.id/perex.html" | utf8 %]