#!/usr/bin/perl -T -w
#
# Addendat version 0.5
#
# Copyright 2001 Deekoo L., released under GPL v2.
#
# http://deekoo.net/technocracy/addendat/

delete($ENV{PATH});

use CGI -debug;
use Fcntl qw(:DEFAULT :flock); # import LOCK_* constants
use IPC::Open3;
use MIME::Base64;


# Your Addendat config file
# Configure this to point at where your Addendat config file will be
#   stored.
$configfile='/home/YOUR_USERNAME/addendat.cfg';

# Uncomment this to enable debugging.
#$DEBUG=1;

# End config.  Below this point, it's safe to leave everything unchanged.

$addendat_version='0.5';
$CGI::POST_MAX=1024 * 100;  # max 100K posts
$CGI::DISABLE_UPLOADS = 1;  # no uploads

@months=('January','February','March','April','May','June',
  'July','August','September','October','November','December');

$query=new CGI;

$hublog=$query->param('publicize');
$preview=$query->param('preview');
$blog=$query->param('blog');
if (!defined($blog)) {
  $blog='';
}
$debug=$query->param('debug');
if (!defined($debug)) {
  $debug=0;
}

undef ($/);
{
  package DefaultedHash;
  use Tie::Hash;

  sub TIEHASH()
  {
    my $self=shift;
    $hash{entryadded}='
<html><head><title>Addendat: Entry added</TITLE></HEAD>
<BODY
bgcolor="#000000" text="#00ff00" link="#00ffcc" vlink="#00cc66" alink="#ffffff">
<P>Entry added to <A HREF="%blogurl%">%blogurl%</A>.</P>
<P>Publicization: <PRE><B>
%response%
</B></PRE>
</P>
</BODY></HTML>';
    my %h=@_;
    my $k;
    foreach $k (keys(%h)) {
      $hash{$k}=$h{$k};
    }
    return bless \%hash, $self;
  }
  sub FETCH()
  {
    my $self=shift;
    my $key=shift;
    my $blog=$main::blog;
    while (defined($blog)) {
      if (exists($$self{"$key($blog)"})) {
        return $$self{"$key($blog)"};
      }
      if ($blog=~/\/./s) {
        $blog=~s/\/(.*?)$/\//s;
      } else {
        undef($blog);
      }
    }
    return $$self{$key};
  }
  sub EXISTS()
  {
    my $self=shift;
    my $key=shift;
    my $blog=$main::blog;
    while (defined($blog)) {
      if (exists($$self{"$key($blog)"})) {
        return exists($$self{"$key($blog)"});
      }
      if ($blog=~/\/./s) {
        $blog=~s/\/(.*?)$/\//s;
      } else {
        undef($blog);
      }
    }
    return exists($$self{$key});
  }
}
tie (%template,'DefaultedHash',&read_config_file());
&init_runtime_mcis();

$action=$query->param('action');
if (defined($action)) {
  if ($action eq 'comment') {
    print "Content-type: text/html\n\n";
    print &expand_mcis($template{commentform});
    print "\n";
    exit;
  }
}

$accesscode=$query->param('accesscode');
if (!exists($template{accesscode})) {
  $template{accesscode}='';
}
if (!defined($accesscode)) {
  $accesscode='';
}
if ($accesscode ne $template{accesscode}) {
  &fatalerror("Invalid access code.");
}

&add_entry();

exit;

sub read_config_file()
{
  local *CFG;
  local $file;
  local $tag;
  local %template;
  open(CFG,"<$configfile") || &fatalerror("Cannot open $configfile.");
  $file=<CFG>;
  close(CFG);
  while ($file=~/\<%(.*?)%\>(.*?)\<\/%(.*?)%\>/s) {
    $tag=$1;
    if ($file=~/\<%\Q$tag\E%\>(.*?)\<\/%\Q$tag\E%\>/s) {
      $template{$tag}=$1;
      $file=~s/\<%\Q$tag\E%\>(.*?)\<\/%\Q$tag\E%\>//s;
    } else {
      &fatalerror("Malformed pseudotag $tag");
    }
  }
  return %template;
}

