specified in any form that invokes a standard script. If debugging mode is turned
on, a form field will be put in that allows the user to enter tracing data.
Trace messages will be placed immediately before the terminal C tag in
the output, formatted as a list.
A typical standard script would loook like the following.
BEGIN {
# Print the HTML header.
print "CONTENT-TYPE: text/html\n\n";
}
use Tracer;
use CGI;
use FIG;
# ... more uses ...
my ($cgi, $varHash) = ScriptSetup();
eval {
# ... get data from $cgi, put it in $varHash ...
};
if ($@) {
Trace("Script Error: $@") if T(0);
}
ScriptFinish("Html/MyTemplate.html", $varHash);
The idea here is that even if the script fails, you'll see trace messages and
useful output.
=over 4
=item webData
A string containing either the full web page to be written to the output or the
name of a template file from which the page is to be constructed. If the name
of a template file is specified, then the second parameter must be present;
otherwise, it must be absent.
=item varHash (optional)
If specified, then a reference to a hash mapping variable names for a template
to their values. The template file will be read into memory, and variable markers
will be replaced by data in this hash reference.
=back
=cut
sub ScriptFinish {
# Get the parameters.
my ($webData, $varHash) = @_;
# Check for a template file situation.
my $outputString;
if (defined $varHash) {
# Here we have a template file. We need to determine the template type.
my $template;
if ($FIG_Config::template_url && $webData =~ /\.php$/) {
$template = "$FIG_Config::template_url/$webData";
} else {
$template = "<<$webData";
}
$outputString = PageBuilder::Build($template, $varHash, "Html");
} else {
# Here the user gave us a raw string.
$outputString = $webData;
}
# Check for trace messages.
if ($Destination ne "NONE" && $TraceLevel > 0) {
# We have trace messages, so we want to put them at the end of the body. This
# is either at the end of the whole string or at the beginning of the BODY
# end-tag.
my $pos = length $outputString;
if ($outputString =~ m##gi) {
$pos = (pos $outputString) - 7;
}
# If the trace messages were queued, we unroll them. Otherwise, we display the
# destination.
my $traceHtml;
if ($Destination eq "QUEUE") {
$traceHtml = QTrace('Html');
} elsif ($Destination =~ /^>>(.+)$/) {
# Here the tracing output it to a file. We code it as a hyperlink so the user
# can copy the file name into the clipboard easily.
my $actualDest = $1;
$traceHtml = "Tracing output to $actualDest.
\n";
} else {
# Here we have one of the special destinations.
$traceHtml = "Tracing output type is $Destination.
\n";
}
substr $outputString, $pos, 0, $traceHtml;
}
# Write the output string.
print $outputString;
}
=head3 Insure
C<< Insure($dirName); >>
Insure a directory is present.
=over 4
=item dirName
Name of the directory to check. If it does not exist, it will be created.
=back
=cut
sub Insure {
my ($dirName) = @_;
if (! -d $dirName) {
Trace("Creating $dirName directory.") if T(2);
eval { mkpath $dirName; };
if ($@) {
Confess("Error creating $dirName: $@");
}
}
}
=head3 ChDir
C<< ChDir($dirName); >>
Change to the specified directory.
=over 4
=item dirName
Name of the directory to which we want to change.
=back
=cut
sub ChDir {
my ($dirName) = @_;
if (! -d $dirName) {
Confess("Cannot change to directory $dirName: no such directory.");
} else {
Trace("Changing to directory $dirName.") if T(4);
my $okFlag = chdir $dirName;
if (! $okFlag) {
Confess("Error switching to directory $dirName.");
}
}
}
=head3 SendSMS
C<< my $msgID = Tracer::SendSMS($phoneNumber, $msg); >>
Send a text message to a phone number using Clickatell. The FIG_Config file must contain the
user name, password, and API ID for the relevant account in the hash reference variable
I<$FIG_Config::phone>, using the keys C, C, and C. For
example, if the user name is C, the password is C, and the API ID
is C<2561022>, then the FIG_Config file must contain
$phone = { user => 'BruceTheHumanPet',
password => 'silly',
api_id => '2561022' };
The original purpose of this method was to insure Bruce would be notified immediately when the
Sprout Load terminates. Care should be taken if you do not wish Bruce to be notified immediately
when you call this method.
The message ID will be returned if successful, and C if an error occurs.
=over 4
=item phoneNumber
Phone number to receive the message, in international format. A United States phone number
would be prefixed by "1". A British phone number would be prefixed by "44".
=item msg
Message to send to the specified phone.
=item RETURN
Returns the message ID if successful, and C if the message could not be sent.
=back
=cut
sub SendSMS {
# Get the parameters.
my ($phoneNumber, $msg) = @_;
# Declare the return variable. If we do not change it, C will be returned.
my $retVal;
# Only proceed if we have phone support.
if (! defined $FIG_Config::phone) {
Trace("Phone support not present in FIG_Config.") if T(1);
} else {
# Get the phone data.
my $parms = $FIG_Config::phone;
# Get the Clickatell URL.
my $url = "http://api.clickatell.com/http/";
# Create the user agent.
my $ua = LWP::UserAgent->new;
# Request a Clickatell session.
my $resp = $ua->post("$url/sendmsg", { user => $parms->{user},
password => $parms->{password},
api_id => $parms->{api_id},
to => $phoneNumber,
text => $msg});
# Check for an error.
if (! $resp->is_success) {
Trace("Alert failed.") if T(1);
} else {
# Get the message ID.
my $rstring = $resp->content;
if ($rstring =~ /^ID:\s+(.*)$/) {
$retVal = $1;
} else {
Trace("Phone attempt failed with $rstring") if T(1);
}
}
}
# Return the result.
return $retVal;
}
=head3 CommaFormat
C<< my $formatted = Tracer::CommaFormat($number); >>
Insert commas into a number.
=over 4
=item number
A sequence of digits.
=item RETURN
Returns the same digits with commas strategically inserted.
=back
=cut
sub CommaFormat {
# Get the parameters.
my ($number) = @_;
# Pad the length up to a multiple of three.
my $padded = "$number";
$padded = " " . $padded while length($padded) % 3 != 0;
# This is a fancy PERL trick. The parentheses in the SPLIT pattern
# cause the delimiters to be included in the output stream. The
# GREP removes the empty strings in between the delimiters.
my $retVal = join(",", grep { $_ ne '' } split(/(...)/, $padded));
# Clean out the spaces.
$retVal =~ s/ //g;
# Return the result.
return $retVal;
}
=head3 SetPermissions
C<< Tracer::SetPermissions($dirName, $group, $mask, %otherMasks); >>
Set the permissions for a directory and all the files and folders inside it.
In addition, the group ownership will be changed to the specified value.
This method is more vulnerable than most to permission and compatability
problems, so it does internal error recovery.
=over 4
=item dirName
Name of the directory to process.
=item group
Name of the group to be assigned.
=item mask
Permission mask. Bits that are C<1> in this mask will be ORed into the
permission bits of any file or directory that does not already have them
set to 1.
=item otherMasks
Map of search patterns to permission masks. If a directory name matches
one of the patterns, that directory and all its members and subdirectories
will be assigned the new pattern. For example, the following would
assign 01664 to most files, but would use 01777 for directories named C.
Tracer::SetPermissions($dirName, 'fig', 01664, '^tmp$' => 01777);
The list is ordered, so the following would use 0777 for C and
0666 for C, C, or C.
Tracer::SetPermissions($dirName, 'fig', 01664, '^tmp1' => 0777,
'^tmp' => 0666);
Note that the pattern matches are all case-insensitive, and only directory
names are matched, not file names.
=back
=cut
sub SetPermissions {
# Get the parameters.
my ($dirName, $group, $mask, @otherMasks) = @_;
# Set up for error recovery.
eval {
# Switch to the specified directory.
ChDir($dirName);
# Get the group ID.
my $gid = getgrnam($group);
# Get the mask for tracing.
my $traceMask = sprintf("%04o", $mask) . "($mask)";
Trace("Fixing permissions for directory $dirName using group $group($gid) and mask $traceMask.") if T(2);
my $fixCount = 0;
my $lookCount = 0;
# @dirs will be a stack of directories to be processed.
my @dirs = (getcwd());
while (scalar(@dirs) > 0) {
# Get the current directory.
my $dir = pop @dirs;
# Check for a match to one of the specified directory names. To do
# that, we need to pull the individual part of the name off of the
# whole path.
my $simpleName = $dir;
if ($dir =~ m!/([^/]+)$!) {
$simpleName = $1;
}
Trace("Simple directory name for $dir is $simpleName.") if T(4);
# Search for a match.
my $match = 0;
my $i;
for ($i = 0; $i < $#otherMasks && ! $match; $i += 2) {
my $pattern = $otherMasks[$i];
if ($simpleName =~ /$pattern/i) {
$match = 1;
}
}
# Check for a match. Note we use $i-1 because the loop added 2
# before terminating due to the match.
if ($match && $otherMasks[$i-1] != $mask) {
# This directory matches one of the incoming patterns, and it's
# a different mask, so we process it recursively with that mask.
SetPermissions($dir, $group, $otherMasks[$i-1], @otherMasks);
} else {
# Here we can process normally. Get all of the non-hidden members.
my @submems = OpenDir($dir, 1);
for my $submem (@submems) {
# Get the full name.
my $thisMem = "$dir/$submem";
Trace("Checking member $thisMem.") if T(4);
$lookCount++;
if ($lookCount % 1000 == 0) {
Trace("$lookCount members examined. Current is $thisMem. Mask is $traceMask") if T(3);
}
# Fix the group.
chown -1, $gid, $thisMem;
# Insure this member is not a symlink.
if (! -l $thisMem) {
# Get its info.
my $fileInfo = stat $thisMem;
# Only proceed if we got the info. Otherwise, it's a hard link
# and we want to skip it anyway.
if ($fileInfo) {
my $fileMode = $fileInfo->mode;
if (($fileMode & $mask) != $mask) {
# Fix this member.
$fileMode |= $mask;
chmod $fileMode, $thisMem;
$fixCount++;
}
# If it's a subdirectory, stack it.
if (-d $thisMem) {
push @dirs, $thisMem;
}
}
}
}
}
}
Trace("$lookCount files and directories processed, $fixCount fixed.") if T(2);
};
# Check for an error.
if ($@) {
Confess("SetPermissions error: $@");
}
}
=head3 CompareLists
C<< my ($inserted, $deleted) = Tracer::CompareLists(\@newList, \@oldList, $keyIndex); >>
Compare two lists of tuples, and return a hash analyzing the differences. The lists
are presumed to be sorted alphabetically by the value in the $keyIndex column.
The return value contains a list of items that are only in the new list
(inserted) and only in the old list (deleted).
=over 4
=item newList
Reference to a list of new tuples.
=item oldList
Reference to a list of old tuples.
=item keyIndex (optional)
Index into each tuple of its key field. The default is 0.
=item RETURN
Returns a 2-tuple consisting of a reference to the list of items that are only in the new
list (inserted) followed by a reference to the list of items that are only in the old
list (deleted).
=back
=cut
sub CompareLists {
# Get the parameters.
my ($newList, $oldList, $keyIndex) = @_;
if (! defined $keyIndex) {
$keyIndex = 0;
}
# Declare the return variables.
my ($inserted, $deleted) = ([], []);
# Loop through the two lists simultaneously.
my ($newI, $oldI) = (0, 0);
my ($newN, $oldN) = (scalar @{$newList}, scalar @{$oldList});
while ($newI < $newN || $oldI < $oldN) {
# Get the current object in each list. Note that if one
# of the lists is past the end, we'll get undef.
my $newItem = $newList->[$newI];
my $oldItem = $oldList->[$oldI];
if (! defined($newItem) || defined($oldItem) && $newItem->[$keyIndex] gt $oldItem->[$keyIndex]) {
# The old item is not in the new list, so mark it deleted.
push @{$deleted}, $oldItem;
$oldI++;
} elsif (! defined($oldItem) || $oldItem->[$keyIndex] gt $newItem->[$keyIndex]) {
# The new item is not in the old list, so mark it inserted.
push @{$inserted}, $newItem;
$newI++;
} else {
# The item is in both lists, so push forward.
$oldI++;
$newI++;
}
}
# Return the result.
return ($inserted, $deleted);
}
=head3 GetLine
C<< my @data = Tracer::GetLine($handle); >>
Read a line of data from a tab-delimited file.
=over 4
=item handle
Open file handle from which to read.
=item RETURN
Returns a list of the fields in the record read. The fields are presumed to be
tab-delimited. If we are at the end of the file, then an empty list will be
returned. If an empty line is read, a single list item consisting of a null
string will be returned.
=back
=cut
sub GetLine {
# Get the parameters.
my ($handle) = @_;
# Declare the return variable.
my @retVal = ();
# Read from the file.
my $line = <$handle>;
# Only proceed if we found something.
if (defined $line) {
# Remove the new-line.
chomp $line;
# If the line is empty, return a single empty string; otherwise, parse
# it into fields.
if ($line eq "") {
push @retVal, "";
} else {
push @retVal, split /\t/,$line;
}
}
# Return the result.
return @retVal;
}
=head3 PutLine
C<< Tracer::PutLine($handle, \@fields); >>
Write a line of data to a tab-delimited file. The specified field values will be
output in tab-separated form, with a trailing new-line.
=over 4
=item handle
Output file handle.
=item fields
List of field values.
=back
=cut
sub PutLine {
# Get the parameters.
my ($handle, $fields) = @_;
# Write the data.
print $handle join("\t", @{$fields}) . "\n";
}
=head3 GenerateURL
C<< my $queryUrl = Tracer::GenerateURL($page, %parameters); >>
Generate a GET-style URL for the specified page with the specified parameter
names and values. The values will be URL-escaped automatically. So, for
example
Tracer::GenerateURL("form.cgi", type => 1, string => "\"high pass\" or highway")
would return
form.cgi?type=1&string=%22high%20pass%22%20or%20highway
=over 4
=item page
Page URL.
=item parameters
Hash mapping parameter names to parameter values.
=item RETURN
Returns a GET-style URL that goes to the specified page and passes in the
specified parameters and values.
=back
=cut
sub GenerateURL {
# Get the parameters.
my ($page, %parameters) = @_;
# Prime the return variable with the page URL.
my $retVal = $page;
# Loop through the parameters, creating parameter elements in a list.
my @parmList = map { "$_=" . uri_escape($parameters{$_}) } keys %parameters;
# If the list is nonempty, tack it on.
if (@parmList) {
$retVal .= "?" . join("&", @parmList);
}
# Return the result.
return $retVal;
}
1;