#!/usr/bin/perl -w use strict; #use diagnostics; # Written by Frederick Dean # v1.1 June 17, 2002 ############################ my $addressFile = '/path/to/address/address.dat'; # or aba file #my $passwd = ""; # for no password operation my $passwd = undef; #my $passwd = "pick-a-better-password"; my $debug = ""; # parse,task, my $showPrivate = 1; ############################## # # Copyright (c) 2001 by Frederick Dean # # This program is free software; you can redistribute it and/or # modify it under the terms of the Artistic License, a copy of which # can be found with the Perl distribution. # # This code is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # Artistic License for more details. # =head1 webpda.cgi This script will let you see into a palmdesk's address book from a web page. You can enable Apache for just one CGI file with Options +ExecCGI AddHandler cgi-script .cgi The home page for this script is ... http://fdd.com/software/palm/ Special thanks to Scott Leighton . http:///www.geocities.com/Heartland/Acres/3216/address_dat.htm =cut print("content-type: text/plain\n\n") if $debug; # Decode the escaping of a URL sub urlUnquote { my ($tounquote) = (@_); $tounquote =~ tr/+/ /; # pluses become spaces $tounquote =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge; return $tounquote; } # Quotify a string, suitable for putting into a URL. sub urlQuote { my($toencode) = (@_); $toencode=~s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; return $toencode; } sub Fatal { my ($error) = (@_); print( "content-type: text/html\n\n". "
Fatal Error
\n". "" . htmlQuote($error) . "\n" . ""); exit(1); } my %form; # This is the has of form data submitted # The keys are the "name=" of the HTML form # useful for debugging. Prints %form sub PrintFormHash { print("
\n\n"); for my $key (keys(%form)) { print("$key => $form{$key}
\n"); } print("
\n"); } # Here we parse the data from apache into a hash sub MakeFormHash { my $queryString = ""; if(defined($ENV{"REQUEST_METHOD"}) and $ENV{"REQUEST_METHOD"} eq "POST") { read(STDIN,$queryString,$ENV{"CONTENT_LENGTH"}); } elsif(defined($ENV{"QUERY_STRING"})) { $queryString = $ENV{"QUERY_STRING"}; } # $queryString looks like pw=passwd&name=rick+dean my @pairs = split('&',$queryString); for my $pair (@pairs) { if($pair =~ /([^=]*)=(.*)/) { $form{$1} = urlUnquote($2); }; } } # useful for debugging sub PrintEnv { print("
\n"); for my $key (keys(%ENV)) { print("$key => $ENV{$key}
\n"); } } # This makes a new URL being smart about the state we want to keep. # The args are a list of "value=param" or "value1=param1&value2=param2" sub Link { my @params = @_; my %answer; my @keepers = qw{ pw sort }; for my $keeper (@keepers) { # for every keeper if(defined($form{$keeper})) { $answer{$keeper} = $form{$keeper}; }; }; for my $parampair (@params) { # for every new parameter for my $param (split("&",$parampair)) { $param =~ m/(.*?)=(.*)/; $answer{$1} = $2; }; }; my $answer = ""; for my $key (keys(%answer)) { # for every answer param $answer .= "$key=" . urlQuote($answer{$key}) . '&'; # append } chop($answer); # kill trailing '&' my $progname = $ENV{'REQUEST_URI'} || $0; $progname =~ s|\?.*||; # trim the question mark and beyond $progname =~ s|.*/||; # trim everything up to last slash return(""); } sub CheckForPasswd { if(!defined($passwd)) { Fatal("Someone needs to set up the password by editing the " . "top of the CGI script."); }; if($passwd eq "") { # no password (public database?) return; }; $passwd = crypt($passwd,"xA"); if(defined($form{'passwd'})) { $form{'pw'} = crypt($form{'passwd'},"xA"); }; if(!defined($form{'pw'}) or $form{'pw'} ne $passwd) { PrintHeader("WebPDA Login"); if(defined($form{'pw'})) { sleep(2); # make exhaustive searches more annoying print("