sub fatalerror()
{
  print "Content-type: text/plain\n\nFatal error: $_[0]\n\n";
  print "Debug information: blog is $blog.\n";
  die;
}

sub open_html_file()
{
#  local *FILE;
  local $err=1;
  local $file=&expand_mcis($template{blogfile});
  if ($file=~/^([-\/a-zA-Z.0-9_~]+)$/s) {
    if ($file=~/\.\./s) {
      &fatalerror("'..' found in blog filename '$file'.  Aborting.");
    }
    $file=$1;
  } else {
    &fatalerror("Invalid characters in filename '$file'.  Supported characters\n
are -, /, a-z, A-Z, 0-9, ., _, and ~.  If you think the character you wanted\n
to use that's not in this set should be considered safe, please email a bug\n
report to deekoo\@tentacle.net.\n"); 
  }
  if (sysopen(FILE,$file,O_RDWR|O_NOFOLLOW)) {
    if (flock(FILE,LOCK_EX)) {
      $err=0;
    } else {
      &fatalerror("Cannot lock $file");
    }
  } else {
    if (exists($template{blankfile})) {
      if (sysopen(FILE,$file,O_CREAT|O_RDWR|O_NOFOLLOW|O_EXCL)) {
        if (flock(FILE,LOCK_EX)) {
          if (print FILE &expand_mcis($template{blankfile})) {
            if (seek(FILE,0,0)) { 
              # Only try to chmod the file if you're running as a UID who should.
              if (exists($template{chmodusers}) && (getpwuid($<)=~/$template{chmodusers}/s)) {
                # Sanitize mode to ensure that config files don't make suid
                # or executable bloglets.
                chmod(oct($template{newfilemode}) & 0666,$file)
                  || &fatalerror("Could not chmod the new file sanely.");
              }
              $err=0;
            } else {
              &fatalerror("seek($file,0,0) failed.");
            }
          } else {
            &fatalerror("Cannot write new file to '$file'.");
          }
        } else {
          &fatalerror("Cannot lock $file");
        }
      } else {
        &fatalerror("Unable to create blog file '$file'.  Check the perms on\nthe directory to ensure that addendat script is allowed\nto create files in it.\n");
      }
    } else {
      &fatalerror("Cannot open $file".
        ' and no blankfile specified in template.');
    }
  }
  if ($err) {
    &fatalerror("Cannot open ".&expand_mcis($template{blogfile}));
  }
  $file=<FILE>;
#  close(FILE);
  @entries=();
  if ($file=~/^(.*?)\<!-- Entry \($blog\) --\>(.*)\<!-- \/Entry \($blog\) --\>(.*?)$/s) {
    $header=$1;
    $footer=$3;
    while ($file=~/\<!-- Entry \($blog\) --\>(.*?)\<!-- \/Entry \($blog\) --\>/s) {
      push @entries,$1;
      $file=~s/\<!-- Entry \($blog\) --\>(.*?)\<!-- \/Entry \($blog\) --\>//s;
    }
  } elsif ($file=~/^(.*?)\<!-- Add Here \($blog\) --\>(.*?)$/s) {
    $header=$1;
    $footer=$2;
  } elsif ($file=~/^(.*?)\<!-- Entry --\>(.*)\<!-- \/Entry --\>(.*?)$/s) {
    $header=$1;
    $footer=$3;
    while ($file=~/\<!-- Entry --\>(.*?)\<!-- \/Entry --\>/s) {
      push @entries,$1;
      $file=~s/\<!-- Entry --\>(.*?)\<!-- \/Entry --\>//s;
    }
  } elsif ($file=~/^(.*?)\<!-- Add Here --\>(.*?)$/s) {
    $header=$1;
    $footer=$2;
  } else {
    &fatalerror("Unable to find blog entries or 'Add Here' comment.  Cannot add.");
  }
  # Note: If you want whitespace below the last entry, do not use literal
  # newlines if you've got the whole page wrapped in a pre/xmp.  Rather,
  # close your pre/xmp and start a new one.
  $footer=~s/^[\n\r]//gms;
}

sub init_runtime_mcis()
{
  local @date=localtime(time);
  $date[5]+=1900;
  $runtime_mcis{minutes}=$date[1];
  $runtime_mcis{hour}=$date[2];
  $runtime_mcis{year}=$date[5];
  $runtime_mcis{hexyear}=sprintf("0x%X",$date[5]);
  $runtime_mcis{month}=$months[$date[4]];
  $runtime_mcis{monthnum}=$date[4]+1;
  $runtime_mcis{day}=$date[3];
  $runtime_mcis{hexday}=sprintf("0x%X",$date[3]);
  if (exists($template{entryidfile})) {
    local *EID;
    sysopen(EID,"$template{entryidfile}",O_RDWR|O_NOFOLLOW) ||
      &fatalerror("Cannot open entry-ID storage file $template{entryidfile}.");
    flock(EID,LOCK_EX) ||
      &fatalerror("Cannot lock entry-ID storage file.");
    local $/="\n";
    local $day=<EID>;
    chomp($day);
    $runtime_mcis{entrynum}=<EID>;
    chomp($runtime_mcis{entrynum});
    local $today=&expand_mcis($template{entryiddateformat});
    if ($day eq $today) {
      $runtime_mcis{entrynum}++;
    } else {
      print STDERR "$day ne $today\n";
      $day=$today;
      $runtime_mcis{entrynum}=0;
    }
    $runtime_mcis{hexentrynum}=sprintf("0x%X",$runtime_mcis{entrynum});
    seek(EID,0,0) || &fatalerror("Error seeking in $template{entryidfile}");
    print EID "$day\n$runtime_mcis{entrynum}\n";
    truncate(EID,tell(EID)) || &fatalerror("Error truncating $template{entryidfile}");
    flock(EID,LOCK_UN);
    close(EID);
    $runtime_mcis{entryid}=&expand_mcis($template{entryidformat});
  }
}

sub expand_mcis()
{
  # args: 0: Text.
  local $text=$_[0];
  local $mci;
  local $param;
  local $whiled=0;
  local %handled;
  while ($text=~/%([a-z]+)%/s) {
    $mci=$1;
    if ($DEBUG) {
      print STDERR "MCI: '$mci'\nText: \"$text\"\n";
      print STDERR $query->param();
    }
    if (exists($handled{$mci})) {
      $text=~s/%$mci%/\%Looping mci $mci\%/gs;
    }
    $param=$query->param($mci);
    if (defined($param)) {
      $text=~s/%$mci%/$param/sg;
      if ($DEBUG) {
        print STDERR "$mci found as CGI parameter ($param)\n";
      }
    } elsif (exists($template{$mci})) {
      $text=~s/%$mci%/$template{$mci}/gs;
    } elsif (exists($runtime_mcis{$mci})) {
      $text=~s/%$mci%/$runtime_mcis{$mci}/gs;
    } else {
      if ($DEBUG) {
        print STDERR "Undefined MCI $mci encountered.\n";
      }
      $runtime_mcis{$mci}="\%MCI $mci not defined.%";
    }
    $handled{$mci}=1;
    $whiled++;
    if ($whiled>500) {
      $text=~s/%([a-z]+)%/ \%Excessive recursion in %-code expansion.% /gs;
    }
  }
  if (exists($template{linkprefix})) {
    $text=~s/\<A(.*?)HREF=\"([-a-zA-Z0-9~_\%\/.]+)\"/\<A$1HREF=\"$template{linkprefix}$2\"/sig;
  }
  return $text;
}

sub publicize()
{
  local *RFH;
  local *WFH;
  local *EFH;
  if ($debug) {
    print STDERR "Publicizing\n";
  }
  $hubdat='url='.&urlescape($template{blogurl}).'&blog='.
    &urlescape($template{blogtitle});
  open3(\*WFH,\*RFH,\*EFH,'telnet',$template{hubmachine},80);
  print WFH "GET $template{hubscript}?$hubdat HTTP/1.1\012Host: $template{hubmachine}\012User-Agent: Addendat/$addendat_version\012Connection: Close\012\012";
  local $/;
  local $response=<RFH>;
  $response=~s/(.*?)\n\n//s;
  if ($response=~s/^([0-9A-Fa-f]+) \012//sm) {
    local $contentlen=hex($1);
    $response=substr($response,0,$contentlen);
  }
  $runtime_mcis{response}=$response;
  close(WFH);
  close(RFH);
}

sub urlescape()
{
  local $url=$_[0];
  local $ret;
  local $x=0;
  local $c;
  local $n;
  while ($x<length($url)) {
    $c=substr($url,$x,1);
    if ($c=~/^[a-zA-Z\/]$/) {
      $ret.=$c;
    } elsif ($c eq ' ') {
      $ret.='+';
    } else {
      $n=ord($c);
      if ($n<16) {
        $ret.='%0'.sprintf("%X",$n);
      } else {
        $ret.='%'.sprintf("%X",$n);
      }
    }
    $x++;
  }
  return $ret;
}

sub add_entry()
{
  if ($debug) {
    print STDERR "Adding entry to '$blog'\n.";
  }
  &add_entry_to_leaf($blog);
  local $leaf;
  # This may move into add_entry_to_leaf() sometime, thus allowing leaves
  # to have leaves.
  if (exists($template{leaves})) {
    if ($debug) {
      print STDERR "Leaves found.\n.";
    }
    local @leaves=split(/,/,$template{leaves});
    foreach $leaf (@leaves) {
      &add_entry_to_leaf($leaf);
    }
  }
  if ($debug) {
    print STDERR "Hublog is '$hublog'.\n";
  }
  if ($hublog) {
    &publicize();
  } else {
    $runtime_mcis{response}='[Publicization not relevant]';
  }
  print "Content-type: text/html\n\n";
  print &expand_mcis($template{entryadded});
  print "\n";
}

sub propagate_entry_http()
{
  local $prot;
  local $server;
  local $path;
  local $port;
  use Socket;
  local $blogfile=&expand_mcis($template{blogfile});
  if ($blogfile=~/^(http(|s)):\/\/([-_a-z.A-Z]+)(|:[0-9]+)(\/(.*?))$/) {
    $prot=$1;
    $server=$3;
    $port=$4;
    $path=$5;
    if (!$port) {
      if ($prot eq 'http') {
        $port=80;
      } else {
        $port=443;
      }
    }
    print STDERR "$prot, $server, $port, $path\n";
  } else {
    &fatalerror("Invalid http(s) URL syntax in $blogfile\n");
  }
  local $ip=inet_aton($server);
  if (!defined($ip)) {
    &fatalerror("Could not resolve $server\'s address.");
  }
  $dest=sockaddr_in($port, $ip);
  if ($prot ne 'http') {
    &fatalerror("$prot form propagation not yet supported.");
  }
  local *HTTP;
  if (!socket(HTTP, &AF_INET(), &SOCK_STREAM(), 0)) {
    &fatalerror("Socket failed.");
  }
  if (!connect(HTTP, $dest)) {
    &fatalerror("Could not connect to $server:$port.\n");
  }
  select(HTTP); $|=1; select(STDOUT);
  print HTTP "POST $path HTTP/1.1\015\012User-agent: Addendat/$addendat_version\015\012Host: $server\015\012Connection: close\015\012";
  if (exists($template{httpusername}) && exists($template{httppassword})) {
    local $auth=encode_base64("$template{httpusername}:$template{httppassword}");
    $auth=~s/[\012\015]//gs;
    print HTTP "Authorization: Basic $auth\015\012";
  }
  local $formentry=&expand_mcis($template{entry});
  print HTTP "Content-type: application/x-www-form-urlencoded\015\012Content-length: ",length($formentry),"\015\012\015\012";
  print HTTP "$formentry\015\012";
#  print HTTP "Content-type: application/x-www-form-urlencoded; boundary=vnorfle\015\012",
#    "Content-length: ",length($formentry)+28,"\015\012\015\012",
#    #sprintf("%x",length($formentry)),
#    "\015\012--vnorfle\015\012$formentry\015\012--vnorfle--\015\012\015\012";
  close(HTTP);
}

sub add_entry_to_leaf()
{
  local $oldblog=$blog;
  $blog=$_[0];
  if ($debug) {
    print STDERR "Adding entry to leaf '$blog'\n";
  }
  if ($template{blogfile} =~ /^http(s|):/) {
    &propagate_entry_http();
    return;
  }
  &open_html_file();
  if ($preview) {
    &fatalerror("Previewing not yet implemented.");
  }
  if ($template{add} eq 'top') {
    @entries=&trim_entries(@entries);
    unshift(@entries,&expand_mcis($template{entry}));
  } elsif ($template{add} eq 'bottom') {
    @entries=&trim_entries(@entries);
    push(@entries,&expand_mcis($template{entry}));
  } else {
    &fatalerror('$template{add} is '."'$template{add}'.
      Only 'top' and 'bottom' are understood by the current version of
      Addendat.");
  }
  $html=$header;
  if ($item=shift @entries) {
    $html.="<!-- Entry ($blog) -->$item<!-- /Entry ($blog) -->\n";
    foreach $item (@entries) {
      $html.="$template{entryseparator}<!-- Entry ($blog) -->$item<!-- /Entry ($blog) -->\n";
    }
  } else {
    $html.="<!-- Add Here ($blog) -->\n";
  }
  $html.=$footer;
#  sysopen(FILE,&expand_mcis($template{blogfile}),O_NOFOLLOW|O_RDWR) ||
#    &fatalerror("Cannot write blog file '".&expand_mcis($template{blogfile})."'.");
#  flock(FILE,LOCK_EX)||&fatalerror("Cannot lock blog.");
  seek(FILE,0,0);
  print FILE $html;
  $htmllen=length($html);
  if ($htmllen=~/([0-9]+)/) {
    $htmllen=$1;
  } else {
    &fatalerror("Cannot untaint '$htmllen'.  Should be a number.\n");
  }
  truncate(FILE,$htmllen);
  flock(FILE,LOCK_UN);
  close(FILE);
  $blog=$oldblog;
}

sub trim_entries()
{
  local $maxlen=$template{length};
  if ((!defined($maxlen)) || ($maxlen eq '')) {
    return @_;
  }
  local $numlen;
  if ($maxlen=~/^([0-9]+)/) {
    $numlen=$1;
  } else {
    return @_;
  }
  $maxlen=~s/$numlen //s;
  if ($maxlen eq 'characters') {
    return &trim_entries_characters($numlen,@_);
  } else {
    &fatalerror("Invalid entry list trim type: '$template{length}'");
  }
}

sub trim_entries_characters()
{
  local $entry;
  local $len=0;
  local $maxlen=shift;
  foreach $entry (@_) {
    $len+=length($entry);
  }
  if ($template{add} eq 'top') {
    while ($len>$maxlen) {
      $len-=length(pop(@_));
    }
  } elsif ($template{add} eq 'bottom') {
    while ($len>$maxlen) {
      $len-=length(shift(@_));
    }
  } else {
    &fatalerror("%add% must be top or bottom.");
  }
  return @_;
}
