From 7a6c08e9a26cba1dc2730d4157b19f21e18f43fd Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Thu, 3 Sep 2020 15:57:20 -0400 Subject: [PATCH] migrating top sip2 service Signed-off-by: Bill Erickson --- Open-ILS/examples/apache_24/eg_vhost.conf.in | 2 +- .../src/perlmods/lib/OpenILS/Application/SIP2.pm | 16 +- .../src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm | 611 --------------------- .../perlmods/lib/OpenILS/WWW/SIP2Gateway/Item.pm | 153 ------ .../perlmods/lib/OpenILS/WWW/SIP2Gateway/Patron.pm | 320 ----------- .../src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm | 87 +++ 6 files changed, 96 insertions(+), 1093 deletions(-) delete mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm delete mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Item.pm delete mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Patron.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm diff --git a/Open-ILS/examples/apache_24/eg_vhost.conf.in b/Open-ILS/examples/apache_24/eg_vhost.conf.in index 66e5802479..00d4f46091 100644 --- a/Open-ILS/examples/apache_24/eg_vhost.conf.in +++ b/Open-ILS/examples/apache_24/eg_vhost.conf.in @@ -612,7 +612,7 @@ RewriteRule ^/conify/([a-z]{2}-[A-Z]{2})/global/(.*)$ /conify/global/$2 [E=local SetHandler perl-script - PerlHandler OpenILS::WWW::SIP2Gateway + PerlHandler OpenILS::WWW::SIP2Mediator Options +ExecCGI Require all granted diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm index b7ce0b57f4..4a572ea567 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/SIP2.pm @@ -210,8 +210,8 @@ sub handle_patron_info { my ($session, $message) = @_; my $sip_account = $session->sip_account; - my $barcode = get_field_value($message, 'AA'); - my $password = get_field_value($message, 'AD'); + my $barcode = $SC->get_field_value($message, 'AA'); + my $password = $SC->get_field_value($message, 'AD'); my $summary = ref $message->{fixed_fields} ? $message->{fixed_fields}->[2] : ''; @@ -236,12 +236,12 @@ sub handle_patron_info { push( @{$response->{fixed_fields}}, - count4($pdetails->{holds_count}), - count4($pdetails->{overdue_count}), - count4($pdetails->{out_count}), - count4($pdetails->{fine_count}), - count4($pdetails->{recall_count}), - count4($pdetails->{unavail_holds_count}) + $SC->count4($pdetails->{holds_count}), + $SC->count4($pdetails->{overdue_count}), + $SC->count4($pdetails->{out_count}), + $SC->count4($pdetails->{fine_count}), + $SC->count4($pdetails->{recall_count}), + $SC->count4($pdetails->{unavail_holds_count}) ); # TODO: Add diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm deleted file mode 100644 index 86510c509b..0000000000 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway.pm +++ /dev/null @@ -1,611 +0,0 @@ -# --------------------------------------------------------------- -# Copyright (C) 2020 King County Library System -# Bill Erickson -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. - -# This program 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 -# GNU General Public License for more details. -# -# --------------------------------------------------------------- -# Code borrows heavily and sometimes copies directly from from -# ../SIP* and SIPServer* -# --------------------------------------------------------------- -package OpenILS::WWW::SIPSession; -use strict; use warnings; -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'; - -# 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) = @_; - my $self = bless(\%args, $class); -} - -# 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; -} - -package OpenILS::WWW::SIP2Gateway; -use strict; use warnings; -use Apache2::Const -compile => - qw(OK FORBIDDEN NOT_FOUND HTTP_INTERNAL_SERVER_ERROR HTTP_BAD_REQUEST); -use Apache2::RequestRec; -use CGI; -use DateTime; -use DateTime::Format::ISO8601; -use JSON::XS; -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::WWW::SIP2Gateway::Patron; -use OpenILS::WWW::SIP2Gateway::Item; - -my $json = JSON::XS->new; -$json->ascii(1); -$json->allow_nonref(1); - -use constant SIP_DATE_FORMAT => "%Y%m%d %H%M%S"; - -# 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, -]; - -my $osrf_config; -sub import { - $osrf_config = shift; -} - -# We need a global pcrud editor for pre-auth activities. -# Each SIPSession will have its own post-auth pcrud editor. -my $editor; -my $config; -my $init_complete = 0; -sub init { - return if $init_complete; - $init_complete = 1; - - OpenSRF::System->bootstrap_client(config_file => $osrf_config); - OpenILS::Utils::CStoreEditor->init; - - $editor = new_editor(); - - my $settings = $editor->retrieve_all_config_sip_setting; - - $config = {institutions => []}; - - # Institution specific settings. - # In addition to the settings, this tells us what institutions we support. - for my $set (grep {$_->institution ne '*'} @$settings) { - my $inst = $set->institution; - my $value = $json->decode($set->value); - my $name = $set->name; - - my ($inst_conf) = grep {$_->{id} eq $inst} @{$config->{institutions}}; - - if (!$inst_conf) { - $inst_conf = { - id => $inst, - settings => {}, - supports => INSTITUTION_SUPPORTS - }; - - push(@{$config->{institutions}}, $inst_conf); - } - - $inst_conf->{settings}->{$name} = $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); - - for my $inst_conf (@{$config->{institutions}}) { - $inst_conf->{settings}->{$name} = $value - unless exists $inst_conf->{settings}->{$name}; - } - } -} - -sub sipdate { - my $date = shift || DateTime->now; - return $date->strftime(SIP_DATE_FORMAT); -} - -# False == 'N' -sub sipbool { - my $bool = shift; - 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); -} - -sub handler { - my $r = shift; - my $cgi = CGI->new; - my ($message, $msg_code, $response); - - init(); - - my $seskey = $cgi->param('session'); - my $msg_json = $cgi->param('message'); - - if ($seskey && $msg_json) { - eval { $message = $json->decode($msg_json) }; - if ($message) { - $msg_code = $message->{code}; - } else { - $logger->error("SIP2: Error parsing message JSON: $@ : $msg_json"); - } - } - - return Apache2::Const::HTTP_BAD_REQUEST unless $msg_code; - - if ($msg_code eq '93') { - $response = handle_login($seskey, $message); - - } elsif ($msg_code eq '99') { - $response = handle_sc_status($seskey, $message); - - } else { - - # 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::WWW::SIPSession->from_cache($seskey); - - return Apache2::Const::FORBIDDEN unless $session; - - if ($msg_code eq '17') { - $response = handle_item_info($session, $message); - } elsif ($msg_code eq '23') { - $response = handle_patron_status($session, $message); - } elsif ($msg_code eq '63') { - $response = handle_patron_info($session, $message); - } - } - - unless ($response) { - $logger->error("SIP2: no response generated for: $msg_code"); - return Apache2::Const::NOT_FOUND; - } - - $r->content_type('application/json'); - $r->print($json->encode($response)); - - return Apache2::Const::OK; -} - - -# 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; -} - -# Login to Evergreen and cache the login data. -sub handle_login { - my ($seskey, $message) = @_; - - my $response = { - code => '94', - fixed_fields => ['0'] # default to login failed. - }; - - my $sip_username = get_field_value($message, 'CN'); - my $sip_password = get_field_value($message, 'CO'); - my $sip_account = $editor->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( - $editor, $sip_account->usr, $sip_password, 'sip2')) { - - my $session = OpenILS::WWW::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::WWW::SIPSession->from_cache($seskey); - - my $instname; - if ($session) { - $instname = $session->sip_account->institution; - - } else { - - # SC Status requires login? - return undef unless - $config->{settings}->{allow_sc_status_before_login}; - - # The SC Status message does not include an institution, but expects - # one in return. Use the configuration for the first institution. - # Maybe the SIP server itself should track which institutoin its - # instance is configured to use? That may multiple servers could - # run, one per institution. - $instname = $config->{institutions}->[0]->{id}; - } - - my ($instconf) = - grep {$_->{id} eq $instname} @{$config->{institutions}}; - - if (!$instconf) { - $logger->warn("SIP2: No config for institution '$instname'"); - $instconf = {supports => []}; - } - - my $response = { - code => '98', - fixed_fields => [ - sipbool(1), # online_status - sipbool(1), # checkin_ok - sipbool(1), # checkout_ok - sipbool(1), # acs_renewal_policy - sipbool(0), # status_update_ok - sipbool(0), # offline_ok - '999', # timeout_period - '999', # retries_allowed - sipdate(), # transaction date - '2.00' # protocol_version - ], - fields => [ - {AO => $instname}, - {BX => join('', @{$instconf->{supports}})} - ] - } -} - -sub handle_item_info { - my ($session, $message) = @_; - - my $sip_account = $session->sip_account; - my $institution = get_field_value($message, 'AO'); - my $instconf = get_inst_config($institution) || return undef; - my $barcode = get_field_value($message, 'AB'); - - my $idetails = OpenILS::WWW::SIP2Gateway::Item->get_item_details( - session => $session, - instconf => $instconf, - 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 - sipdate() - ], - fields => [{AB => $barcode, AJ => ''}] - }; - }; - - return { - code => '18', - fixed_fields => [ - $idetails->{circ_status}, - '02', # Security Marker, consistent with ../SIP* - $idetails->{fee_type}, - sipdate() - ], - fields => [ - {AB => $barcode}, - {AJ => $idetails->{title}}, - {CF => $idetails->{hold_queue_length}}, - {AH => $idetails->{due_date}}, - {CM => $idetails->{hold_pickup_date}}, - {BG => $idetails->{item}->circ_lib->shortname}, - {BH => $instconf->{currency}}, - {BV => $idetails->{item}->deposit_amount}, - {CK => $idetails->{media_type}}, - {AQ => $idetails->{item}->circ_lib->shortname}, - {AP => $idetails->{item}->circ_lib->shortname}, - ] - }; -} - -sub handle_patron_info { - my ($session, $message) = @_; - my $sip_account = $session->sip_account; - - my $institution = get_field_value($message, 'AO'); - my $barcode = get_field_value($message, 'AA'); - my $password = get_field_value($message, 'AD'); - my $instconf = get_inst_config($institution) || return undef; - my $summary = $message->{fixed_fields}->[2]; - - my $pdetails = OpenILS::WWW::SIP2Gateway::Patron->get_patron_details( - session => $session, - instconf => $instconf, - barcode => $barcode, - password => $password, - summary_start_item => get_field_value($message, 'BP'), - summary_end_item => get_field_value($message, 'BQ'), - summary_list_items => patron_summary_list_items($summary) - ); - - my $response = patron_response_common_data( - $session, $institution, $barcode, $password, $pdetails); - - $response->{code} = '64'; - - return $response unless $pdetails; - - push( - @{$response->{fixed_fields}}, - count4($pdetails->{holds_count}), - count4($pdetails->{overdue_count}), - count4($pdetails->{out_count}), - count4($pdetails->{fine_count}), - count4($pdetails->{recall_count}), - count4($pdetails->{unavail_holds_count}) - ); - - # TODO: Add - # overdue items AT variable-length optional field (this field should be sent for each overdue item). - # charged items AU variable-length optional field (this field should be sent for each charged item). - # fine items AV variable-length optional field (this field should be sent for each fine item). - # recall items BU variable-length optional field (this field should be sent for each recall item). - # unavailable hold items CD variable-length optional field (this field should be sent for each unavailable hold item). - - my $list_items = patron_summary_list_items($summary) || ''; - - if ($list_items eq 'hold_items') { - for my $hold (@{$pdetails->{hold_items}}) { - push(@{$response->{fields}}, {AS => $hold}); - } - } - - return $response; -} - -sub handle_patron_status { - my ($session, $message) = @_; - my $sip_account = $session->sip_account; - - my $institution = get_field_value($message, 'AO'); - my $barcode = get_field_value($message, 'AA'); - my $password = get_field_value($message, 'AD'); - my $instconf = get_inst_config($institution) || return undef; - - my $pdetails = OpenILS::WWW::SIP2Gateway::Patron->get_patron_details( - session => $session, - instconf => $instconf, - barcode => $barcode, - password => $password - ); - - my $response = patron_response_common_data( - $session, $institution, $barcode, $password, $pdetails); - - $response->{code} = '24'; - - return $response; -} - -# Patron Info and Patron Status responses share mostly the same data. -# This returns the base data which can be augmented as needed. -# Note we don't call Patron->get_patron_details here since different -# messages collect different amounts of data. -sub patron_response_common_data { - my ($session, $institution, $barcode, $password, $pdetails) = @_; - - if (!$pdetails) { - # No such user. Return a stub response with all things denied. - - return { - fixed_fields => [ - spacebool(1), # charge denied - spacebool(1), # renew denied - spacebool(1), # recall denied - spacebool(1), # holds denied - split('', (' ' x 10)), - '000', # language - sipdate() - ], - fields => [ - {AO => $institution}, - {AA => $barcode}, - {BL => sipbool(0)}, # valid patron - {CQ => sipbool(0)} # valid patron password - ] - }; - } - - return { - fixed_fields => [ - spacebool($pdetails->{charge_denied}), - spacebool($pdetails->{renew_denied}), - spacebool($pdetails->{recall_denied}), - spacebool($pdetails->{holds_denied}), - spacebool($pdetails->{patron}->card->active eq 'f'), - spacebool(0), # too many charged - spacebool($pdetails->{too_may_overdue}), - spacebool(0), # too many renewals - spacebool(0), # too many claims retruned - spacebool(0), # too many lost - spacebool($pdetails->{too_many_fines}), - spacebool($pdetails->{too_many_fines}), - spacebool(0), # recall overdue - spacebool($pdetails->{too_many_fines}), - '000', # language - sipdate() - ], - fields => [ - {AO => $institution}, - {AA => $barcode}, - {BL => sipbool(1)}, # valid patron - {BV => $pdetails->{balance_owed}}, # fee amount - {CQ => sipbool($password)} # password verified if exists - ] - }; -} - -# Determines which class of data the SIP client wants detailed -# information on in the patron info request. -sub patron_summary_list_items { - my $summary = shift; - - my $idx = index($summary, 'Y'); - - return 'hold_items' if $idx == 0; - return 'overdue_items' if $idx == 1; - return 'charged_items' if $idx == 2; - return 'fine_items' if $idx == 3; - return 'recall_items' if $idx == 4; - return 'unavailable_holds' if $idx == 5; -} - -1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Item.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Item.pm deleted file mode 100644 index 90a4401feb..0000000000 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Item.pm +++ /dev/null @@ -1,153 +0,0 @@ -# --------------------------------------------------------------- -# Copyright (C) 2020 King County Library System -# Bill Erickson -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. - -# This program 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 -# GNU General Public License for more details. -# --------------------------------------------------------------- -package OpenILS::WWW::SIP2Gateway::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/; -my $U = 'OpenILS::Application::AppUtils'; - -sub get_item_details { - my ($class, %params) = @_; - - my $session = $params{session}; - my $instconf = $params{instconf}; - my $barcode = $params{barcode}; - - my $e = new_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} = - $instconf->{due_date_use_sip_date_format} ? - 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 ? 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/WWW/SIP2Gateway/Patron.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Patron.pm deleted file mode 100644 index 5ab3faf5ad..0000000000 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Gateway/Patron.pm +++ /dev/null @@ -1,320 +0,0 @@ -# --------------------------------------------------------------- -# Copyright (C) 2020 King County Library System -# Bill Erickson -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. - -# This program 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 -# GNU General Public License for more details. -# -# --------------------------------------------------------------- -# Code borrows heavily and sometimes copies directly from from -# ../SIP* and SIPServer* -# --------------------------------------------------------------- -package OpenILS::WWW::SIP2Gateway::Patron; -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/; -my $U = 'OpenILS::Application::AppUtils'; - -sub get_patron_details { - my ($class, %params) = @_; - - my $session = $params{session}; - my $instconf = $params{instconf}; - my $barcode = $params{barcode}; - my $password = $params{password}; - - my $e = new_editor(); - my $details = {}; - - my $card = $e->search_actor_card([{ - barcode => $barcode - }, { - flesh => 3, - flesh_fields => { - ac => [qw/usr/], - au => [qw/ - billing_address - mailing_address - profile - stat_cat_entries - /], - actscecm => [qw/stat_cat/] - } - }])->[0]; - - my $patron = $details->{patron} = $card->usr; - $patron->card($card); - - # We only attempt to verify the password if one is provided. - return undef if defined $password && - !$U->verify_migrated_user_password($e, $patron->id, $password); - - my $penalties = get_patron_penalties($session, $patron); - - set_patron_privileges($session, $instconf, $details, $penalties); - - $details->{too_many_overdue} = 1 if - grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_OVERDUE_COUNT} - @$penalties; - - $details->{too_many_fines} = 1 if - grep {$_->{id} == OILS_PENALTY_PATRON_EXCEEDS_FINES} - @$penalties; - - my $summary = $e->retrieve_money_open_user_summary($patron->id); - $details->{balance_owed} = ($summary) ? $summary->balance_owed : 0; - - set_patron_summary_items($session, $instconf, $details, %params); - set_patron_summary_list_items($session, $instconf, $details, %params); - - return $details; -} - - -# Sets: -# holds_count -# overdue_count -# out_count -# fine_count -# recall_count -# unavail_holds_count -sub set_patron_summary_items { - my ($session, $instconf, $details, %params) = @_; - - my $patron = $details->{patron}; - my $e = new_editor(); - - $details->{recall_count} = 0; # not supported - - my $hold_ids = get_hold_ids($e, $instconf, $patron); - $details->{holds_count} = scalar(@$hold_ids); - - my $unavail_hold_ids = get_hold_ids($e, $instconf, $patron, 1); - $details->{unavail_holds_count} = scalar(@$unavail_hold_ids); - - $details->{overdue_count} = 0; - $details->{out_count} = 0; - - my $circ_summary = $e->retrieve_action_open_circ_list($patron->id); - if ($circ_summary) { # undef if no circs for user - my $overdue_ids = [ grep {$_ > 0} split(',', $circ_summary->overdue) ]; - my $out_ids = [ grep {$_ > 0} split(',', $circ_summary->out) ]; - $details->{overdue_count} = scalar(@$overdue_ids); - $details->{out_count} = scalar(@$out_ids) + scalar(@$overdue_ids); - } - - my $xacts = $U->simplereq( - 'open-ils.actor', - 'open-ils.actor.user.transactions.history.have_balance', - $session->ils_authtoken, - $patron->id - ); - - $details->{fine_count} = scalar(@$xacts); -} - -sub get_hold_ids { - my ($e, $instconf, $patron, $unavail, $offset, $limit) = @_; - - my $holds_where = { - usr => $patron->id, - fulfillment_time => undef, - cancel_time => undef - }; - - if ($unavail) { - $holds_where->{'-or'} = [ - {current_shelf_lib => undef}, - {current_shelf_lib => {'!=' => {'+ahr' => 'pickup_lib'}}} - ]; - - } else { - - $holds_where->{current_shelf_lib} = {'=' => {'+ahr' => 'pickup_lib'}} - if $instconf->{msg64_hold_items_available}; - } - - my $query = { - select => {ahr => ['id']}, - from => 'ahr', - where => {'+ahr' => $holds_where} - }; - - $query->{offset} = $offset if $offset; - $query->{limit} = $limit if $limit; - - my $id_hashes = $e->json_query($query); - - return [map {$_->{id}} @$id_hashes]; -} - -sub set_patron_summary_list_items { - my ($session, $instconf, $details, %params) = @_; - my $e = new_editor(); - - my $list_items = $params{summary_list_items}; - my $offset = $params{summary_start_item} || 0; - my $end_item = $params{summary_end_item} || 10; - my $limit = $end_item - $offset; - - add_hold_items($e, $session, $instconf, $details, $offset, $limit) - if $list_items eq 'hold_items'; -} - -sub add_hold_items { - my ($e, $session, $instconf, $details, $offset, $limit) = @_; - - my $patron = $details->{patron}; - my $format = $instconf->{msg64_hold_datatype} || ''; - my $hold_ids = get_hold_ids($e, $instconf, $patron, 0, $offset, $limit); - - my @hold_items; - for my $hold_id (@$hold_ids) { - my $hold = $e->retrieve_action_hold_request($hold_id); - - if ($format eq 'barcode') { - my $copy = find_copy_for_hold($e, $hold); - push(@hold_items, $copy->barcode) if $copy; - } else { - my $title = find_title_for_hold($e, $hold); - push(@hold_items, $title) if $title; - } - } - - $details->{hold_items} = \@hold_items; -} - -# Hold -> reporter.hold_request_record -> display field for title. -sub find_title_for_hold { - my ($e, $hold) = @_; - - my $bib_link = $e->retrieve_reporter_hold_request_record($hold->id); - - my $title_field = $e->search_metabib_flat_display_entry({ - source => $bib_link->bib_record, name => 'title'})->[0]; - - return $title_field ? $title_field->value : ''; -} - -# Finds a representative copy for the given hold. If no copy exists at -# all, undef is returned. The only limit placed on what constitutes a -# "representative" copy is that it cannot be deleted. Otherwise, any -# copy that allows us to find the hold later is good enough. -sub find_copy_for_hold { - my ($e, $hold) = @_; - - return $e->retrieve_asset_copy($hold->current_copy) - if $hold->current_copy; - - return $e->retrieve_asset_copy($hold->target) - if $hold->hold_type =~ /C|R|F/; - - return $e->search_asset_copy([ - {call_number => $hold->target, deleted => 'f'}, - {limit => 1}])->[0] if $hold->hold_type eq 'V'; - - my $bre_ids = [$hold->target]; - - if ($hold->hold_type eq 'M') { - # find all of the bibs that link to the target metarecord - my $maps = $e->search_metabib_metarecord_source_map( - {metarecord => $hold->target}); - $bre_ids = [map {$_->record} @$maps]; - } - - my $vol_ids = $e->search_asset_call_number( - {record => $bre_ids, deleted => 'f'}, - {idlist => 1} - ); - - return $e->search_asset_copy([ - {call_number => $vol_ids, deleted => 'f'}, - {limit => 1} - ])->[0]; -} - - -sub set_patron_privileges { - my ($session, $instconf, $details, $penalties) = @_; - my $patron = $details->{patron}; - - my $expire = DateTime::Format::ISO8601->new - ->parse_datetime(clean_ISO8601($patron->expire_date)); - - if ($expire < DateTime->now) { - $logger->info("SIP2 Patron account is expired; all privileges blocked"); - $details->{charge_denied} = 1; - $details->{recall_denied} = 1; - $details->{renew_denied} = 1; - $details->{holds_denied} = 1; - return; - } - - # Non-expired patrons are allowed all privileges when - # patron_status_permit_all is true. - return if $instconf->{patron_status_permit_all}; - - my $blocked = ( - $patron->barred eq 't' - || $patron->active eq 'f' - || $patron->card->active eq 'f' - ); - - my @block_tags = map {$_->{block_list}} grep {$_->{block_list}} @$penalties; - - return unless $blocked || @block_tags; # no blocks remain - - $details->{holds_denied} = ($blocked || grep {$_ =~ /HOLD/} @block_tags); - - # Ignore loan-related blocks? - return if $instconf->{patron_status_permit_loans}; - - $details->{charge_denied} = ($blocked || grep {$_ =~ /CIRC/} @block_tags); - $details->{renew_denied} = ($blocked || grep {$_ =~ /RENEW/} @block_tags); - - # In evergreen, patrons cannot create Recall holds directly, but that - # doesn't mean they would not have said privilege if the functionality - # existed. Base the ability to perform recalls on whether they have - # checkout and holds privilege, since both would be needed for recalls. - $details->{recall_denied} = - ($details->{charge_denied} || $details->{holds_denied}); -} - -# Returns an array of penalty hashes with keys "id" and "block_list" -sub get_patron_penalties { - my ($session, $patron) = @_; - - return new_editor()->json_query({ - select => {csp => ['id', 'block_list']}, - from => {ausp => 'csp'}, - where => { - '+ausp' => { - usr => $patron->id, - '-or' => [ - {stop_date => undef}, - {stop_date => {'>' => 'now'}} - ], - org_unit => - $U->get_org_full_path($session->editor->requestor->ws_ou) - } - } - }); -} - - - - -1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm new file mode 100644 index 0000000000..9e0f18af44 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SIP2Mediator.pm @@ -0,0 +1,87 @@ +# --------------------------------------------------------------- +# Copyright (C) 2020 King County Library System +# Bill Erickson +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. + +# This program 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 +# GNU General Public License for more details. +# +# --------------------------------------------------------------- +# Code borrows heavily and sometimes copies directly from from +# ../SIP* and SIPServer* +# --------------------------------------------------------------- +package OpenILS::WWW::SIP2Mediator; +use strict; use warnings; +use Apache2::Const -compile => + qw(OK FORBIDDEN NOT_FOUND HTTP_INTERNAL_SERVER_ERROR HTTP_BAD_REQUEST); +use Apache2::RequestRec; +use CGI; +use JSON::XS; +use OpenSRF::System; +use OpenSRF::Utils::Logger q/$logger/; +use OpenILS::Application::AppUtils; +my $U = 'OpenILS::Application::AppUtils'; + +my $json = JSON::XS->new; +$json->ascii(1); +$json->allow_nonref(1); + +my $osrf_config; +sub import { + $osrf_config = shift; +} + +my $init_complete = 0; +sub init { + return if $init_complete; + $init_complete = 1; + OpenSRF::System->bootstrap_client(config_file => $osrf_config); +} + +sub handler { + my $r = shift; + my $cgi = CGI->new; + my ($message, $msg_code); + + init(); + + my $seskey = $cgi->param('session'); + my $msg_json = $cgi->param('message'); + + return Apache2::Const::FORBIDDEN unless $seskey; + + if ($msg_json) { + eval { $message = $json->decode($msg_json) }; + if ($message) { + $msg_code = $message->{code}; + } else { + $logger->error("SIP2: Error parsing message JSON: $@ : $msg_json"); + } + } + + return Apache2::Const::HTTP_BAD_REQUEST unless $msg_code; + + my $response = $U->simplereq( + 'open-ils.sip2', + 'open-ils.sip2.request', $seskey, $message); + + if (my $textcode = $response->{textcode}) { + # we got an event instead of a SIP response + $logger->error("SIP2: Request returned $textcode: $msg_json"); + # TODO: Could return more context-specific responses. + return Apache2::Const::HTTP_BAD_REQUEST; + } + + $r->content_type('application/json'); + $r->print($json->encode($response)); + + return Apache2::Const::OK; +} + +1; -- 2.11.0