#
# Ph.pm - a module for talking to CCSO Ph servers
#
# Copyright 1995-1998, Garrett D'Amore.
# See the Artistic License for licensing details and use
# agreements.
#
# Important: READ THE Artistic FILE -- it limits my liability
# and provids the specific agreements to which you must agree
# if you use this package.
#
package Ph;
use strict;
use vars qw($VERSION $DefaultPhPort $DefaultPhServer @ISA);
$VERSION = '2.01';
@ISA = qw();
require 5.003;
use IO::Socket;
use Carp;
$DefaultPhPort = "ns(105)";
$DefaultPhServer = "ns";
sub new
{
my ($class, %arg) = @_;
return bless \%arg, $class;
}
sub Add
{
my $self = shift;
my $entry = shift;
my $request = "add";
$request .= $self->_MakeFieldsLine($entry);
$self->_SendRequest($request);
return $self->_IsSuccessful($self->_GetCode());
}
sub Change
{
my $self = shift;
my ($query,$changes) = @_;
my $request = "change ";
$request .= $self->_MakeFieldsLine($query);
$request .= " make";
$request .= $self->_MakeFieldsLine($changes);
$self->_SendRequest($request);
return $self->_IsSuccessful($self->_GetCode());
}
sub Connect
{
my $self = shift;
my $code;
my ($PhServer, $PhPort) = @_;
if (!defined $PhServer)
{
$PhServer = $DefaultPhServer;
}
if (!defined $PhPort)
{
$PhPort = $DefaultPhPort;
}
my $sock = IO::Socket::INET->new(PeerAddr => $PhServer,
PeerPort => $PhPort,
Proto => 'tcp');
if (!defined $sock)
{
$self->{last_message} = $@;
$self->{last_message} =~ s/^IO::Socket::INET/connect/;
$self->{last_code} = 998;
return 0;
}
# make socket unbuffered, this is the default under 5.004_04
$sock->autoflush;
$self->{sock} = $sock;
# send id -- we send our UNIX account name -- this is not
# strictly required but it is polite
$self->_SendRequest("id $_GetCode();
if (!$self->_IsSuccessful($code))
{
return 0;
}
# get the database status
$self->_SendRequest("status");
return $self->_IsSuccessful($self->_GetCode());
}
sub Delete
{
my $self = shift;
my $entry = shift;
my $request = "delete";
$request .= $self->_MakeFieldsLine($entry);
$self->_SendRequest($request);
return $self->_IsSuccessful($self->_GetCode());
}
sub Disconnect
{
my $self = shift;
my $code;
if (defined $self->{sock})
{
$self->_SendRequest("quit");
# we don't care about the value, we just throw it away
$self->_GetCode();
close($self->{sock});
delete $self->{sock};
}
}
sub ErrorMessage
{
my $self = shift;
my $code = shift;
my %messages = (
100 => 'In progress (general).',
101 => 'Echo of current command.',
102 => 'Count of number of matches to query.',
200 => 'Success (general).',
201 => 'Database ready, but read only.',
300 => 'More information (general).',
301 => 'Encrypt this string.',
400 => 'Temporary error (general).',
401 => 'Internal database error.',
402 => 'Lock not obtained within timeout period.',
475 => 'Database unavailable; try later.',
500 => 'Permanent error (general).',
501 => 'No matches to query.',
502 => 'Too many matches to query.',
503 => 'Not authorized for requested information.',
504 => 'Not authorized for requested search criteria.',
505 => 'Not authorized to change requested field.',
506 => 'Request refused; must be logged in to execute.',
507 => 'Field does not exist.',
508 => 'Field is not present in requested entry.',
509 => 'Alias already in use.',
510 => 'Not authorized to change this entry.',
511 => 'Not authorized to add entries.',
512 => 'Illegal value.',
513 => 'Unknown option.',
514 => 'Unknown command.',
515 => 'No indexed field in query.',
516 => 'No authorization for request.',
517 => 'Operation failed because database is read only.',
518 => 'Too many entries selected by change command.',
520 => 'CPU usage limit exceeded.',
521 => 'Change command would have overridden existing field, and the "addonly" option is on.',
522 => 'Attempt to view "Encrypted" field.',
523 => 'Expecting "answer" or "clear"',
524 => 'Names of help topics may not contain "/".',
598 => 'Command unknown.',
599 => 'Syntax error.',
995 => '[socket failure]',
996 => '[bind failure]',
997 => '[unknown host]',
998 => '[connection failure]',
999 => '[connection closed]'
);
return $messages{$code};
}
sub Fields
{
my $self = shift;
my %result = ();
my $name;
my $code;
# get the field descriptions
$self->_SendRequest("fields");
do
{
my @response = $self->_ParseResponse($self->_GetResponse());
$code = $response[0];
$name = $response[2];
# second line is just the description
if (defined $result{$name})
{
$result{$name}->{desc} = $response[3];
}
# first line contains the good stuff
else
{
my $flag;
my @flags = split(/\s+/, $response[3]);
$result{$name} = {};
$result{$name}->{number} = $response[1];
$result{$name}->{name} = $name;
foreach $flag (@flags)
{
if ($flag =~ /^\d+$/)
{
$result{$name}->{max} = $flag;
}
elsif ($flag eq 'max')
{
# nop
}
else
{
$result{$name}->{$flag} = $flag;
}
} # foreach $flag
} # defined $result{$name}, i.e. end of field description
} while ($code < 200);
return ($self->_IsSuccessful($code)) ? %result : undef;
}
sub _IsSuccessful
{
my $self = shift;
my $code = shift;
return (int($code / 100) == 2);
}
sub _GetCode
{
my $self = shift;
my $code;
do
{
my @result = $self->_ParseResponse($self->_GetResponse());
$code = $result[0];
} while ($code < 200);
return $code;
}
sub _GetResponse
{
my $self = shift;
my $sock = $self->{sock};
my $response;
if (!defined $sock)
{
$response="999: [no active connection]\n";
}
else
{
$response = $sock->getline();
}
if (!defined $response)
{
$response="999: [connection closed]\n";
delete $self->{sock};
}
chomp $response;
print STDERR "server> $response\n" if ($self->{Debug});
return $response;
}
sub GetLastCode
{
my $self = shift;
return $self->{last_code};
}
sub GetLastMessage
{
my $self = shift;
return $self->{last_message};
}
sub IsConnected
{
my $self = shift;
return defined($self->{sock});
}
sub Login
{
my $self = shift;
my ($alias, $password) = @_;
my ($code, $challenge, $answer);
$self->_SendRequest("login $alias");
$code = $self->_GetCode();
# abort if we were not challenged!
return 0 if $code != 301;
#
# unfortunately, I don't know how to answer the challenge
# Dorner's code is *very* hard to understand! But if we
# ever figure it out, here's where we'll put the challenge
# response.
#
# so for now we send the password in the clear (blech!)
$self->_SendRequest("clear $password");
# overwrite password to minimize exposure; not sure if
# this really helps with perl
$password = "xxxxxxxxxxxxxx";
return $self->_IsSuccessful($self->_GetCode());
}
sub Logout
{
my $self = shift;
$self->_SendRequest("logout");
return $self->_IsSuccessful($self->_GetCode());
}
sub _MakeFieldsLine
{
my $self = shift;
my $fields = shift;
my ($output, $field);
$output = "";
if (ref($fields) eq 'HASH')
{
foreach $field (keys %$fields)
{
$fields->{$field} =~ s/"/\\"/;
$output .= " $field=\"$fields->{$field}\"";
}
}
elsif (ref($fields) eq 'ARRAY')
{
foreach $field (@$fields)
{
$output .= " $field";
}
}
else
{
$output = " $fields";
}
return $output;
}
sub _ParseResponse
{
my $self = shift;
my $unparsed = shift;
my @parsed = split(/: */, $unparsed, 4);
$self->{last_code} = $parsed[0];
$self->{last_message} = $parsed[1];
return @parsed;
}
sub Query
{
my $self = shift;
my ($query, $fieldlist) = @_;
my ($key, $request, $index, @response, $lastfield, $code, $field, $value);
my @matches = ();
$index = 0;
$request = "query";
$request .= $self->_MakeFieldsLine($query);
if (defined ($fieldlist))
{
$request .= " return";
$request .= $self->_MakeFieldsLine($fieldlist);
}
$self->_SendRequest($request);
do
{
@response = ();
@response = $self->_ParseResponse($self->_GetResponse());
$code = $response[0];
if (defined $response[3])
{
$index = $response[1] - 1;
$field = length($response[2]) ? $response[2] : $lastfield;
$lastfield = $field;
$value = $response[3];
if (!defined $matches[$index])
{
$matches[$index] = { };
}
if (!defined $matches[$index]->{$field} )
{
$matches[$index]->{$field} = '';
}
else
{
$matches[$index]->{$field} .= "\n";
}
$matches[$index]->{$field} .= $value;
}
} while ($code < 200);
return $self->_IsSuccessful($code) ? @matches : undef;
}
sub _SendRequest
{
my $self = shift;
my $request = shift;
my $sock = $self->{sock};
if (!defined $sock)
{
print STDERR "client> [connection not open]\n" if $self->{Debug};
}
elsif (print $sock "$request\r\n")
{
print STDERR "client> $request\n" if $self->{Debug};
}
else
{
print STDERR "client> [connection closed]\n" if $self->{Debug};
delete $self->{sock};
}
}
sub SiteInfo
{
my $self = shift;
my %results;
my $code;
$self->_SendRequest("siteinfo");
do
{
my @response = $self->_ParseResponse($self->_GetResponse());
if ($response[2])
{
$results{$response[2]} = $response[3];
}
$code = $response[0];
} while ($code < 200);
return $self->_IsSuccessful($code) ? %results : undef;
}
sub Version
{
return $VERSION;
}
1;
__END__
=head1 NAME
Ph -- provide a perl API for talking to CSO ph servers.
=head1 SYNOPSIS
use Ph;
=head1 NOTICE
This version of Ph.pm is not compatbile with pre-2.0 versions.
Users of older versions (primarily at QUALCOMM) will need to
convert their scripts to be compatible with this version of
Ph.pm. (This was done to make Ph.pm more object-oriented, allowing
multiple connections at once.)
=head1 DESCRIPTION
The B module provides a uniform API for I scripts that need to
talk to CSO ph servers. It handles many of the messy details automatically.
It also maintains an open connection to the server, minimizing the costs
of repeated openings and closings of server connections (and the associated
costs on the server of repeatedly forking off short-lived processes to service
requests made by the client.)
In order to use the B protocol module, you will need to have the
L support and associated perl modules installed. You wil
also need to use perl 5.003 or later. (See L.) You should
already be familiar with perl data structures and perl references. If
not, read L and L.
It is assumed that you are already familiar with the ph protocol. If
you are not, please read I
by Steven Dorner and Paul Pomes. It contains all of the descriptions
of the various ph requests that one can make. (This API does simplify
the problems associated with parsing server responses.)
=head1 CONSTRUCTOR
=over 4
=item new ( [ARGS])
The constructor takes a number of arguments.
PhServer: Remote Ph server name (default is 'ns').
PhPort: Remove Ph port (default is 'ns', or '105').
Debug: If set, module debugging is enabled.
=head1 GLOBALS
=item $DefaultPhPort
This constant value is the default port to make connections on if one
is not specified as an argument to C. Its default value
is the value assigned to 'ns' in F, or 105.
=item $DefaultPhServer
The default ph server to use if not specified in the arguments to
C. This defaults to 'ns'.
=head1 METHODS
The following methods are provided as part of the API.
=item Add ENTRY
This is a request to create a B ph record. The record has
fields with values specified by ENTRY, which can be either a
reference to an associative array, or it may just be a string itself.
Hero login status is required. A boolean return indicates whether
the change was successful or not.
=item Change QUERY CHANGES
This is a request to modify existing ph record(s). The QUERY is a
reference to an associative array, or a query string (Exactly as for
the C subroutine.) The CHANGES is another reference to an
associative array (or scalar string), containing the field names to be
changed, and the new values that they are to hold. The value returned
is the ph result code returned by the server. B: Fields that
are Encrypted (e.g. password) cannot be changed, yet! I will implement
this as soon as I better understand how the encryption works. The
return value is a boolean variable indicating whether or not the
change was successful.
=item Connect SERVER PORT
This is a request to establish a connection to a ph server. In
addition, several other requests are sent to the server. They are
C, C, and C. The returned values are stored in
variables for use later. Only one connection to a server can be
active within a perl program at any one time. Returns truth on
success, false on failure.
=item Delete ENTRY
This deletes an entire B from the ph server. The record is
selected by the query matching ENTRY, which is either a reference to
an associative array, or a string query specification. The value
returned is the result code returned by the ph server. B Hero mode is required. Returns true of false to indicate
successful or failed results.
=item Disconnect
This disconnects from the server. It sends the C request before
closing, to be polite to the server. This is also called
automatically by the package destructor, if necessary. No return
value is used.
=item ErrorMessage CODE
This returns a string representation of the message associated with
the result code returned by the server.
=item Fields
Returns an associative array containing the field attributes for the
fields defined by the currently connected ph server. The keys are
the field names, and the values are references to an attribute
description hash:
name the field name
max the maximum length of the field
desc textual description for the field
* other flags (e.g. Default, Lookup, Public) for the field
=item GetLastCode
This returns the last positive result code returned by the Ph server.
=item GetLastMessage
This returns the last message returned by the Ph server. This can be
useful in determining the cause of failure -- it sometimes provides
more detail than is possible with just the GetLastCode value.
=item IsConnected
This returns a boolean value which indicates whether or not an active
connection to a server has been established.
=item Login ALIAS PASSWORD
This request logs into the server, if possible. It should use the
C method rather than the less secure C method to
encrypt the response. Unfortunately, the F file included
with the ph distribution is incredibly, well, I! I am not
exactly sure what it is doing, and converting it to perl is proving
tiresome. (I have been told that the encryption is based upon a 3-rotor
enigma. If anyone is feeling ambitious enough to provide a perl
equivalent, I would be happy to fix this routine.) A truth value is
returned to the caller to indicate whether the login succeeded or not.
=item Logout
Simply put, logout of the ph server! (Obviously, you must be logged
in first!) B: There is a serious bug in some Ph servers
which prevents the logout action from removing any privileges. A hero
mode session that has been logged out can still destroy the database
(accidentally or on purpose). This was discovered the hard way by the
author of this package, who had to reenter his entire ph record by
hand after running over this bug!
=cut
# Undocumented internals:
# =item _GetCode
#
# Suck in all result lines until a result code >= 200 is returned.
# (Lines that have continuations are negative values.) Returns the
# final result code.
#
# =item _MakeFieldsLine FIELDS
#
# This takes a B to an associative array and returns a string
# of the form " field=value ..." based upon the data in the associative
# array. If it is passed scalar, it returns the scalar unmodified.
# This subroutine is used to simplify building of query specifications
# from associative arrays, and is primarily intended for internal use.
#
# =item _ParseResponse RESPONSE
#
# This parses the value returned by C into an array. The
# array is built as ( code sub-code field-name field-value ). Note that
# not all elements will be defined for some responses. This is an
# intermediate access method, and its direct use in user scripts is not
# recommened.
#
# =item _SendRequest PH_REQUEST
#
# This sends a ph request to the server. PH_REQUEST should be string
# (without any terminating newlines or carriage returns) to send to the
# server. This provides a low-level way to send protocol to the server.
# Its direct use is not recommended.
#
# =item _GetResponse
#
# This reads a single line of text from the server (presumably a server
# response) and returns it. This provides a low-level access method to
# the Ph server, and its use is not recommended. It may have to be
# called many times to retrieve all returned lines from the server for a
# single request. Failure to do this will confuse subsequent
# transactions.
#
=item Query QUERY FIELDS
This sends a query to the ph server and builds an array containing
references to associative arrays. These associative arrays correspond
to matching records, with each field of the match represented by a key
in the associative array. QUERY should be a reference to an
associative array containing the fields to query for. Alternatively,
it can be a string of the form "field=value ...".
FIELDS is reference to an array containing the names of the fields to
return. If you want to return all fields, just specify the word
"all". Otherwise, only default fields provided by the server
returned. Note that the ph server may enforce certain restrictions
about what fields you can view.
The returned value
will be an array filled with references to associative arrays. For
example, to obtain the phone number of the first person named "Garrett"
into $phone, you might try this:
@matches = $Ph->Query("name=garrett", "phone");
$phone = $matches[0]->{phone};
=item SiteInfo
Returns an associative array containing the results of the C
ph request for the currently connected server. Each key corresponds
to a field name in the returned result.
=item Version
This function is used merely to determine the RCS revision number of
the module that you are using. Newer versions may be posted from time
to time, and this allows you to determine if you are using the latest
version of the Ph module.
=head1 AUTHOR
This module was written entirely in perl by Garrett D'Amore. It is
Copyright 1995, Qualcomm, Inc. You can reach the author at the
e-mail address B.
=cut