#!/usr/local/bin/perl # # DBMEdit 1.0 # # Add, edit, or delete records from a DBM file, via a Web interface. # The DBM records are assumed to be several fields concatenated into # one long string with some delimiting string (default is "\0"). # # This program displays a DBM database as a table, and provides # auto-sized forms to add or edit records. It protects data reasonably # well in multi-user situations. # # # TO INSTALL/CONFIGURE: # # 1) Put this script where you want it. # # 2) Create a directory below that called "data/" (the name is # user-configurable, below). # # 3) Put your existing DBM files in the data/ directory, or make # symbolic links in there that point to the real DBM files. # # 4) Figure out how you want to handle permissions. The Web server # must be able to write the files in data/, and the data/ directory # itself must be writable if you want to create new files in it # via this script. # # Note that if data/ is writable by the Web server's user, then any # local hacker with CGI can overwrite your data. If you work around # this with setgid or setuid, see the security note in the # USER CONFIGURATION section below. # # !! 5) PASSWORD-PROTECT THE URL OF THIS SCRIPT! Otherwise, anyone can # edit your DBM files-- probably not what you want. Also, set # @ALLOWED_USERS in the USER CONFIGURATION section below. # # Security within the script is limited at best; it relies on the # authentication of whoever's running the script. # # # TO USE: # # You can create new DBM databases with this program, or edit existing # ones that follow the same field-delimiting scheme. # # Define each database by the DBM filename, the list of column names, # and the delimiter between fields within each record. This database # definition is saved in the URL, so you can bookmark it directly or # put it in an HTML link. # # FILENAME: # Leave out the extension. Don't point into another directory. # # COLUMNS: # Comma-separated list of text strings, for display only. Each # column name may be followed by a ':' and optional one-letter # flags. Currently supported flags are: # r read-only (for convenience only, NOT security) # t textarea (multi-line) input instead of one-line input # # Example column list: # # Name, Birthdate:r, Favorite Quote:t # # DELIMITER: # Any string of characters can be used. Express it as a list of # ASCII codes, as decimal numbers. For example, CRLF is "13 10". # The default is one null character, which is "0". # # Note that the data in the database fields can't contain the # delimiter string, or the database will get messed up. If you need # to put arbitrary binary data in a field, use a long sequence of # random bytes here, like "188 45 217 206 51". Five bytes means # you'll mess it up about once for every terabyte (1000 GB) you # store. # # Once you've defined and loaded your database, be sure to bookmark the # full URL or copy it to an HTML link. # # # Further comments are at the end of this file. # # written by James Marshall, james@jmarshall.com # see http://www.jmarshall.com/tools/dbmedit/ for more info # #----- USER CONFIGURATION (NORMALLY UNNEEDED) ------------------------ # For security, only let this script modify DBMs in a certain directory. # If you have DBMs all over the place, put symbolic links in this # directory to point to the actual locations. # This directory must be accessible by the uid that the Web server runs as. $DATA_DIR= 'data' ; # Set this to a list of allowed usernames, to restrict who REMOTE_USER can # be, or leave empty for no restrictions. This guards against a few # potential security holes. For example, someone could make a symbolic # link to your copy of this script, bypassing any password-protection. @ALLOWED_USERS= qw( ) ; # If you run this setuid or setgid, there is a slight security risk # of someone running this from the command line in another directory # with certain symbolic links, and potentially modifying DBM files # in other directories of yours. If you care about this, then set # one or both of the following two variables. # Username or UID the Web server runs under (either will work). # If you set this, the program will verify this is the real user running it. $WEB_SERVER_USER= '' ; # Directory where this program should be run, i.e. where it lives. # If you set this, the program will chdir to the directory before running. $HOME_DIRECTORY= '' ; # The delimiter between fields in the DBM file, if none is specified. $DEFAULT_DELIM= "\0" ; #----- END OF (USEFUL) USER CONFIGURATION ---------------------------- use Fcntl qw(:DEFAULT :flock) ; use NDBM_File ; # Where all the lock files go. This will be created if it doesn't exist. $lockdir= "$DATA_DIR/locks" ; # The default title for simple error responses $errtitle= "$0 error" ; # Guard against unauthorized access, if needed if (@ALLOWED_USERS) { &HTMLdie("Sorry, you're not authorized to run this script.") unless grep( ($_ eq $ENV{'REMOTE_USER'} ), @ALLOWED_USERS ) ; } # Guard against a slim security hole chdir($HOME_DIRECTORY) || &HTMLdie("Couldn't chdir: $!") if $HOME_DIRECTORY ne '' ; # Guard against a slim security hole, take 2 if ($WEB_SERVER_USER ne '') { # First, convert to numeric UID if needed $WEB_SERVER_USER= getpwnam($WEB_SERVER_USER) if $WEB_SERVER_USER=~ /\D/ ; &HTMLdie("Access forbidden.") unless ( $WEB_SERVER_USER == $< ) ; } %in= &getcgivars ; $in{'file'}=~ s/(^\s*|\s*$)//g ; # standardize on no leading/trailing blanks $in{'referer'}||= $ENV{'HTTP_REFERER'} ; &displaystartform unless $in{'file'} ; # Only allow files with no paths. # Heck, only allow word chars for now. &HTMLdie("The filename '$in{'file'}' is not allowed.") if ($in{'file'}=~ m#/|\.\.#) || ($in{'file'}=~ /[^\w.-]/) ; # Homespun lock mechanism-- can't figure out how to use flock() on DBM file :( # Make a lock file to get a lock on-- safer for interruptable processes. mkdir($lockdir, 0777) || &HTMLdie("Couldn't create lock directory: $!") unless -e $lockdir ; chmod(0777, $lockdir) ; # otherwise, it's tough to get rid of $lockfile= "$lockdir/$in{'file'}.lock"; # safe because $in{'file'} is safe system('touch', $lockfile) unless -e $lockfile ; open(DB_LOCK, ">$lockfile") || &HTMLdie("Couldn't open lockfile: $!") ; # For some reason, LOCK_SH doesn't always work-- gets "Bad file number". :P # So, we'll just do an exclusive lock for everything. Best we can do. :( flock(DB_LOCK, LOCK_EX) || &HTMLdie("Couldn't get lock: $!") ; # $now is saved in the form, and is used for safe updates. # Note that file will not be modified until at least the end of this script, # so $now is "equivalent" to the time the form will be generated. $now= time ; # for (@goodmen) tie %dbdata, 'NDBM_File', "$DATA_DIR/$in{'file'}", O_RDWR|O_CREAT, 0664 ; # Used to test modification time for safe updates. # DBM filenames vary, so see which files exist. Try .pag, else take .db. # What other extensions are created with DBMs? $dbfilename= -e "$DATA_DIR/$in{'file'}.pag" ? "$DATA_DIR/$in{'file'}.pag" : "$DATA_DIR/$in{'file'}.db" ; # Perhaps we should allow the user to read the file even if it's not # writable? To do so, set $topmsg, and alter the flags on "tie", above. &HTMLdie("Web server couldn't create DBM file.") unless -e $dbfilename ; &HTMLdie("DBM file isn't readable by Web server.") unless -r $dbfilename ; &HTMLdie("DBM file isn't writable by Web server.") unless -w $dbfilename ; &calcglobals ; #----- end of initialization, main block below ----------------------- # a catch-all way to cancel actions: show message and do default command if ($in{'noconfirm'}) { $topmsg= "
Tip: Creative use of the forward and back browser buttons can be very
helpful here, to view current data or recover lost data.
EOF
&printfooter ;
exit ; # hmm, not the cleanest
}
# Copy "in_nnn" fields into $dbdata{$in{'key'}} (used by add and update)
# Currently, this does NOT fill in gaps in data, e.g. (in_001, in_003) is
# only two fields, not three with a blank one in the middle.
sub putfieldstodb {
# create full data string, removing empty fields at the end
my(@field)= sort grep(/^in_\d\d\d$/, keys %in) ;
$#field-- while ( ($#field>0) && !length($in{$field[$#field]}));
# Normalize raw CRLF into LF, to accommodate brain-dead OS's
foreach (@field) { $in{$_}=~ s/\r\n/\n/g }
$dbdata{$in{'key'}}= join($delim, map { &slashunescape($_) } @in{@field}) ;
}
#----- translation to/from slash-escaped string format ---------------
# unescape the user input into raw data
sub slashunescape {
my($s)= @_ ;
$s=~ s/(\\(n|r|t|f|b|a|e|0(?!\d\d)|\d\d\d|x[0-9A-Fa-f]{2}|c.|\\))/
eval qq(\"$1\") /ge ;
return $s ;
}
# use backslashes to escape string, to make it suitable for input form
sub slashescape {
my($s)= @_ ;
$s=~ s/\\/\\\\/g ;
$s=~ s/\n/\\n/g ;
$s=~ s/\r/\\r/g ;
$s=~ s/\t/\\t/g ;
$s=~ s/\f/\\f/g ;
$s=~ s/\x08/\\b/g ;
$s=~ s/\a/\\a/g ;
$s=~ s/\e/\\e/g ;
$s=~ s/\0(?!\d\d)/\\0/ ;
$s=~ s/([\ca-\cz])/ "\\c" . chr(ord($1)+64) /ge ;
$s=~ s/([^\x20-\x7e])/ "\\x" . sprintf("%02x",ord($1)) /ge ;
return $s ;
}
# Identical to &slashescape(), except doesn't escape \n
sub slashescapetextarea {
my($s)= @_ ;
$s=~ s/\\/\\\\/g ;
$s=~ s/\r/\\r/g ;
$s=~ s/\t/\\t/g ;
$s=~ s/\f/\\f/g ;
$s=~ s/\x08/\\b/g ;
$s=~ s/\a/\\a/g ;
$s=~ s/\e/\\e/g ;
$s=~ s/\0(?!\d\d)/\\0/ ;
$s=~ s/([\ca-\ci\ck-\cz])/ "\\c" . chr(ord($1)+64) /ge ;
$s=~ s/([^\x20-\x7e\n])/ "\\x" . sprintf("%02x",ord($1)) /ge ;
return $s ;
}
#----- routines to calculate various globals and arrays --------------
# Calculate all global scalars and arrays, as part of initialization.
# Order is sometimes important here.
# None of these should need recalculating; they should all be constant.
sub calcglobals {
@safein{keys %in}= map { &HTMLescape($_) } values %in ;
# Save database definition to send to script again
$dbdefnget= &urlencodelist(&subhash(*in, qw(file delim columns referer))) ;
$dbdefnpost= &hiddenvars(&subhash(*in, qw(file delim columns referer))) ;
$delim= ($in{'delim'}=~ /\d/)
? join("", map { chr } ($in{'delim'}=~ /(\d+)/g) )
: $DEFAULT_DELIM ;
$safedelim= &slashescape($delim) ;
# Columns are "title:flags"; store in @title and @flags[]{flags}.
# Flags are one char and may take numeric value, e.g. "title:a5b11cd".
# Default flag value is 1. Initial numeric value stored in 'preflag'. (?)
# could be cleaner here...
@column= (split(/\s*,\s*/, $in{'columns'})) ;
for (0..$#column) { ($title[$_],$flags[$_])= split(/:/, $column[$_], 2) }
@title= map { &HTMLescape($_) } @title ;
foreach (0..$#flags) {
($flags[$_])= { 'preflag', split(/([a-zA-Z])/, $flags[$_]) } ;
foreach $key (keys %{$flags[$_]}) {
$flags[$_]{$key}= '1' unless length($flags[$_]{$key}) ;
}
}
}
# Find current parameters of table
# columns start with column 0
sub calctablesize {
&findmaxwidths ;
$lastcol= ($#column>$#maxwidth) ? $#column : $#maxwidth ;
}
# Find maximum widths of data in columns
# jsm-- field array could be saved to use later, for speed?
sub findmaxwidths {
@maxwidth= () ;
@maxheight= () ;
foreach $key (keys %dbdata) {
my($i, $numlines) ;
foreach (split(/$delim/, $dbdata{$key})) {
# @maxheight() calc is only needed for textareas, but let's
# figure all of them, may come in handy.
$numlines= s/\n/\n/g + 1 ;
$maxheight[$i]= $numlines if $maxheight[$i]<$numlines ;
if ($flags[$i]{'t'}) {
foreach my $l (split(/\n/)) {
$l= &slashescape($l) ;
$maxwidth[$i]= length($l) if $maxwidth[$i]
$debug
Editing "$safein{'file'}" database
$topmsg
EOF
}
# Print common footer
sub printfooter {
print <
DBMEdit 1.0