Password Incorrect

\n"); }; print("
\n". " Please enter your password:
\n". " \n". " \n". "
\n". "\n"); exit(); }; } # returns a string converted to html # double spaces and newlines are not processed sub htmlQuote { my ($html) = (@_); $html =~ s/\&/\&/g; $html =~ s//\>/g; $html =~ s/\n/\n
/g; return $html; } # Create a new has of everythin html quoted sub htmlQuoteHash { my ($orig) = (@_); my %quoted; for my $key (keys(%$orig)) { $quoted{$key} = htmlQuote($$orig{$key}); }; return \%quoted; } # Palm stores some stuff as CStrings # If the string is less than 255 chars long, it is one byte # of length then the string, otherwise it is a 0xff byte, # two bytes of length (LSB first) and then the data. # # Palm apparently first wrote the PalmDesk using MFC, # and stored the data as MFC packed streamed data, which puts # four bytes of zeros before the string. sub ReadCString { my ($fh,$comment,$noGarbageFlag) = (@_); if(!$noGarbageFlag) { read($fh,my $garbage,4) or return undef; }; my $tell = tell($fh); read($fh,my $lengthByte,1) or return undef; my $length = unpack("C",$lengthByte); if($length == 0xff) { # we read the integer as two byte explicitly to avoid endian problems read($fh,$lengthByte,1) or return undef; $length = unpack("C",$lengthByte); read($fh,$lengthByte,1) or return undef; $length = ($length << 8) + unpack("C",$lengthByte); }; my $string; if($length == 0) { $string = ""; } else { read($fh,$string,$length) or return undef; }; printf("tell=$tell %25s length=$length string=$string\n",$comment) if $debug =~ /parse/; return $string; } # Read a 32-bit integer. # We read the integer as explicit bytes to avoid endian problems. sub ReadLongInt { my ($fh,$comment) = (@_); my $tell = tell($fh); read($fh,my $byte1,1) or return undef; read($fh,my $byte2,1) or return undef; read($fh,my $byte3,1) or return undef; read($fh,my $byte4,1) or return undef; my $int = (unpack("C",$byte4) << 24) + (unpack("C",$byte3) << 16) + (unpack("C",$byte2) << 8) + unpack("C",$byte1); printf("tell=$tell %25s int=$int (0x%08x)\n",$comment,$int) if $debug =~ /parse/; return $int; } sub ReadShortInt { my ($fh,$comment) = (@_); my $tell = tell($fh); read($fh,my $byte,1) or return undef; my $int = unpack("C",$byte); read($fh,$byte,1) or return undef; $int = $int + (unpack("C",$byte) << 8); printf("tell=$tell %25s int=$int (0x%04x) \n",$comment,$int) if $debug =~ /parse/; return $int; } # We store the addresses as a list of hash references # Each record (person) has his own hash. my @records; # We are inefficient that we load all of this into RAM every time no matter # if we are only using one record. my(%categories,%revCategories); # The key is categoryNum and the value is the number of records found my %numRecordsInCategory; my @phoneLabels = qw{work home fax other email main pager mobile}; sub LabelRangeFix { my($id) = (@_); #$id--; if($id < 0 or $id > $#phoneLabels) { $id = 0; } return $id; } sub LoadDatFile { my $fh = $_[0]; ReadLongInt($fh,"version tag"); # 0x00014241 my $filename = ReadCString($fh,"true filename",1); # filename my $customLabels = ReadCString($fh,"custom labels",1); my $nextId = ReadLongInt($fh,"next id"); # should be 0x00000083 my $categoryCount = ReadLongInt($fh,"category count"); while($categoryCount-- > 0) { my $index = ReadLongInt($fh,"category index"); ReadLongInt($fh,"category id"); ReadLongInt($fh,"category data flag"); my $name = ReadCString($fh,"long category name",1); ReadCString($fh,"short category name",1); $categories{$index} = $name; $revCategories{$name} = $index; } $categories{0} = "Unfiled"; # schema ReadLongInt($fh,"resource id"); my $fieldsPerRow = ReadLongInt($fh,"fields per row (30)"); ReadLongInt($fh,"record id position"); ReadLongInt($fh,"record status position"); ReadLongInt($fh,"placement position"); my $numFields = ReadShortInt($fh,"field count"); my @fields = (); while($numFields-- > 0) { push(@fields,ReadShortInt($fh,"field entry")); }; print($#fields+1," fields are ",join(",",@fields),"\n") if $debug =~ /parse/; my $numEntries = ReadLongInt($fh,"num entries"); print($numEntries/$fieldsPerRow," records in file\n") if $debug =~ /parse/; while($numEntries-- > 0) { my %record; # type: none=0 long=1 float=2 date=3 alpha=4 cstr=5 bool=6 bitflag=7 repeatevent=8 ReadLongInt($fh,"field type"); last if eof($fh); $record{'id'} = ReadLongInt($fh,"record id"); ReadLongInt($fh,"field type"); # status: 0x08=pending 0x01=add 0x02=update 0x04=del 0x80=archive $record{'status'} = ReadLongInt($fh,"status"); ReadLongInt($fh,"field type"); $record{'position'} = ReadLongInt($fh,"position"); ReadLongInt($fh,"field type"); $record{'last'} = my $name = ReadCString($fh,"last (name)"); ReadLongInt($fh,"field type"); $record{'first'} = ReadCString($fh,"first"); $record{'name'} = "$record{'last'}, $record{'first'}"; $record{'id'} = "0-$record{'last'}-$record{'first'}" if $record{'id'} == 0; ReadLongInt($fh,"field type"); $record{'title'} = ReadCString($fh,"title"); ReadLongInt($fh,"field type"); $record{'company'} = ReadCString($fh,"company"); ReadLongInt($fh,"field type"); $record{'phone1label'} = LabelRangeFix(ReadLongInt($fh,"phone1 label id")); ReadLongInt($fh,"field type"); $record{'phone1'} = ReadCString($fh,"phone1"); ReadLongInt($fh,"field type"); $record{'phone2label'} = LabelRangeFix(ReadLongInt($fh,"phone2 label id")); ReadLongInt($fh,"field type"); $record{'phone2'} = ReadCString($fh,"phone2"); ReadLongInt($fh,"field type"); $record{'phone3label'} = LabelRangeFix(ReadLongInt($fh,"phone2 label id")); ReadLongInt($fh,"field type"); $record{'phone3'} = ReadCString($fh,"phone3"); ReadLongInt($fh,"field type"); $record{'phone4label'} = LabelRangeFix(ReadLongInt($fh,"phone4 label id")); ReadLongInt($fh,"field type"); $record{'phone4'} = ReadCString($fh,"phone4"); ReadLongInt($fh,"field type"); $record{'phone5label'} = LabelRangeFix(ReadLongInt($fh,"phone5 label id")); ReadLongInt($fh,"field type"); $record{'phone5'} = ReadCString($fh,"phone5"); ReadLongInt($fh,"field type"); $record{'street'} = ReadCString($fh,"street"); ReadLongInt($fh,"field type"); $record{'city'} = ReadCString($fh,"city"); ReadLongInt($fh,"field type"); $record{'state'} = ReadCString($fh,"state"); ReadLongInt($fh,"field type"); $record{'zip'} = ReadCString($fh,"zip"); ReadLongInt($fh,"field type"); $record{'country'} = ReadCString($fh,"country"); ReadLongInt($fh,"field type"); $record{'note'} = ReadCString($fh,"note"); ReadLongInt($fh,"field type"); $record{'private'} = ReadLongInt($fh,"private"); ReadLongInt($fh,"field type"); $record{'categoryNum'} = ReadLongInt($fh,"categoryNum"); $record{'category'} = $categories{$record{'categoryNum'}}; ReadLongInt($fh,"field type"); $record{'custom1'} = ReadCString($fh,"custom 1"); ReadLongInt($fh,"field type"); $record{'custom2'} = ReadCString($fh,"custom 2"); ReadLongInt($fh,"field type"); $record{'custom3'} = ReadCString($fh,"custom 3"); ReadLongInt($fh,"field type"); $record{'custom4'} = ReadCString($fh,"custom 4"); ReadLongInt($fh,"field type"); my $primary = ReadLongInt($fh,"primary display index"); if(!defined($primary) or $primary !~ /^\d+$/ or $primary < 0 or $primary > 6) { $primary = 1; } $record{'primaryNum'} = $primary; $record{'primary'} = $record{"phone$primary"}; next if $record{'private'} && !$showPrivate; $numRecordsInCategory{$record{'categoryNum'}}++; push(@records,\%record); # save hash reference in list last if eof($fh); # if incomplete record }; } sub LoadAddresses { -e $addressFile or Fatal("file $addressFile doesn't exist"); -r $addressFile or Fatal("I don't have read permission for file $addressFile"); print("opening $addressFile\n") if $debug =~ /task/; if(!open(DAT,"<$addressFile")) { PrintHeader(); print("Could not open $addressFile\n"); exit(); } LoadDatFile(\*DAT); close(DAT); } sub PrintSummaryAddr { my ($fieldsRef) = (@_); print("PrintSummaryAddr()\n") if $debug =~ /task/; my $categoryNum; if(defined($form{'category'})) { $categoryNum = $revCategories{$form{'category'}}; }; print("\n"); print(" \n"); my @fields = @$fieldsRef; my $firstField = shift(@fields); my $secondField = $fields[0]; my $sort1 = $form{'sort'} || $firstField; # default if not defined my $firstref = $records[0]; if(!defined($$firstref{$sort1})) { $sort1 = $firstField; # sort must be legal field }; my $sort2; if($sort1 eq $firstField) { $sort2 = $secondField; } else { $sort2 = $firstField; }; my %hasField; for my $field ($firstField,@fields) { $hasField{$field} = 1; }; push(@fields,$sort1) if !$hasField{$sort1}; for my $field ($firstField,@fields) { print(" \n"); my $rowNum = 0; @records = sort { (my $aa = $$a{$sort1}) =~ tr/a-z/A-Z/; # case insensitive (my $bb = $$b{$sort1}) =~ tr/a-z/A-Z/; (my $aaa = $$a{$sort2}) =~ tr/a-z/A-Z/; (my $bbb = $$b{$sort2}) =~ tr/a-z/A-Z/; $aa cmp $bb or $aaa cmp $bbb; } @records; my $searchRE; # search regular expression if(defined($form{'search'}) and $form{'search'} ne "") { ($searchRE = $form{'search'}) =~ s/(\W)/\\$1/g; # escape if($searchRE =~ /^\d+$/) { # if search is all numeric ($searchRE = $form{'search'}) =~ s/(\d)/${1}\\W*/g; # ignore punctuation }; }; for my $recref (@records) { # for every record if(defined($categoryNum) and $categoryNum ne $$recref{'categoryNum'}) { next; # skip records not in our category (family, personal, etc) } if(defined($searchRE)) { # if we have search pattern my $found; for my $value (values(%$recref)) { # for every field of this record if($value =~ /$searchRE/i) { $found = 1; last; }; }; next if(!$found); # didn't match search }; if($rowNum & 1) { # is odd row (for colors) print(" \n"); # default white } else { print(" \n"); # light gray }; print(" \n"); for my $field (@fields) { if($$recref{$field} eq "") { print(" \n"); # netscape bug } else { print(" \n"); }; }; print(" \n"); $rowNum++; }; print("
".Link("sort=$field")."
". "$field
\n"); }; print("
".Link("id=$$recref{'id'}").htmlQuote($$recref{$firstField})." ".htmlQuote($$recref{$field})."
\n"); print("$rowNum record" . ($rowNum == 1 ? "" : "s") . " found
\n"); } sub PrintDetailedAddr { my ($id) = (@_); print("PrintDetailedAddr(id=$id)\n") if $debug =~ /task/; my $recref; for my $maybeRecref (@records) { if($$maybeRecref{'id'} eq $id) { $recref = $maybeRecref; last; } }; if(!defined($recref)) { print("id #$id not found\n"); return; }; $recref = htmlQuoteHash($recref); print("\n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); my $star = $$recref{'display'} == 1 ? "*" : ""; print(" ". "\n"); $star = $$recref{'display'} == 2 ? "*" : ""; print(" ". "\n"); $star = $$recref{'display'} == 3 ? "*" : ""; print(" ". "\n"); $star = $$recref{'display'} == 4 ? "*" : ""; print(" ". "\n"); $star = $$recref{'display'} == 5 ? "*" : ""; print(" ". "\n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); print(" \n"); my $categoryNum = $$recref{'categoryNum'}; print(" \n"); my $private = $$recref{'private'} ? "yes" : "no"; print(" \n"); print(" \n"); print("
Name
last name:$$recref{'last'}
first name:$$recref{'first'}
title:$$recref{'title'}
company:$$recref{'company'}
Address
street:$$recref{'street'}
city:$$recref{'city'}
state:$$recref{'state'}
zip:$$recref{'zip'}
country:$$recref{'country'}
Contact Info
$star $phoneLabels[$$recref{'phone1label'}]:$$recref{'phone1'}
$star $phoneLabels[$$recref{'phone2label'}]:$$recref{'phone2'}
$star $phoneLabels[$$recref{'phone3label'}]:$$recref{'phone3'}
$star $phoneLabels[$$recref{'phone4label'}]:$$recref{'phone4'}
$star $phoneLabels[$$recref{'phone5label'}]:$$recref{'phone5'}
Other
custom1:$$recref{'custom1'}
custom2:$$recref{'custom2'}
custom3:$$recref{'custom3'}
custom4:$$recref{'custom4'}
category:$categories{$categoryNum}
private:$private
note:$$recref{'note'}
\n"); } sub PrintCategoryLinks { print(Link() . "All
(". ($#records +1) .")
"); for my $id (sort(keys(%categories))) { next unless $numRecordsInCategory{$id}; my $name = $categories{$id}; print(Link("category=$name") . htmlQuote($name)." ($numRecordsInCategory{$id})
"); }; } sub PrintHeader { my ($title) = (@_); if(!defined($title)) { $title = "WedPDA"; }; print("content-type: text/html\n\n". "\n". "$title\n". "\n". " $title
\n"); } sub PrintMainTable { print("
\n"); } sub PrintFooter { print("\n"); print("
Addresses
\n"); PrintCategoryLinks(); print("
Table sorted by
\n"); print(Link("sort=name","id=")."last name
\n"); print(Link("sort=first","id=")."first name
\n"); print(Link("sort=city","id=")."city
\n"); print(Link("sort=state","id=")."state
\n"); print(Link("sort=zip","id=")."zip
\n"); print(Link("sort=country","id=")."country
\n"); print(Link("sort=category","id=")."category
\n"); print("
Search
\n"); print("
\n"); print("
\n"); print("
\n"); print("\n"); print("
\n"); print("
\n"); } MakeFormHash(); CheckForPasswd(); # may not return print("password okay\n") if $debug =~ /task/; PrintHeader(); PrintMainTable(); LoadAddresses(); print("loaded okay\n") if $debug =~ /task/; if($form{'id'}) { PrintDetailedAddr($form{'id'}); } else { PrintSummaryAddr(["name","company","primary"]); }; PrintFooter(); #PrintFormHash(); print("done\n") if $debug =~ /task/;