1 # ---------------------------------------------------------------
2 # Copyright © 2014 Jason J.A. Stephenson <jason@sigio.com>
4 # This file is part of NCIPServer.
6 # NCIPServer is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # NCIPServer is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with NCIPServer. If not, see <http://www.gnu.org/licenses/>.
18 # ---------------------------------------------------------------
19 package NCIP::ILS::Evergreen;
22 use XML::LibXML::Simple qw(XMLin);
24 use DateTime::Format::ISO8601;
25 use Digest::MD5 qw/md5_hex/;
27 use OpenSRF::Utils qw/:datetime/;
28 use OpenSRF::Utils::SettingsClient;
29 use OpenILS::Utils::Fieldmapper;
30 use OpenILS::Utils::CStoreEditor qw/:funcs/;
31 use OpenILS::Application::AppUtils;
32 use OpenILS::Const qw/:const/;
37 # We need a bunch of NCIP::* objects.
41 use NCIP::User::OptionalFields;
42 use NCIP::User::AddressInformation;
44 use NCIP::User::BlockOrTrap;
45 use NCIP::User::Privilege;
46 use NCIP::User::PrivilegeStatus;
47 use NCIP::StructuredPersonalUserName;
48 use NCIP::StructuredAddress;
49 use NCIP::ElectronicAddress;
51 # Inherit from NCIP::ILS.
52 use parent qw(NCIP::ILS);
54 # Default values we define for things that might be missing in our
55 # runtime environment or configuration file that absolutely must have
58 # OILS_NCIP_CONFIG_DEFAULT is the default location to find our
59 # driver's configuration file. This location can be overridden by
60 # setting the path in the OILS_NCIP_CONFIG environment variable.
62 # BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
63 # "short" bibs. It is used only if no entry is supplied in the
64 # configuration file. The provided default is 2, the id of the
65 # "System Local" source that comes with a default Evergreen
68 OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
69 BIB_SOURCE_DEFAULT => 2
72 # A common Evergreen code shortcut to use AppUtils:
73 my $U = 'OpenILS::Application::AppUtils';
75 # The usual constructor:
78 $class = ref $class or $class;
80 # Instantiate our parent with the rest of the arguments. It
81 # creates a blessed hashref.
82 my $self = $class->SUPER::new(@_);
84 # Look for our configuration file, load, and parse it:
87 # Bootstrap OpenSRF and prepare some OpenILS components.
90 # Initialize the rest of our internal state.
100 # Check our session and login if necessary.
101 $self->login() unless ($self->checkauth());
103 my $message_type = $self->parse_request_type($request);
105 # Let's go ahead and create our response object. We need this even
106 # if there is a problem.
107 my $response = NCIP::Response->new({type => $message_type . "Response"});
108 $response->header($self->make_header($request));
110 # Need to parse the request object to get the barcode and other
112 my $barcode = $self->find_barcode($request);
114 # If we can't find a barcode, report a problem.
116 # Fill in a problem object and stuff it in the response.
117 my $problem = NCIP::Problem->new();
118 $problem->ProblemType('Needed Data Missing');
119 $problem->ProblemDetail('Cannot find user barcode in message.');
120 $problem->ProblemElement('AuthenticationInputType');
121 $problem->ProblemValue('Barcode');
122 $response->problem($problem);
126 # Look up our patron by barcode:
127 my $user = $U->simplereq(
129 'open-ils.actor.user.fleshed.retrieve_by_barcode',
130 $self->{session}->{authtoken},
135 # Check for a failure, or a deleted, inactive, or expired user,
136 # and if so, return empty userdata.
137 if (!$user || $U->event_code($user) || $U->is_true($user->deleted())
138 || !grep {$_->barcode() eq $barcode && $U->is_true($_->active())} @{$user->cards()}) {
140 my $problem = NCIP::Problem->new();
141 $problem->ProblemType('Unknown User');
142 $problem->ProblemDetail("User with barcode $barcode unknown");
143 $problem->ProblemElement('AuthenticationInputData');
144 $problem->ProblemValue($barcode);
148 # We got the information, so lets fill in our userdata.
149 my $userdata = NCIP::User->new();
151 # Make an array of the user's active barcodes.
153 foreach my $card (@{$user->cards()}) {
154 if ($U->is_true($card->active())) {
155 my $id = NCIP::User::Id->new({
156 UserIdentifierType => 'Barcode',
157 UserIdentifierValue => $card->barcode()
162 $userdata->UserId($ids);
164 # Check if they requested any optional fields and return those.
165 my $elements = $request->{$message_type}->{UserElementType};
167 $elements = [$elements] unless (ref $elements eq 'ARRAY');
168 my $optionalfields = NCIP::User::OptionalFields->new();
170 # First, we'll look for name information.
171 if (grep {$_ eq 'Name Information'} @$elements) {
172 my $name = NCIP::StructuredPersonalUserName->new();
173 $name->Surname($user->given_name());
174 $name->GivenName($user->first_given_name());
175 $name->Prefix($user->prefix());
176 $name->Suffix($user->suffix());
177 $optionalfields->NameInformation($name);
180 # Next, check for user address information.
181 if (grep {$_ eq 'User Address Information'} @$elements) {
184 # See if the user has any valid, physcial addresses.
185 foreach my $addr (@{$user->addresses()}) {
186 next if ($U->is_true($addr->pending()));
187 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>$addr->address_type()});
188 my $physical = NCIP::StructuredAddress->new();
189 $physical->Line1($addr->street1());
190 $physical->Line2($addr->street2());
191 $physical->Locality($addr->city());
192 $physical->Region($addr->state());
193 $physical->PostalCode($addr->post_code());
194 $physical->Country($addr->country());
195 $address->PhysicalAddress($physical);
196 push @$addresses, $address;
199 # Right now, we're only sharing email address if the user
200 # has it. We don't share phone numbers.
201 if ($user->email()) {
202 my $address = NCIP::User::AddressInformation->new({UserAddressRoleType=>'Email Address'});
203 $address->ElectronicAddress(
204 NCIP::ElectronicAddress->new({
205 Type=>'Email Address',
209 push @$addresses, $address;
212 $optionalfields->UserAddressInformation($addresses);
215 # Fetch the user's home_ou. We'll need for a couple of things
217 my $aou = $self->editor->retrieve_actor_org_unit($user->home_ou());
219 # Check for User Privilege.
220 if (grep {$_ eq 'User Privilege'} @$elements) {
221 # Get the user's group:
222 my $pgt = $self->editor->retrieve_permission_grp_tree($user->profile());
224 my $privilege = NCIP::User::Privilege->new();
225 $privilege->AgencyId($aou->shortname());
226 $privilege->AgencyUserPrivilegeType($pgt->name());
227 $privilege->ValidToDate($user->expire_date());
229 my $status = 'Active';
230 if (_expired($user)) {
232 } elsif ($U->is_true($user->barred())) {
234 } elsif (!$U->is_true($user->active())) {
235 $status = 'Inactive';
238 $privilege->UserPrivilegeStatus(
239 NCIP::User::PrivilegeStatus->new({
240 UserPrivilegeStatusType => $status
245 $optionalfields->UserPrivilege([$privilege]);
249 # Check for Block Or Trap.
250 if (grep {$_ eq 'Block Or Trap'} @$elements) {
253 # First, let's check if the profile is blocked from ILL.
254 if (grep {$_->id() == $user->profile()} @{$self->{block_profiles}}) {
255 my $block = NCIP::User::BlockOrTrap->new();
256 $block->AgencyId($aou->shortname());
257 $block->BlockOrTrapType('Block Interlibrary Loan');
258 push @$blocks, $block;
261 # Next, we loop through the user's standing penalties
262 # looking for blocks on CIRC, HOLD, and RENEW.
263 my ($have_circ, $have_renew, $have_hold) = (0,0,0);
264 foreach my $penalty (@{$user->standing_penalties()}) {
265 my @block_list = split(/\|/, $penalty->standing_penalty->block_list());
266 my $ou = $self->editor->retrieve_actor_org_unit($penalty->standing_penalty->org_unit());
269 if (!$have_circ && grep {$_ eq 'CIRC'} @block_list) {
270 my $bot = NCIP::User::BlockOrTrap->new();
271 $bot->AgencyId($ou->shortname());
272 $bot->BlockOrTrapType('Block Checkout');
278 if (!$have_hold && grep {$_ eq 'HOLD'} @block_list) {
279 my $bot = NCIP::User::BlockOrTrap->new();
280 $bot->AgencyId($ou->shotrname());
281 $bot->BlockOrTrapType('Block Holds');
287 if (!$have_renew && grep {$_ eq 'RENEW'} @block_list) {
288 my $bot = NCIP::User::BlockOrTrap->new();
289 $bot->AgencyId($ou->shortname());
290 $bot->BlockOrTrapType('Block Renewals');
295 # Stop after we report one of each, even if more
297 last if ($have_circ && $have_renew && $have_hold);
300 $optionalfields->BlockOrTrap($blocks);
303 $userdata->UserOptionalFields($optionalfields);
306 $response->data($userdata);
311 # Implementation functions that might be useful to a subclass.
313 # Get a CStoreEditor:
317 # If we have an editor, check the validity of the auth session, then
318 # invalidate the editor if the session is not valid.
319 if ($self->{editor}) {
320 undef($self->{editor}) unless ($self->checkauth());
323 # If we don't have an editor, make a new one.
324 unless (defined($self->{editor})) {
325 $self->login() unless ($self->checkauth());
326 $self->{editor} = new_editor(authtoken=>$self->{session}->{authtoken});
329 return $self->{editor};
332 # Login via OpenSRF to Evergreen.
336 # Get the authentication seed.
337 my $seed = $U->simplereq(
339 'open-ils.auth.authenticate.init',
340 $self->{config}->{credentials}->{username}
345 my $response = $U->simplereq(
347 'open-ils.auth.authenticate.complete',
349 username => $self->{config}->{credentials}->{username},
351 $seed . md5_hex($self->{config}->{credentials}->{password})
354 workstation => $self->{config}->{credentials}->{workstation}
358 $self->{session}->{authtoken} = $response->{payload}->{authtoken};
359 $self->{session}->{authtime} = $response->{payload}->{authtime};
364 # Return 1 if we have a 'valid' authtoken, 0 if not.
368 # We implement our own version of this function, rather than rely
369 # on CStoreEditor, because we may want to check this at times that
370 # we don't have a CStoreEditor.
372 # We use AppUtils to do the heavy lifting.
373 if (defined($self->{session})) {
374 if ($U->check_user_session($self->{session}->{authtoken})) {
381 # If we reach here, we don't have a session, so we are definitely
386 # private subroutines not meant to be used directly by subclasses.
387 # Most have to do with setup and/or state checking of implementation
390 # Find, load, and parse our configuration file:
394 # Find the configuration file via variables:
395 my $file = OILS_NCIP_CONFIG_DEFAULT;
396 $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
398 $self->{config} = XMLin($file, NormaliseSpace => 2,
399 ForceArray => ['block_profile', 'stat_cat_entry']);
402 # Bootstrap OpenSRF::System, load the IDL, and initialize the
403 # CStoreEditor module.
407 my $bootstrap_config = $self->{config}->{bootstrap};
408 OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
410 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
411 Fieldmapper->import(IDL => $idl);
413 OpenILS::Utils::CStoreEditor->init;
416 # Login and then initialize some object data based on the
421 # Login to Evergreen.
425 my $e = $self->editor();
427 # Retrieve the work_ou as an object.
428 my $work_ou = $e->search_actor_org_unit(
429 {shortname => $self->{config}->{credentials}->{work_ou}}
431 $self->{work_ou} = $work_ou->[0] if ($work_ou && @$work_ou);
433 # Load the barred groups as pgt objects into a blocked_profiles
435 $self->{blocked_profiles} = [];
436 foreach (@{$self->{config}->{patrons}->{block_profile}}) {
438 my $pgt = $e->retrieve_permission_grp_tree($_->{grp});
439 push(@{$blocked_profiles}, $pgt) if ($pgt);
441 my $result = $e->search_permission_grp_tree({name => $_});
442 if ($result && @$result) {
443 map {push(@{$self->{blocked_profiles}}, $_)} @$result;
448 # Load the bib source if we're not using precats.
449 unless ($self->{config}->{items}->{use_precats}) {
450 # Retrieve the default
451 my $cbs = $e->retrieve_config_bib_source(BIB_SOURCE_DEFAULT);
452 my $data = $self->{config}->{items}->{bib_source};
454 $data = $data->[0] if (ref($data) eq 'ARRAY');
456 my $result = $e->retrieve_config_bib_source($data->{cbs});
457 $cbs = $result if ($result);
459 my $result = $e->search_config_bib_source({source => $data});
460 if ($result && @$result) {
461 $cbs = $result->[0]; # Use the first one.
465 $self->{bib_source} = $cbs;
468 # Load the required asset.stat_cat_entries:
469 $self->{stat_cat_entries} = [];
470 foreach (@{$self->{config}->{items}->{stat_cat_entry}}) {
471 # Must have the stat_cat attr and the name, so we must have a
474 # We want to limit the search to the work org and its
476 my $ancestors = $U->get_org_ancestors($self->{work_ou}->id());
477 my $result = $e->search_asset_stat_cat_entry(
479 stat_cat => $_->{stat_cat},
480 value => $_->{content},
484 if ($result && @$result) {
485 map {push(@{$self->{stat_cat_entries}}, $_)} @$result;
490 # Standalone, "helper" functions. These do not take an object or
493 # Check if a user is past their expiration date.
498 # Users might not expire. If so, they have no expire_date.
499 if ($user->expire_date()) {
500 my $expires = DateTime::Format::ISO8601->parse_datetime(
501 cleanse_ISO8601($user->expire_date())
503 my $now = DateTime->now()->epoch();
504 $expired = $now > $expires;