From 10881b0f3ce93adfa789998dad996125bd4edbeb Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Thu, 3 Sep 2020 15:06:32 -0400 Subject: [PATCH] migrating top sip2 service Signed-off-by: Bill Erickson --- Open-ILS/examples/opensrf.xml.example | 25 +++ .../src/perlmods/lib/OpenILS/Application/SIP2.pm | 194 +++++++++++++++++++++ .../lib/OpenILS/Application/SIP2/Common.pm | 42 +++++ .../perlmods/lib/OpenILS/Application/SIP2/Item.pm | 141 +++++++++++++++ .../lib/OpenILS/Application/SIP2/Session.pm | 166 ++++++++++++++++++ .../src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm | 12 -- 6 files changed, 568 insertions(+), 12 deletions(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Common.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Item.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Session.pm diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index 36a43961ab..5eecd0051e 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -1289,6 +1289,26 @@ vim:et:ts=4:sw=4: + + 5 + 1 + perl + OpenILS::Application::SIP2 + 100 + + sip2_unix.sock + sip2_unix.pid + sip2_unix.log + 1000 + 1 + 15 + 1 + 5 + + + + + 5 1 @@ -1308,6 +1328,7 @@ vim:et:ts=4:sw=4: + @@ -1355,8 +1376,12 @@ vim:et:ts=4:sw=4: open-ils.serial open-ils.hold-targeter open-ils.ebook_api +<<<<<<< HEAD open-ils.courses open-ils.curbside +======= + open-ils.sip2 +>>>>>>> migrating top sip2 service diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm new file mode 100644 index 0000000000..e765d5b3f1 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm @@ -0,0 +1,194 @@ +package OpenILS::Application::SIP2; +use strict; use warnings; +use base 'OpenILS::Application'; +use OpenSRF::Utils::Cache; +use OpenILS::Application; +use OpenILS::Event; +use OpenILS::Utils::Fieldmapper; +use OpenSRF::Utils::Logger qw(:logger); +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenILS::Application::AppUtils; +use OpenILS::Application::SIP2::Common; +use OpenILS::Application::SIP2::Session; +use OpenILS::Application::SIP2::Item; +my $U = 'OpenILS::Application::AppUtils'; +my $SC = 'OpenILS::Application::SIP2::Common'; + + +__PACKAGE__->register_method( + method => 'dispatch_sip2_request', + api_name => 'open-ils.sip2.request', + api_level => 1, + argc => 2, + signature => { + desc => q/ + Takes a SIP2 JSON message and handles the request/, + params => [{ + name => 'seskey', + desc => 'The SIP2 session key', + type => 'string' + }, { + name => 'message', + desc => 'SIP2 JSON message', + type => q/SIP JSON object/ + }], + return => { + desc => q/SIP2 JSON message on success, Event on error/, + type => 'object' + } + } +); + +sub dispatch_sip2_request { + my ($self, $client, $seskey, $message) = @_; + + return OpenILS::Event->new('SIP2_SESSION_REQUIRED') unless $seskey; + my $msg_code = $message->{code}; + + return handle_login($seskey, $message) if $msg_code eq '93'; + return handle_sc_status($seskey, $message) if $msg_code eq '99'; + + # A cached session means we have successfully logged in with + # the SIP credentials provided during a login request. All + # message types following require authentication. + my $session = OpenILS::Application::SIPSession->from_cache($seskey); + return OpenILS::Event->new('SIP2_SESSION_REQUIRED') unless $session; + + my $MESSAGE_MAP = { + '17' => &handle_item_info, + '23' => &handle_patron_status, + '63' => &handle_patron_info + }; + + return OpenILS::Event->new('SIP2_NOT_IMPLEMENTED', {payload => $message}) + unless exists $MESSAGE_MAP->{$msg_code}; + + return $MESSAGE_MAP->{$msg_code}->($session, $message); +} + +# Login to Evergreen and cache the login data. +sub handle_login { + my ($seskey, $message) = @_; + my $e = new_editor(); + + # Default to login-failed + my $response = {code => '94', fixed_fields => ['0']}; + + my $sip_username = get_field_value($message, 'CN'); + my $sip_password = get_field_value($message, 'CO'); + my $sip_account = $e->search_config_sip_account([ + {sip_username => $sip_username, enabled => 't'}, + {flesh => 1, flesh_fields => {csa => ['workstation']}} + ])->[0]; + + if (!$sip_account) { + $logger->warn("SIP2: No such SIP account: $sip_username"); + return $response; + } + + if ($U->verify_user_password($e, $sip_account->usr, $sip_password, 'sip2')) { + + my $session = OpenILS::Application::SIPSession->new( + seskey => $seskey, + sip_account => $sip_account + ); + $response->{fixed_fields}->[0] = '1' if $session->set_ils_account; + + } else { + $logger->info("SIP2: login failed for user=$sip_username") + } + + return $response; +} + +sub handle_sc_status { + my ($seskey, $message) = @_; + + my $session = OpenILS::Application::SIPSession->from_cache($seskey); + + my $config; + + if ($session) { + $config = $session->config; + + } else { + # TODO: where should the 'allow_sc_status_before_login' setting + # live, since we don't yet have an institution configuration loaded? + # TODO: Do we need a 'default institution' setting? + $config = {id => 'NONE', supports => [], settings => {}}; + } + + my $response = { + code => '98', + fixed_fields => [ + $SC->sipbool(1), # online_status + $SC->sipbool(1), # checkin_ok + $SC->sipbool(1), # checkout_ok + $SC->sipbool(1), # acs_renewal_policy + $SC->sipbool(0), # status_update_ok + $SC->sipbool(0), # offline_ok + '999', # timeout_period + '999', # retries_allowed + $SC->sipdate, # transaction date + '2.00' # protocol_version + ], + fields => [ + {AO => $config->{id}}, + {BX => join('', @{$config->{supports}})} + ] + } +} + +sub handle_item_info { + my ($session, $message) = @_; + + my $barcode = get_field_value($message, 'AB'); + my $config = $session->config; + + my $idetails = OpenILS::Application::SIP2::Item->get_item_details( + $session, barcode => $barcode + ); + + if (!$idetails) { + # No matching item found, return a minimal response. + return { + code => '18', + fixed_fields => [ + '01', # circ status: other/Unknown + '01', # security marker: other/unknown + '01', # fee type: other/unknown + $SC->sipdate + ], + fields => [{AB => $barcode, AJ => ''}] + }; + }; + + return { + code => '18', + fixed_fields => [ + $idetails->{circ_status}, + '02', # Security Marker, consistent with ../SIP* + $idetails->{fee_type}, + $SC->sipdate + ], + fields => [ + {AB => $barcode}, + {AH => $idetails->{due_date}}, + {AJ => $idetails->{title}}, + {AP => $idetails->{item}->circ_lib->shortname}, + {AQ => $idetails->{item}->circ_lib->shortname}, + {BG => $idetails->{item}->circ_lib->shortname}, + {BH => $config->{settings}->{currency}}, + {BV => $idetails->{item}->deposit_amount}, + {CF => $idetails->{hold_queue_length}}, + {CK => $idetails->{media_type}}, + {CM => $idetails->{hold_pickup_date}} + ] + }; +} + +1; + + + + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Common.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Common.pm new file mode 100644 index 0000000000..424fa52296 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Common.pm @@ -0,0 +1,42 @@ +package OpenILS::Application::SIP2::Common; +use strict; use warnings; + +use constant SIP_DATE_FORMAT => "%Y%m%d %H%M%S"; + +sub sipdate { + my ($class, $date) = @_; + $date ||= DateTime->now; + return $date->strftime(SIP_DATE_FORMAT); +} + +# False == 'N' +sub sipbool { + my ($class, $bool) = @_; + return $bool ? 'Y' : 'N'; +} + +# False == ' ' +sub spacebool { + my $bool = shift; + return $bool ? 'Y' : ' '; +} + +sub count4 { + my $value = shift; + return ' ' unless defined $value; + return sprintf("%04d", $value); +} + +# Returns the value of the first occurrence of the requested SIP code. +sub get_field_value { + my ($message, $code) = @_; + for my $field (@{$message->{fields}}) { + while (my ($c, $v) = each(%$field)) { # one pair per field + return $v if $c eq $code; + } + } + + return undef; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Item.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Item.pm new file mode 100644 index 0000000000..1def432981 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Item.pm @@ -0,0 +1,141 @@ +package OpenILS::Application::SIP2::Item; +use strict; use warnings; +use DateTime; +use DateTime::Format::ISO8601; +use OpenSRF::System; +use OpenILS::Utils::CStoreEditor q/:funcs/; +use OpenSRF::Utils::Logger q/$logger/; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::DateTime qw/:datetime/; +use OpenILS::Const qw/:const/; +use OpenILS::Application::SIP2::Common; +use OpenILS::Application::SIP2::Session; +my $U = 'OpenILS::Application::AppUtils'; +my $SC = 'OpenILS::Application::SIP2::Common'; + +sub get_item_details { + my ($class, $session, %params) = @_; + + my $config = $session->config; + my $barcode = $params{barcode}; + my $e = $session->editor; + + my $item = $e->search_asset_copy([{ + barcode => $barcode, + deleted => 'f' + }, { + flesh => 3, + flesh_fields => { + acp => [qw/circ_lib call_number + status stat_cat_entry_copy_maps circ_modifier/], + acn => [qw/owning_lib record/], + bre => [qw/flat_display_entries/], + ascecm => [qw/stat_cat stat_cat_entry/], + } + }])->[0]; + + return undef unless $item; + + my $details = {item => $item}; + + $details->{circ} = $e->search_action_circulation([{ + target_copy => $item->id, + checkin_time => undef, + '-or' => [ + {stop_fines => undef}, + {stop_fines => [qw/MAXFINES LONGOVERDUE/]}, + ] + }, { + flesh => 2, + flesh_fields => {circ => ['usr'], au => ['card']} + }])->[0]; + + if ($details->{circ}) { + + my $due_date = DateTime::Format::ISO8601->new-> + parse_datetime(clean_ISO8601($details->{circ}->due_date)); + + $details->{due_date} = + $config->{due_date_use_sip_date_format} ? + $SC->sipdate($due_date) : + $due_date->strftime('%F %T'); + } + + + if ($item->status->id == OILS_COPY_STATUS_IN_TRANSIT) { + $details->{transit} = $e->search_action_transit_copy([{ + target_copy => $item->id, + dest_recv_time => undef, + cancel_time => undef + },{ + flesh => 1, + flesh_fields => {atc => ['dest']} + }])->[0]; + } + + if ($item->status->id == OILS_COPY_STATUS_ON_HOLDS_SHELF || ( + $details->{transit} && + $details->{transit}->copy_status == OILS_COPY_STATUS_ON_HOLDS_SHELF)) { + + $details->{hold} = $e->search_action_hold_request([{ + current_copy => $item->id, + capture_time => {'!=' => undef}, + cancel_time => undef, + fulfillment_time => undef + }, { + limit => 1, + flesh => 1, + flesh_fields => {ahr => ['pickup_lib']} + }])->[0]; + } + + + if ($details->{hold}) { + my $pickup_date = $details->{hold}->shelf_expire_time; + $details->{hold_pickup_date} = + $pickup_date ? $SC->sipdate($pickup_date) : undef; + } + + my ($title_entry) = grep {$_->name eq 'title'} + @{$item->call_number->record->flat_display_entries}; + + $details->{title} = $title_entry ? $title_entry->value : ''; + + # Same as ../SIP* + $details->{hold_queue_length} = $details->{hold} ? 1 : 0; + + $details->{circ_status} = circulation_status($item->status->id); + + $details->{fee_type} = + ($item->deposit_amount > 0.0 && $item->deposit eq 'f') ? + '06' : '01'; + + my $cmod = $item->circ_modifier; + $details->{magnetic_media} = $cmod && $cmod->magnetic_media eq 't'; + $details->{media_type} = $cmod ? $cmod->sip2_media_type : '001'; + + return $details; +} + +# Maps item status to SIP circulation status constants. +sub circulation_status { + my $stat = shift; + + return '02' if $stat == OILS_COPY_STATUS_ON_ORDER; + return '03' if $stat == OILS_COPY_STATUS_AVAILABLE; + return '04' if $stat == OILS_COPY_STATUS_CHECKED_OUT; + return '06' if $stat == OILS_COPY_STATUS_IN_PROCESS; + return '08' if $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF; + return '09' if $stat == OILS_COPY_STATUS_RESHELVING; + return '10' if $stat == OILS_COPY_STATUS_IN_TRANSIT; + return '12' if ( + $stat == OILS_COPY_STATUS_LOST || + $stat == OILS_COPY_STATUS_LOST_AND_PAID + ); + return '13' if $stat == OILS_COPY_STATUS_MISSING; + + return '01'; +} + +1 + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Session.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Session.pm new file mode 100644 index 0000000000..12f36bfd4e --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2/Session.pm @@ -0,0 +1,166 @@ +package OpenILS::Application::SIPSession; +use strict; use warnings; +use JSON::XS; +use OpenSRF::Utils::Cache; +use OpenSRF::Utils::Logger q/$logger/; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor q/:funcs/; +my $U = 'OpenILS::Application::AppUtils'; +my $json = JSON::XS->new; +$json->ascii(1); +$json->allow_nonref(1); + +# Supported Messages (BX) +# Currently hard-coded, since it's based on availabilty of functionality +# in the code, but it could be moved into the database to limit access for +# specific institutions. +use constant INSTITUTION_SUPPORTS => [ + 'Y', # patron status request, + 'Y', # checkout, + 'Y', # checkin, + 'N', # block patron, + 'Y', # acs status, + 'N', # request sc/acs resend, + 'Y', # login, + 'Y', # patron information, + 'N', # end patron session, + 'Y', # fee paid, + 'Y', # item information, + 'N', # item status update, + 'N', # patron enable, + 'N', # hold, + 'Y', # renew, + 'N', # renew all, +]; + +# Cache instances cannot be created until opensrf is connected. +my $_cache; +sub cache { + $_cache = OpenSRF::Utils::Cache->new unless $_cache; + return $_cache; +} + +sub new { + my ($class, %args) = @_; + return bless(\%args, $class); +} + +sub config { + my $self = shift; + return $self->{config} if $self->{config}; + + my $inst = $self->sip_account->institution; + + my $config = { + id => $inst, + settings => { + currency => 'USD' # TODO add db setting + }, + supports => INSTITUTION_SUPPORTS + }; + + # Institution "*" provides default values for all institution configs. + my $settings = + $self->editor->search_config_sip_setting({institution => ['*', $inst]}); + + # Institution specific settings. + for my $set (grep {$_->institution eq $inst} @$settings) { + $config->{settings}->{$set->name} = $json->decode($set->value); + } + + # Apply values for global settings without replacing + # institution-specific values. + for my $set (grep {$_->institution eq '*'} @$settings) { + my $name = $set->name; + my $value = $json->decode($set->value); + + $config->{settings}->{$name} = $value + unless exists $config->{settings}->{$name}; + } + + return $self->{config} = $config; +} + +# Create a new sessesion from cached data. +sub from_cache { + my ($class, $seskey) = @_; + + my $ses = cache()->get_cache("sip2_$seskey"); + + if ($ses) { + + my $session = $class->new( + seskey => $seskey, + sip_account => $ses->{sip_account} + ); + + $session->editor->authtoken($ses->{ils_authtoken}); + + return $session if $session->set_ils_account; + + return undef; + + } else { + + $logger->warn("SIP2: No session found in cache for key $seskey"); + return undef; + } +} + +# The editor contains the authtoken and ILS user account (requestor). +sub editor { + my $self = shift; + $self->{editor} = new_editor() unless $self->{editor}; + return $self->{editor}; +} + +sub seskey { + my $self = shift; + return $self->{seskey}; +} + +# SIP account +sub sip_account { + my $self = shift; + return $self->{sip_account}; +} + +# Logs in to Evergreen and caches the auth token/login with the SIP +# account data. +# Returns true on success, false on failure to authenticate. +sub set_ils_account { + my $self = shift; + + # Verify previously applied authtoken is still valid. + return 1 if $self->editor->authtoken && $self->editor->checkauth; + + my $seskey = $self->seskey; + + my $auth = $U->simplereq( + 'open-ils.auth_internal', + 'open-ils.auth_internal.session.create', { + user_id => $self->sip_account->usr, + workstation => $self->sip_account->workstation->name, + login_type => 'staff' + }); + + if ($auth->{textcode} ne 'SUCCESS') { + $logger->warn( + "SIP2 failed to create an internal login session for ILS user: ". + $self->sip_account->usr); + return 0; + } + + my $ses = { + sip_account => $self->sip_account, + ils_authtoken => $auth->{payload}->{authtoken} + }; + + $self->editor->authtoken($ses->{ils_authtoken}); + $self->editor->checkauth; + + cache()->put_cache("sip2_$seskey", $ses); + return 1; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm index 5e070a623a..86510c509b 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm @@ -315,18 +315,6 @@ sub get_field_value { return undef; } -# Returns the configuation chunk mapped to the requested institution. -sub get_inst_config { - my $institution = shift; - my ($instconf) = grep {$_->{id} eq $institution} @{$config->{institutions}}; - - $logger->error( - "SIP2: has no configuration for institution: $institution") - unless $instconf; - - return $instconf; -} - # Login to Evergreen and cache the login data. sub handle_login { my ($seskey, $message) = @_; -- 2.11.0