#!/usr/bin/perl -wT
#
# txt2dat 0.2.0 online interface
# 4 March 2007
# Jim DeVona
# http://anoved.net/txt2dat.html
#
# Just a web front end for Ross Crawford's txt2dat utility,
# which converts text strings to LDraw stickers:
# http://www.br-eng.info/about/lego/LDraw/txt2dat/index.htm
#
# The accompanying HTML form defines the default values, if any, for
# each txt2dat argument. Parameters that are not defined are omitted.
#
# Version history:
#
# - 0.2.0 (13 March 2007) - adopted txt2dat version number,
# added description field,
# match -f to assigned filename
#
# Issues:
#
# - Font uploading is the iffiest thing here, what with tmp files and all
# - Forking and fork error catching are new tricks, so I may be tricked
#
# License:
#
# txt2dat is a separate work to which the following terms do not apply.
# You can do whatever you to do with this script.
# Improvements are encouraged.
# No support is promised.
#
use strict;
use CGI;
use CGI::Carp qw/ fatalsToBrowser /;
use File::Temp qw/ tempfile tempdir /;
# Configuration
use constant MAX_KB_UPLOAD => 150;
use constant MAX_BASE_FILENAME => 25;
use constant TXT2DAT => '/home/anoved/bin/txt2dat';
#use constant TXT2DAT => '/usr/local/bin/txt2dat';
use constant TXT2DAT_HOME => '../txt2dat.html';
use constant FONT1 => '/home/anoved/fonts/verasans.ttf';
use constant FONT2 => '/home/anoved/fonts/veraserif.ttf';
use constant FONT3 => '/home/anoved/fonts/nasaliza.ttf';
#use constant FONT1 => '/Users/anoved/Sites/fonts/verasans.ttf';
#use constant FONT2 => '/Users/anoved/Sites/fonts/veraserif.ttf';
#use constant FONT3 => '/Users/anoved/Sites/fonts/nasaliza.ttf';
# Allow but restrict uploads
$CGI::DISABLE_UPLOADS = 0;
$CGI::POST_MAX = 1024 * MAX_KB_UPLOAD;
# parse CGI request
my $q = new CGI;
if ($q->cgi_error) {
t2d_error($q, $q->cgi_error . ' (maximum upload size is ' . MAX_KB_UPLOAD . ' kilobytes)');
}
# tx2dat is clearly a prerequisite
if (not -e TXT2DAT or not -x TXT2DAT) {
t2d_error($q, 'Cannot find executable ' . TXT2DAT);
}
my @opts;
# sticker text is the only strictly required parameter
if (not defined $q->param('s')) {
t2d_error($q, 'Missing sticker text (parameter s not defined)');
}
if ('' eq $q->param('s')) {
t2d_error($q, 'No sticker text entered');
}
if ($q->param('s') =~ /^([\w .!?-]+)$/) {
# untainted sticker text
push @opts, '-s', $1;
} else {
# crudely restrict input
t2d_error($q, 'Sorry, text is currently restricted to upper or lower case letters A to Z, digits 0 to 9, spaces, and the following punctuation: .!?- (download txt2dat to your computer to circumvent this online restriction)');
}
# font color
if (defined $q->param('c')) {
# get raw color code
my $c_taint;
if ('other' eq $q->param('c')) {
if (not defined $q->param('oc')) {
t2d_error($q, 'Other font color requested but not defined (missing oc parameter)');
} else {
$c_taint = $q->param('oc');
}
} else {
$c_taint = $q->param('c');
}
# validate and untaint color code
if ('-' eq $c_taint) {
push @opts, '-c', '-';
} else {
if ($c_taint =~ /^(\d+)$/) {
if ($1 < 0 || $1 > 511) {
# restrict range
t2d_error($q, 'Font color must be an integer no less than 0 and no greater than 511');
} else {
# untainted ldraw color code
push @opts, '-c', $1;
}
} else {
# not a number (txt2dat accepts negative color codes, but we don't)
t2d_error($q, 'Invalid characters in font color (digits 0 to 9 only)');
}
}
}
# font size
if (defined $q->param('p')) {
# get raw point size
my $p_taint;
if ('other' eq $q->param('p')) {
if (not defined $q->param('op')) {
t2d_error($q, 'Other font size requested but not defined (missing op parameter)');
} else {
$p_taint = $q->param('op');
}
} else {
$p_taint = $q->param('p');
}
# validate and untaint point size
# based on http://www.regular-expressions.info/floatingpoint.html
if ($p_taint =~ /^([0-9]*\.?[0-9]+)$/) {
if ($1 < 1) {
t2d_error($q, 'Font size must be greater than or equal to 1');
} else {
push @opts, '-p', $1;
}
} else {
t2d_error($q, 'Invalid characters in font size (. with digits 0 to 9 only)');
}
}
# spline resolution
if (defined $q->param('r')) {
# get raw resolution
my $r_taint;
if ('other' eq $q->param('r')) {
if (not defined $q->param('or')) {
t2d_error($q, 'Other spline resolution requested but not defined (missing or parameter)');
} else {
$r_taint = $q->param('or');
}
} else {
$r_taint = $q->param('r');
}
# validate and untaint resolution
if ($r_taint =~ /^([0-9]*\.?[0-9]+)$/) {
if ($1 < 0) {
t2d_error($q, 'Spline resolution should be greeater than or equal to 0');
} else {
push @opts, '-r', $1;
}
} else {
t2d_error($q, 'Invalid characters in spline resolution (. with digits 0 to 9 only)');
}
}
# custom description
if ((defined $q->param('dd')) && ('ON' eq $q->param('dd'))) {
if (defined $q->param('d')) {
if ($q->param('d') =~ /^([\w .!?-]+)$/) {
push @opts, '-d', $1;
} else {
t2d_error($q, 'Sorry, descriptions are currently restricted to upper or lower case letters A to Z, digits 0 to 9, spaces, and the following punctuation: .!?- (download txt2dat to your computer to circumvent this online restriction)');
}
}
}
# edges
if ((not defined $q->param('ll')) || ('ON' ne $q->param('ll'))) {
# this is a reverse switch - we set the flag when the checkbox is NOT on
push @opts, '-l';
}
# bounding box
if ((defined $q->param('xx')) && ('ON' eq $q->param('xx'))) {
# look at parameters specific to the bounding box
# width
if ((defined $q->param('dw')) && ('ON' eq $q->param('dw'))) {
if (defined $q->param('w')) {
if ($q->param('w') =~ /^([0-9]*\.?[0-9]+)$/) {
if ($1 < 1) {
t2d_error($q, 'Width should be greater than or equal to 1');
} else {
push @opts, '-w', $1;
}
} else {
t2d_error($q, 'Invalid characters in width (. with digits 0 to 9 only)');
}
}
}
# height
if ((defined $q->param('dh')) && ('ON' eq $q->param('dh'))) {
if (defined $q->param('h')) {
if ($q->param('h') =~ /^([0-9]*\.?[0-9]+)$/) {
if ($1 < 1) {
t2d_error($q, 'Height should be greater than or equal to 1');
} else {
push @opts, '-h', $1;
}
} else {
t2d_error($q, 'Invalid characters in height (. with digits 0 to 9 only)');
}
}
}
# background color
if (defined $q->param('b')) {
# get raw color code
my $b_taint;
if ('other' eq $q->param('b')) {
if (not defined $q->param('ob')) {
t2d_error($q, 'Other background color requested but not defined (missing ob parameter)');
} else {
$b_taint = $q->param('ob');
}
} else {
$b_taint = $q->param('b');
}
# validate and untaint color code
if ('-' eq $b_taint) {
push @opts, '-b', '-';
} else {
if ($b_taint =~ /^(\d+)$/) {
if ($1 < 0 || $1 > 511) {
# restrict range
t2d_error($q, 'Background coor must be an integer no less than 0 and no greater than 511');
} else {
# untainted ldraw color code
push @opts, '-b', $1;
}
} else {
# not a number (txt2dat accepts negative color codes, but we don't)
t2d_error($q, 'Invalid characters in background color (digits 0 to 9 only)');
}
}
}
} else {
# indicate no bounding box
push @opts, '-x';
}
# internal or uploaded font
if (defined $q->param('font')) {
if ('0' eq $q->param('font')) {
# no argument to add for default font
} elsif ('1' eq $q->param('font')) {
push @opts, FONT1;
} elsif ('2' eq $q->param('font')) {
push @opts, FONT2;
} elsif ('3' eq $q->param('font')) {
push @opts, FONT3;
} elsif ('other' eq $q->param('font')) {
if (not defined $q->param('ofont')) {
t2d_error($q, 'Other font requested but not defined (missing ofont parameter)');
} else {
# create temp file in tmp directory; *should* be auto-deleted
my $tmp_dir = tempdir(CLEANUP=>1) or t2d_error($q, 'CGI cannot create temporary directory for uploaded font');
my ($tmp_fh, $tmp_fontfile) = tempfile(DIR=>$tmp_dir) or t2d_error($q, 'CGI cannot create temporary file for uploaded font');
# temporarily save uploaded font so txt2dat can have at it
my $cgi_fh = $q->upload('ofont') or t2d_error($q, 'Cannot access uploaded font');
binmode $cgi_fh;
binmode $tmp_fh;
my $tmp_buffer = ''; # arbitrary buffer size
while (read($cgi_fh, $tmp_buffer, 1024)) {
print $tmp_fh $tmp_buffer;
}
close $tmp_fh;
# tell txt2dat where to get it
push @opts, $tmp_fontfile;
}
} else {
t2d_error($q, 'Inalid font parameter (should be 0, 1, 2, 3, or other)');
}
}
# devise an output filename
my $filename = '';
my $taintname = $q->param('s');
$taintname =~ s/[^\w]//g;
if ($taintname =~ /^(\w+)$/) {
$filename .= substr($1,0,MAX_BASE_FILENAME);
} else {
$filename .= 'sticker';
}
$filename .= '.dat';
# tell txt2dat what we're naming the sticker
push @opts, '-f', $filename;
# fork open a pipe to a clone of ourselves
# this technique of forking to exec without risking a shell is
# derived from examples in O'Reilly's "CGI Programming with Perl" 2nd ed.
my $pipe;
my $pid = open $pipe, "-|";
if (not defined $pid) {
t2d_error($q, "CGI cannot fork or open pipe to fork ($!)");
}
# the parent gets the child's pid while the child gets zero
# end of the line for the child fork as it assumes the identity of txt2dat
if ($pid == 0) {
$ENV{'PATH'} = '';
exec TXT2DAT, @opts or exit 1;
}
# if the pipe exits with nothing assume child fork failed
my $firstline = <$pipe>;
if (("" == $firstline) && (eof $pipe)) {
t2d_error($q, 'CGI cannot execute ' . TXT2DAT . ' ' . join(' ',@opts));
}
# otherwise original process continues to channel txt2dat's output to user
print $q->header(-type => 'application/x-ldraw', -attachment => $filename);
print $firstline;
print while <$pipe>;
close $pipe;
exit;
# print minimal HTML formatted error message and exit
sub t2d_error {
my ($q, $msg) = @_;
print $q->header('text/html'),
$q->start_html('txt2dat Error'),
$q->h1('txt2dat Error:'),
$q->h2($msg),
$q->p('Please return to ' . $q->a({-href=>TXT2DAT_HOME},'txt2dat') . ' and verify your input.'),
$q->end_html;
exit 1;
}