487a7630f8e1d1608b1c099dfc0b3d07964547ae
[working/NCIPServer.git] / lib / NCIP / ILS / Evergreen.pm
1 # ---------------------------------------------------------------
2 # Copyright © 2014 Jason Stephenson <jason@sigio.com>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 # ---------------------------------------------------------------
14 package NCIP::ILS::Evergreen;
15
16 use Modern::Perl;
17 use Object::Tiny qw/name/;
18 use XML::XPath;
19 use DateTime;
20 use DateTime::Format::ISO8601;
21 use Digest::MD5 qw/md5_hex/;
22 use OpenSRF::System;
23 use OpenSRF::Utils qw/:datetime/;
24 use OpenSRF::Utils::SettingsClient;
25 use OpenILS::Utils::Fieldmapper;
26 use OpenILS::Utils::CStoreEditor qw/:funcs/;
27 use OpenILS::Application::AppUtils;
28 use OpenILS::Const qw/:const/;
29 use MARC::Record;
30 use MARC::Field;
31 use MARC::File::XML;
32
33 # Default values we define for things that might be missing in our
34 # runtime environment or configuration file that absolutely must have
35 # values.
36 #
37 # OILS_NCIP_CONFIG_DEFAULT is the default location to find our
38 # driver's configuration file.  This location can be overridden by
39 # setting the path in the OILS_NCIP_CONFIG environment variable.
40 #
41 # BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
42 # "short" bibs.  It is used only if no entry is supplied in the
43 # configuration file.  The provided default is 2, the id of the
44 # "System Local" source that comes with a default Evergreen
45 # installation.
46 use constant {
47     OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
48     BIB_SOURCE_DEFAULT => 2
49 };
50
51 # A common Evergreen code shortcut to use AppUtils:
52 my $U = 'OpenILS::Application::AppUtils';
53
54 # The usual constructor:
55 sub new {
56     my $class = shift;
57     $class = ref $class or $class;
58
59     # Instantiate our Object::Tiny parent with the rest of the
60     # arguments.  It creates a blessed hashref.
61     my $self = $class->SUPER::new(@_);
62
63     # Look for our configuration file, load, and parse it:
64     $self->_configure();
65
66     # Bootstrap OpenSRF and prepare some OpenILS components.
67     $self->_bootstrap();
68
69     # Initialize the rest of our internal state.
70     $self->_init();
71
72     return $self;
73 }
74
75 # Subroutines required by the NCIPServer interface:
76 sub itemdata {}
77
78 sub userdata {
79     my $self = shift;
80     my $barcode = shift;
81
82     # Check our session and login if necessary.
83     $self->login() unless ($self->checkauth());
84
85     # Initialize the hashref we need to return to the caller.
86     my $userdata = {
87         borrowernumber => '',
88         cardnumber => '',
89         streetnumber => '',
90         address => '',
91         address2 => '',
92         city => '',
93         state => '',
94         zipcode => '',
95         country => '',
96         firstname => '',
97         surname => '',
98         blocked => ''
99     };
100
101     # Look up our patron by barcode:
102     my $user = $U->simplereq(
103         'open-ils.actor',
104         'open-ils.actor.user.fleshed.retrieve_by_barcode',
105         $barcode,
106         0
107     );
108
109     # Check for a failure, or a deleted, inactive, or expired user,
110     # and if so, return empty userdata.
111     if (!$user || $U->event_code($user) || $user->deleted() || !$user->active()
112             || _expired($user) || !$user->card()->active()) {
113         # We'll return the empty userdata hashref to indicate a patron
114         # was not found.
115         return ($userdata, 'Borrower not found');
116     }
117
118     # We also need to check if the barcode used to retrieve the patron
119     # is an active barcode.
120     if (!grep {$_->barcode() eq $barcode && $_->active()} @{$user->cards()}) {
121         return ($userdata, 'Borrower not found');
122     }
123
124     # We got the information, so lets fill in our userdata.
125     $userdata->{borrowernumber} = $user->id();
126     $userdata->{cardnumber} = $user->card()->barcode();
127     $userdata->{firstname} = $user->first_given_name();
128     $userdata->{surname} = $user->family_name();
129     # Use the first address in the array that is valid and not
130     # pending, since no one said whether or not to use billing or
131     # mailing address.
132     my @addrs = grep {$_->valid() && !$_->pending()} @{$user->addresses()};
133     if (@addrs) {
134         $userdata->{city} = $addrs[0]->city();
135         $userdata->{country} = $addrs[0]->country();
136         $userdata->{zipcode} = $addrs[0]->post_code();
137         $userdata->{state} = $addrs[0]->state();
138         $userdate->{address} = $addrs[0]->street1();
139         $userdata->{address2} = $addrs[0]->street2();
140     }
141
142     # Check for barred patron.
143     if ($user->barred()) {
144         $userdata->{blocked} = 1;
145     }
146
147     # Check if the patron's profile is blocked from ILL.
148     if (!$userdata->{blocked} &&
149             grep {$_->id() == $user->profile()} @{$self->{block_profiles}}) {
150         $userdata->{blocked} = 1;
151     }
152
153     # Check for penalties that block CIRC, HOLD, or RENEW.
154     unless ($userdata->{blocked}) {
155         foreach my $penalty (@{$user->standing_penalties()}) {
156             if ($penalty->standing_penalty->block_list()) {
157                 my @blocks = split /\|/,
158                     $penalty->standing_penalty->block_list();
159                 if (grep /(?:CIRC|HOLD|RENEW)/, @blocks) {
160                     $userdata->{blocked} = 1;
161                     last;
162                 }
163             }
164         }
165     }
166
167     return ($userdata, '');
168 }
169
170 sub checkin {}
171
172 sub checkout {}
173
174 sub renew {}
175
176 sub request {}
177
178 sub cancelrequest {}
179
180 sub acceptitem {}
181
182 # Implementation functions that might be useful to a subclass.
183
184 # Get a CStoreEditor:
185 sub editor {
186     my $self = shift;
187
188     # If we have an editor, check the validity of the auth session, then
189     # invalidate the editor if the session is not valid.
190     if ($self->{editor}) {
191         undef($self->{editor}) unless ($self->checkauth());
192     }
193
194     # If we don't have an editor, make a new one.
195     unless (defined($self->{editor})) {
196         $self->login() unless ($self->checkauth());
197         $self->{editor} = new_editor(authtoken=>$self->{session}->{authtoken});
198     }
199
200     return $self->{editor};
201 }
202
203 # Login via OpenSRF to Evergreen.
204 sub login {
205     my $self = shift;
206
207     # Get the authentication seed.
208     my $seed = $U->simplereq(
209         'open-ils.auth',
210         'open-ils.auth.authenticate.init',
211         $self->{config}->{username}
212     );
213
214     # Actually login.
215     if ($seed) {
216         my $response = $U->simplereq(
217             'open-ils.auth',
218             'open-ils.auth.authenticate.complete',
219             {
220                 username => $self->{config}->{username},
221                 password => md5_hex(
222                     $seed . md5_hex($self->{config}->{password})
223                 ),
224                 type => 'staff',
225                 workstation => $self->{config}->{workstation}
226             }
227         );
228         if ($response) {
229             $self->{session}->{authtoken} = $response->{payload}->{authtoken};
230             $self->{session}->{authtime} = $response->{payload}->{authtime};
231         }
232     }
233 }
234
235 # Return 1 if we have a 'valid' authtoken, 0 if not.
236 sub checkauth {
237     my $self = shift;
238
239     # We implement our own version of this function, rather than rely
240     # on CStoreEditor, because we may want to check this at times that
241     # we don't have a CStoreEditor.
242
243     # We use AppUtils to do the heavy lifting.
244     if (defined($self->{session})) {
245         if ($U->check_user_session($self->{session}->{authtoken})) {
246             return 1;
247         } else {
248             return 0;
249         }
250     }
251
252     # If we reach here, we don't have a session, so we are definitely
253     # not logged in.
254     return 0;
255 }
256
257 # private subroutines not meant to be used directly by subclasses.
258 # Most have to do with setup and/or state checking of implementation
259 # components.
260
261 # Find, load, and parse our configuration file:
262 sub _configure {
263     my $self = shift;
264
265     # Find the configuration file via variables:
266     my $file = OILS_NCIP_CONFIG_DEFAULT;
267     $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
268
269     # Load our configuration with XML::XPath.
270     my $xpath = XML::XPath->new(filename => $file);
271     # Load configuration into $self:
272     $self->{config}->{bootstrap} =
273         _strip($xpath->findvalue("/ncip/bootstrap")->value());
274     $self->{config}->{username} =
275         _strip($xpath->findvalue("/ncip/credentials/username")->value());
276     $self->{config}->{password} =
277         _strip($xpath->findvalue("/ncip/credentials/password")->value());
278     $self->{config}->{work_ou} =
279         _strip($xpath->findvalue("/ncip/credentials/work_ou")->value());
280     $self->{config}->{workstation} =
281         _strip($xpath->findvalue("/ncip/credentials/workstation")->value());
282     # Look for a list of patron profiles to treat as blocked.  This is
283     # useful if you have a patron group or groups that are not
284     # permitted to do ILL.
285     $self->{config}->{barred_groups} = [];
286     my $nodes = $xpath->findnodes('/ncip/patrons/block_profile');
287     if ($nodes) {
288         foreach my $node ($nodes->get_nodelist()) {
289             my $data = {id => 0, name => ""};
290             my $attr = $xpath->findvalue('@pgt', $node);
291             if ($attr) {
292                 $data->{id} = $attr;
293             }
294             $data->{name} = _strip($node->string_value());
295             push(@{$self->{config}->{barred_groups}}, $data)
296                 if ($data->{id} || $data->{name});
297         }
298     }
299     # Check for the use_precats setting for acceptitem.  This should
300     # only be set if you are using 2.7.0-alpha or later of Evergreen.
301     $self->{config}->{use_precats} = 0;
302     undef($nodes);
303     $nodes = $xpath->find('/ncip/items/use_precats');
304     $self->{config}->{use_precats} = 1 if ($nodes);
305     # If we're not using precats, we will be making "short" bibs.  We
306     # need to look up and see if a special bib source has been
307     # configured for these.
308     undef($nodes);
309     $nodes = $xpath->findnodes('/ncip/items/bib_source');
310     if ($nodes) {
311         my $node = $nodes->get_node(1);
312         my $attr = $xpath->findvalue('@cbs', $node);
313         if ($attr) {
314             $self->{config}->{cbs}->{id} = $attr;
315         }
316         $self->{config}->{cbs}->{name} = _strip($node->string_value());
317     }
318     # Look for any required asset.copy.stat_cat_entry entries.
319     $self->{config}->{asces} = [];
320     undef($nodes);
321     $nodes = $xpath->findnodes('/ncip/items/stat_cat_entry');
322     if ($nodes) {
323         foreach my $node ($nodes->get_nodelist()) {
324             my $data = {asc => 0, id => 0, name => ''};
325             my $asc = $xpath->findvalue('@asc', $node);
326             $data->{asc} = $asc if ($asc);
327             my $asce = $xpath->findvalue('@asce', $node);
328             $data->{id} = $asce if ($asce);
329             $data->{name} = _strip($node->string_value());
330             push(@{$self->{config}->{asces}}, $data)
331                 if ($data->{id} || ($data->{name} && $data->{asc}));
332         }
333     }
334 }
335
336 # Bootstrap OpenSRF::System, load the IDL, and initialize the
337 # CStoreEditor module.
338 sub _bootstrap {
339     my $self = shift;
340
341     my $bootstrap_config = $self->{config}->{bootstrap};
342     OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
343
344     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
345     Fieldmapper->import(IDL => $idl);
346
347     OpenILS::Utils::CStoreEditor->init;
348 }
349
350 # Login and then initialize some object data based on the
351 # configuration.
352 sub _init {
353     my $self = shift;
354
355     # Login to Evergreen.
356     $self->login();
357
358     # Create an editor.
359     my $e = $self->editor();
360
361     # Load the barred groups as pgt objects into a blocked_profiles
362     # list.
363     $self->{blocked_profiles} = [];
364     foreach (@{$self->{config}->{barred_groups}}) {
365         if ($_->{id}) {
366             my $pgt = $e->retrieve_permission_grp_tree($_->{id});
367             push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
368         } else {
369             my $result = $e->search_permission_grp_tree(
370                 {name => $_->{name}}
371             );
372             if ($result && @$result) {
373                 map {push(@{$self->{blocked_profiles}}, $_)} @$result;
374             }
375         }
376     }
377
378     # Load the bib source if we're not using precats.
379     unless ($self->{config}->{use_precats}) {
380         # Retrieve the default
381         my $cbs = $e->retrieve_config_bib_source(BIB_SOURCE_DEFAULT);
382         my $data = $self->{config}->{cbs};
383         if ($data) {
384             if ($data->{id}) {
385                 my $result = $e->retrieve_config_bib_source($data->{id});
386                 $cbs = $result if ($result);
387             } else {
388                 my $result = $e->search_config_bib_source(
389                     {source => $data->{name}}
390                 );
391                 if ($result && @$result) {
392                     $cbs = $result->[0]; # Use the first one.
393                 }
394             }
395         }
396         $self->{bib_source} = $cbs;
397     }
398
399     # Load the required asset.stat_cat_entries:
400     $self->{asces} = [];
401     foreach (@{$self->{config}->{asces}}) {
402         if ($_->{id}) {
403             my $asce = $e->retrieve_asset_stat_cat_entry($_->{id});
404             push(@{$self->{asces}}, $asce) if ($asce);
405         } elsif ($_->{asc} && $_->{name}) {
406             # We want to limit the search to the work org and its
407             # ancestors.
408             my $ancestors = $U->get_org_ancestors($self->{config}->{work_ou});
409             my $result = $e->search_asset_stat_cat_entry(
410                 {
411                     stat_cat => $_->{asc},
412                     value => $_->{name},
413                     owner => $ancestors
414                 }
415             );
416             if ($result && @$result) {
417                 map {push(@{$self->{asces}}, $_)} @$result;
418             }
419         }
420     }
421 }
422
423 # Standalone, "helper" functions.  These do not take an object or
424 # class reference.
425
426 # Strip leading and trailing whitespace (incl. newlines) from a string
427 # value.
428 sub _strip {
429     my $string = shift;
430     if ($string) {
431         $string =~ s/^\s+//;
432         $string =~ s/\s+$//;
433     }
434     return $string;
435 }
436
437 # Check if a user is past their expiration date.
438 sub _expired {
439     my $user = shift;
440     my $expired = 0;
441
442     # Users might not expire.  If so, they have no expire_date.
443     if ($user->expire_date()) {
444         my $expires = DateTime::Format::ISO8601->parse_datetime(
445             cleanse_ISO8601($user->expire_date())
446         )->epoch();
447         my $now = DateTime->now()->epoch();
448         $expired = $now > $expires;
449     }
450
451     return $expired;
452 }
453
454 1;