Bug 3813: SIP2, Bad Patron Information Response to Message 64
[koha.git] / C4 / SIP / Sip / MsgType.pm
1 #
2 # Sip::MsgType.pm
3 #
4 # A Class for handing SIP messages
5 #
6
7 package Sip::MsgType;
8
9 use strict;
10 use warnings;
11 use Exporter;
12 use Sys::Syslog qw(syslog);
13
14 use Sip qw(:all);
15 use Sip::Constants qw(:all);
16 use Sip::Checksum qw(verify_cksum);
17
18 use Data::Dumper;
19 use CGI;
20 use C4::Auth qw(&check_api_auth);
21
22 use UNIVERSAL qw(can);  # make sure this is *after* C4 modules.
23
24 use vars qw(@ISA $VERSION @EXPORT_OK);
25
26 BEGIN {
27         $VERSION = 1.01;
28         @ISA = qw(Exporter);
29         @EXPORT_OK = qw(handle);
30 }
31
32 # Predeclare handler subroutines
33 use subs qw(handle_patron_status handle_checkout handle_checkin
34             handle_block_patron handle_sc_status handle_request_acs_resend
35             handle_login handle_patron_info handle_end_patron_session
36             handle_fee_paid handle_item_information handle_item_status_update
37             handle_patron_enable handle_hold handle_renew handle_renew_all);
38
39 #
40 # For the most part, Version 2.00 of the protocol just adds new
41 # variable fields, but sometimes it changes the fixed header.
42 #
43 # In general, if there's no '2.00' protocol entry for a handler, that's
44 # because 2.00 didn't extend the 1.00 version of the protocol.  This will
45 # be handled by the module initialization code following the declaration,
46 # which goes through the handlers table and creates a '2.00' entry that
47 # points to the same place as the '1.00' entry.  If there's a 2.00 entry
48 # but no 1.00 entry, then that means that it's a completely new service
49 # in 2.00, so 1.00 shouldn't recognize it.
50
51 my %handlers = (
52                 (PATRON_STATUS_REQ) => {
53                     name => "Patron Status Request",
54                     handler => \&handle_patron_status,
55                     protocol => {
56                         1 => {
57                             template => "A3A18",
58                             template_len => 21,
59                             fields => [(FID_INST_ID), (FID_PATRON_ID),
60                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
61                         }
62                     }
63                 },
64                 (CHECKOUT) => {
65                     name => "Checkout",
66                     handler => \&handle_checkout,
67                     protocol => {
68                         1 => {
69                             template => "CCA18A18",
70                             template_len => 38,
71                             fields => [(FID_INST_ID), (FID_PATRON_ID),
72                                        (FID_ITEM_ID), (FID_TERMINAL_PWD)],
73                         },
74                         2 => {
75                             template => "CCA18A18",
76                             template_len => 38,
77                             fields => [(FID_INST_ID), (FID_PATRON_ID),
78                                        (FID_ITEM_ID), (FID_TERMINAL_PWD),
79                                        (FID_ITEM_PROPS), (FID_PATRON_PWD),
80                                        (FID_FEE_ACK), (FID_CANCEL)],
81                         },
82                     }
83                 },
84                 (CHECKIN) => {
85                     name => "Checkin",
86                     handler => \&handle_checkin,
87                     protocol => {
88                         1 => {
89                             template => "CA18A18",
90                             template_len => 37,
91                             fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
92                                        (FID_ITEM_ID), (FID_TERMINAL_PWD)],
93                         },
94                         2 => {
95                             template => "CA18A18",
96                             template_len => 37,
97                             fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
98                                        (FID_ITEM_ID), (FID_TERMINAL_PWD),
99                                        (FID_ITEM_PROPS), (FID_CANCEL)],
100                         }
101                     }
102                 },
103                 (BLOCK_PATRON) => {
104                     name => "Block Patron",
105                     handler => \&handle_block_patron,
106                     protocol => {
107                         1 => {
108                             template => "CA18",
109                             template_len => 19,
110                             fields => [(FID_INST_ID), (FID_BLOCKED_CARD_MSG),
111                                        (FID_PATRON_ID), (FID_TERMINAL_PWD)],
112                         },
113                     }
114                 },
115                 (SC_STATUS) => {
116                     name => "SC Status",
117                     handler => \&handle_sc_status,
118                     protocol => {
119                         1 => {
120                             template =>"CA3A4",
121                             template_len => 8,
122                             fields => [],
123                         }
124                     }
125                 },
126                 (REQUEST_ACS_RESEND) => {
127                     name => "Request ACS Resend",
128                     handler => \&handle_request_acs_resend,
129                     protocol => {
130                         1 => {
131                             template => "",
132                             template_len => 0,
133                             fields => [],
134                         }
135                     }
136                 },
137                 (LOGIN) => {
138                     name => "Login",
139                     handler => \&handle_login,
140                     protocol => {
141                         2 => {
142                             template => "A1A1",
143                             template_len => 2,
144                             fields => [(FID_LOGIN_UID), (FID_LOGIN_PWD),
145                                        (FID_LOCATION_CODE)],
146                         }
147                     }
148                 },
149                 (PATRON_INFO) => {
150                     name => "Patron Info",
151                     handler => \&handle_patron_info,
152                     protocol => {
153                         2 => {
154                             template => "A3A18A10",
155                             template_len => 31,
156                             fields => [(FID_INST_ID), (FID_PATRON_ID),
157                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD),
158                                        (FID_START_ITEM), (FID_END_ITEM)],
159                         }
160                     }
161                 },
162                 (END_PATRON_SESSION) => {
163                     name => "End Patron Session",
164                     handler => \&handle_end_patron_session,
165                     protocol => {
166                         2 => {
167                             template => "A18",
168                             template_len => 18,
169                             fields => [(FID_INST_ID), (FID_PATRON_ID),
170                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
171                         }
172                     }
173                 },
174                 (FEE_PAID) => {
175                     name => "Fee Paid",
176                     handler => \&handle_fee_paid,
177                     protocol => {
178                         2 => {
179                             template => "A18A2A3",
180                             template_len => 0,
181                             fields => [(FID_FEE_AMT), (FID_INST_ID),
182                                        (FID_PATRON_ID), (FID_TERMINAL_PWD),
183                                        (FID_PATRON_PWD), (FID_FEE_ID),
184                                        (FID_TRANSACTION_ID)],
185                         }
186                     }
187                 },
188                 (ITEM_INFORMATION) => {
189                     name => "Item Information",
190                     handler => \&handle_item_information,
191                     protocol => {
192                         2 => {
193                             template => "A18",
194                             template_len => 18,
195                             fields => [(FID_INST_ID), (FID_ITEM_ID),
196                                        (FID_TERMINAL_PWD)],
197                         }
198                     }
199                 },
200                 (ITEM_STATUS_UPDATE) => {
201                     name => "Item Status Update",
202                     handler => \&handle_item_status_update,
203                     protocol => {
204                         2 => {
205                             template => "A18",
206                             template_len => 18,
207                             fields => [(FID_INST_ID), (FID_PATRON_ID),
208                                        (FID_ITEM_ID), (FID_TERMINAL_PWD),
209                                        (FID_ITEM_PROPS)],
210                         }
211                     }
212                 },
213                 (PATRON_ENABLE) => {
214                     name => "Patron Enable",
215                     handler => \&handle_patron_enable,
216                     protocol => {
217                         2 => {
218                             template => "A18",
219                             template_len => 18,
220                             fields => [(FID_INST_ID), (FID_PATRON_ID),
221                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
222                         }
223                     }
224                 },
225                 (HOLD) => {
226                     name => "Hold",
227                     handler => \&handle_hold,
228                     protocol => {
229                         2 => {
230                             template => "AA18",
231                             template_len => 19,
232                             fields => [(FID_EXPIRATION), (FID_PICKUP_LOCN),
233                                        (FID_HOLD_TYPE), (FID_INST_ID),
234                                        (FID_PATRON_ID), (FID_PATRON_PWD),
235                                        (FID_ITEM_ID), (FID_TITLE_ID),
236                                        (FID_TERMINAL_PWD), (FID_FEE_ACK)],
237                         }
238                     }
239                 },
240                 (RENEW) => {
241                     name => "Renew",
242                     handler => \&handle_renew,
243                     protocol => {
244                         2 => {
245                             template => "CCA18A18",
246                             template_len => 38,
247                             fields => [(FID_INST_ID), (FID_PATRON_ID),
248                                        (FID_PATRON_PWD), (FID_ITEM_ID),
249                                        (FID_TITLE_ID), (FID_TERMINAL_PWD),
250                                        (FID_ITEM_PROPS), (FID_FEE_ACK)],
251                         }
252                     }
253                 },
254                 (RENEW_ALL) => {
255                     name => "Renew All",
256                     handler => \&handle_renew_all,
257                     protocol => {
258                         2 => {
259                             template => "A18",
260                             template_len => 18,
261                             fields => [(FID_INST_ID), (FID_PATRON_ID),
262                                        (FID_PATRON_PWD), (FID_TERMINAL_PWD),
263                                        (FID_FEE_ACK)],
264                         }
265                     }
266                 }
267                 );
268
269 #
270 # Now, initialize some of the missing bits of %handlers
271 #
272 foreach my $i (keys(%handlers)) {
273     if (!exists($handlers{$i}->{protocol}->{2})) {
274         $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
275     }
276 }
277
278 sub new {
279     my ($class, $msg, $seqno) = @_;
280     my $self = {};
281     my $msgtag = substr($msg, 0, 2);
282
283     if ($msgtag eq LOGIN) {
284         # If the client is using the 2.00-style "Login" message
285         # to authenticate to the server, then we get the Login message
286         # _before_ the client has indicated that it supports 2.00, but
287         # it's using the 2.00 login process, so it must support 2.00.
288                 $protocol_version = 2;
289     }
290     syslog("LOG_DEBUG", "Sip::MsgType::new('%s', '%s...', '%s'): seq.no '%s', protocol %s",
291                 $class, substr($msg, 0, 10), $msgtag, $seqno, $protocol_version);
292         # warn "SIP PROTOCOL: $protocol_version";       
293     if (!exists($handlers{$msgtag})) {
294                 syslog("LOG_WARNING", "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'",
295                $msgtag, $msg);
296                 return(undef);
297     } elsif (!exists($handlers{$msgtag}->{protocol}->{$protocol_version})) {
298                 syslog("LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'",
299                $msgtag, $protocol_version);
300                 return(undef);
301     }
302
303     bless $self, $class;
304
305     $self->{seqno} = $seqno;
306     $self->_initialize(substr($msg,2), $handlers{$msgtag});
307
308     return($self);
309 }
310
311 sub _initialize {
312         my ($self, $msg, $control_block) = @_;
313         my ($fs, $fn, $fe);
314         my $proto = $control_block->{protocol}->{$protocol_version};
315
316         $self->{name}    = $control_block->{name};
317         $self->{handler} = $control_block->{handler};
318
319         $self->{fields}       = {};
320         $self->{fixed_fields} = [];
321
322         chomp($msg);            # These four are probably unnecessary now.
323         $msg =~ tr/\cM//d;
324         $msg =~ s/\^M$//;
325         chomp($msg);
326
327         foreach my $field (@{$proto->{fields}}) {
328                 $self->{fields}->{$field} = undef;
329         }
330
331     syslog("LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...)",
332                 $self->{name}, $msg, $proto->{template}, $proto->{template_len});
333
334     $self->{fixed_fields} = [ unpack($proto->{template}, $msg) ];   # see http://perldoc.perl.org/5.8.8/functions/unpack.html
335
336     # Skip over the fixed fields and the split the rest of
337     # the message into fields based on the delimiter and parse them
338     foreach my $field (split(quotemeta($field_delimiter), substr($msg, $proto->{template_len}))) {
339                 $fn = substr($field, 0, 2);
340
341         if (!exists($self->{fields}->{$fn})) {
342                 syslog("LOG_WARNING", "Unsupported field '%s' in %s message '%s'",
343                         $fn, $self->{name}, $msg);
344         } elsif (defined($self->{fields}->{$fn})) {
345                 syslog("LOG_WARNING", "Duplicate field '%s' (previous value '%s') in %s message '%s'",
346                         $fn, $self->{fields}->{$fn}, $self->{name}, $msg);
347         } else {
348                 $self->{fields}->{$fn} = substr($field, 2);
349         }
350         }
351
352         return($self);
353 }
354
355 sub handle {
356     my ($msg, $server, $req) = @_;
357     my $config = $server->{config};
358     my $self;
359
360     #
361     # What's the field delimiter for variable length fields?
362     # This can't be based on the account, since we need to know
363     # the field delimiter to parse a SIP login message
364     #
365         if (defined($server->{config}->{delimiter})) {
366                 $field_delimiter = $server->{config}->{delimiter};
367         }
368
369     # error detection is active if this is a REQUEST_ACS_RESEND
370     # message with a checksum, or if the message is long enough
371     # and the last nine characters begin with a sequence number
372     # field
373     if ($msg eq REQUEST_ACS_RESEND_CKSUM) {
374                 # Special case
375                 $error_detection = 1;
376                 $self = new Sip::MsgType ((REQUEST_ACS_RESEND), 0);
377     } elsif((length($msg) > 11) && (substr($msg, -9, 2) eq "AY")) {
378                 $error_detection = 1;
379
380         if (!verify_cksum($msg)) {
381             syslog("LOG_WARNING", "Checksum failed on message '%s'", $msg);
382             # REQUEST_SC_RESEND with error detection
383             $last_response = REQUEST_SC_RESEND_CKSUM;
384             print("$last_response\r");
385             return REQUEST_ACS_RESEND;
386         } else {
387             # Save the sequence number, then strip off the
388             # error detection data to process the message
389             $self = new Sip::MsgType (substr($msg, 0, -9), substr($msg, -7, 1));
390         }
391     } elsif ($error_detection) {
392         # We received a non-ED message when ED is supposed to be active.
393         # Warn about this problem, then process the message anyway.
394                 syslog("LOG_WARNING",
395                "Received message without error detection: '%s'", $msg);
396                 $error_detection = 0;
397                 $self = new Sip::MsgType ($msg, 0);
398     } else {
399                 $self = new Sip::MsgType ($msg, 0);
400     }
401
402         if ((substr($msg, 0, 2) ne REQUEST_ACS_RESEND) &&
403                 $req && (substr($msg, 0, 2) ne $req)) {
404                 return substr($msg, 0, 2);
405         }
406         unless ($self->{handler}) {
407                 syslog("LOG_WARNING", "No handler defined for '%s'", $msg);
408                 return undef;
409         }
410     return($self->{handler}->($self, $server));  # FIXME
411         # FIXME: Use of uninitialized value in subroutine entry
412         # Can't use string ("") as a subroutine ref while "strict refs" in use
413 }
414
415 ##
416 ## Message Handlers
417 ##
418
419 #
420 # Patron status messages are produced in response to both
421 # "Request Patron Status" and "Block Patron"
422 #
423 # Request Patron Status requires a patron password, but
424 # Block Patron doesn't (since the patron may never have
425 # provided one before attempting some illegal action).
426
427 # ASSUMPTION: If the patron password field is present in the
428 # message, then it must match, otherwise incomplete patron status
429 # information will be returned to the terminal.
430
431 sub build_patron_status {
432     my ($patron, $lang, $fields)= @_;
433     my $patron_pwd = $fields->{(FID_PATRON_PWD)};
434     my $resp = (PATRON_STATUS_RESP);
435
436     if ($patron) {
437         $resp .= patron_status_string($patron);
438         $resp .= $lang . Sip::timestamp();
439         $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
440
441         # while the patron ID we got from the SC is valid, let's
442         # use the one returned from the ILS, just in case...
443         $resp .= add_field(FID_PATRON_ID, $patron->id);
444         if ($protocol_version >= 2) {
445             $resp .= add_field(FID_VALID_PATRON, 'Y');
446             # Patron password is a required field.
447                 $resp .= add_field(FID_VALID_PATRON_PWD, sipbool($patron->check_password($patron_pwd)));
448             $resp .= maybe_add(FID_CURRENCY, $patron->currency);
449             $resp .= maybe_add(FID_FEE_AMT, $patron->fee_amount);
450         }
451
452         $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
453         $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
454     } else {
455         # Invalid patron id.  Report that the user has no privs.,
456         # no personal name, and is invalid (if we're using 2.00)
457         $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
458         $resp .= add_field(FID_PERSONAL_NAME, '');
459
460         # the patron ID is invalid, but it's a required field, so
461         # just echo it back
462         $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
463
464         ($protocol_version >= 2) and 
465                 $resp .= add_field(FID_VALID_PATRON, 'N');
466     }
467
468     $resp .= add_field(FID_INST_ID, $fields->{(FID_INST_ID)});
469     return $resp;
470 }
471
472 sub handle_patron_status {
473         my ($self, $server) = @_;
474         warn "handle_patron_status server: " . Dumper(\$server);  
475         my $ils = $server->{ils};
476         my $patron;
477         my $resp = (PATRON_STATUS_RESP);
478         my $account = $server->{account};
479     my ($lang, $date) = @{$self->{fixed_fields}};
480     my $fields = $self->{fields};
481         #warn Dumper($fields);
482         #warn FID_INST_ID;
483         #warn $fields->{(FID_INST_ID)};
484     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_patron_status");
485     $patron = $ils->find_patron($fields->{(FID_PATRON_ID)});
486     $resp = build_patron_status($patron, $lang, $fields);
487     $self->write_msg($resp);
488     return (PATRON_STATUS_REQ);
489 }
490
491 sub handle_checkout {
492     my ($self, $server) = @_;
493     my $account = $server->{account};
494     my $ils = $server->{ils};
495     my $inst = $ils->institution;
496     my ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date);
497     my $fields;
498     my ($patron_id, $item_id, $status);
499     my ($item, $patron);
500     my $resp;
501
502     ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date) =
503         @{$self->{fixed_fields}};
504     $fields = $self->{fields};
505
506     $patron_id = $fields->{(FID_PATRON_ID)};
507     $item_id   = $fields->{(FID_ITEM_ID)};
508
509
510     if ($no_block eq 'Y') {
511         # Off-line transactions need to be recorded, but there's
512         # not a lot we can do about it
513         syslog("LOG_WARNING", "received no-block checkout from terminal '%s'",
514                $account->{id});
515
516         $status = $ils->checkout_no_block($patron_id, $item_id,
517                                           $sc_renewal_policy,
518                                           $trans_date, $nb_due_date);
519     } else {
520         # Does the transaction date really matter for items that are
521         # checkout out while the terminal is online?  I'm guessing 'no'
522                 $status = $ils->checkout($patron_id, $item_id, $sc_renewal_policy);
523     }
524
525     $item = $status->item;
526     $patron = $status->patron;
527
528     if ($status->ok) {
529         # Item successfully checked out
530         # Fixed fields
531         $resp = CHECKOUT_RESP . '1';
532         $resp .= sipbool($status->renew_ok);
533         if ($ils->supports('magnetic media')) {
534             $resp .= sipbool($item->magnetic);
535         } else {
536             $resp .= 'U';
537         }
538         # We never return the obsolete 'U' value for 'desensitize'
539         $resp .= sipbool($status->desensitize);
540         $resp .= Sip::timestamp;
541
542         # Now for the variable fields
543         $resp .= add_field(FID_INST_ID, $inst);
544         $resp .= add_field(FID_PATRON_ID, $patron_id);
545         $resp .= add_field(FID_ITEM_ID, $item_id);
546         $resp .= add_field(FID_TITLE_ID, $item->title_id);
547         $resp .= add_field(FID_DUE_DATE, $item->due_date);
548
549         $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
550         $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
551
552         if ($protocol_version >= 2) {
553             if ($ils->supports('security inhibit')) {
554                 $resp .= add_field(FID_SECURITY_INHIBIT,
555                                    $status->security_inhibit);
556             }
557             $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
558             $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
559
560             # Financials
561             if ($status->fee_amount) {
562                 $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
563                 $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
564                 $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
565                 $resp .= maybe_add(FID_TRANSACTION_ID,
566                                    $status->transaction_id);
567             }
568         }
569
570     } else {
571         # Checkout failed
572         # Checkout Response: not ok, no renewal, don't know mag. media,
573         # no desensitize
574         $resp = sprintf("120NUN%s", Sip::timestamp);
575         $resp .= add_field(FID_INST_ID, $inst);
576         $resp .= add_field(FID_PATRON_ID, $patron_id);
577         $resp .= add_field(FID_ITEM_ID, $item_id);
578
579         # If the item is valid, provide the title, otherwise
580         # leave it blank
581         $resp .= add_field(FID_TITLE_ID, $item ? $item->title_id : '');
582         # Due date is required.  Since it didn't get checked out,
583         # it's not due, so leave the date blank
584         $resp .= add_field(FID_DUE_DATE, '');
585
586         $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
587         $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
588
589         if ($protocol_version >= 2) {
590             # Is the patron ID valid?
591             $resp .= add_field(FID_VALID_PATRON, sipbool($patron));
592
593             if ($patron && exists($fields->{FID_PATRON_PWD})) {
594                 # Password provided, so we can tell if it was valid or not
595                 $resp .= add_field(FID_VALID_PATRON_PWD,
596                                    sipbool($patron->check_password($fields->{(FID_PATRON_PWD)})));
597             }
598         }
599     }
600
601     $self->write_msg($resp);
602     return(CHECKOUT);
603 }
604
605 sub handle_checkin {
606     my ($self, $server) = @_;
607     my $account = $server->{account};
608     my $ils     = $server->{ils};
609     my ($current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel);
610     my ($patron, $item, $status);
611     my $resp = CHECKIN_RESP;
612     my ($no_block, $trans_date, $return_date) = @{$self->{fixed_fields}};
613         my $fields = $self->{fields};
614
615         $current_loc = $fields->{(FID_CURRENT_LOCN)};
616         $inst_id     = $fields->{(FID_INST_ID)};
617         $item_id     = $fields->{(FID_ITEM_ID)};
618         $item_props  = $fields->{(FID_ITEM_PROPS)};
619         $cancel      = $fields->{(FID_CANCEL)};
620
621     $ils->check_inst_id($inst_id, "handle_checkin");
622
623     if ($no_block eq 'Y') {
624         # Off-line transactions, ick.
625         syslog("LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id});
626         $status = $ils->checkin_no_block($item_id, $trans_date, $return_date, $item_props, $cancel);
627     } else {
628         $status = $ils->checkin($item_id, $trans_date, $return_date, $current_loc, $item_props, $cancel);
629     }
630
631     $patron = $status->patron;
632     $item   = $status->item;
633
634     $resp .= $status->ok ? '1' : '0';
635     $resp .= $status->resensitize ? 'Y' : 'N';
636     if ($item && $ils->supports('magnetic media')) {
637                 $resp .= sipbool($item->magnetic);
638     } else {
639         # The item barcode was invalid or the system doesn't support
640         # the 'magnetic media' indicator
641                 $resp .= 'U';
642     }
643     $resp .= $status->alert ? 'Y' : 'N';
644     $resp .= Sip::timestamp;
645     $resp .= add_field(FID_INST_ID, $inst_id);
646     $resp .= add_field(FID_ITEM_ID, $item_id);
647
648     if ($item) {
649         $resp .= add_field(FID_PERM_LOCN, $item->permanent_location);
650         $resp .= maybe_add(FID_TITLE_ID,  $item->title_id);
651     }
652
653     if ($protocol_version >= 2) {
654         $resp .= maybe_add(FID_SORT_BIN, $status->sort_bin);
655         if ($patron) {
656             $resp .= add_field(FID_PATRON_ID, $patron->id);
657         }
658         if ($item) {
659             $resp .= maybe_add(FID_MEDIA_TYPE,        $item->sip_media_type     );
660             $resp .= maybe_add(FID_ITEM_PROPS,        $item->sip_item_properties);
661             # $resp .= maybe_add(FID_COLLECTION_CODE, $item->collection_code    );
662             # $resp .= maybe_add(FID_CALL_NUMBER,     $item->call_number        );
663             # $resp .= maybe_add(FID_DESTINATION,     $item->destination_loc    );
664             # $resp .= maybe_add(FID_ALERT_TYPE,      $item->alert_type         );
665             # $resp .= maybe_add(FID_PATRON_ID,       $item->hold_patron_id     );
666             # $resp .= maybe_add(FID_PATRON_NAME,     $item->hold_patron_name   );
667         }
668     }
669
670     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
671     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
672
673     $self->write_msg($resp);
674
675     return(CHECKIN);
676 }
677
678 sub handle_block_patron {
679     my ($self, $server) = @_;
680     my $account = $server->{account};
681     my $ils = $server->{ils};
682     my ($card_retained, $trans_date);
683     my ($inst_id, $blocked_card_msg, $patron_id, $terminal_pwd);
684     my ($fields,$resp,$patron);
685
686     ($card_retained, $trans_date) = @{$self->{fixed_fields}};
687     $fields = $self->{fields};
688     $inst_id          = $fields->{(FID_INST_ID)};
689     $blocked_card_msg = $fields->{(FID_BLOCKED_CARD_MSG)};
690     $patron_id        = $fields->{(FID_PATRON_ID)};
691     $terminal_pwd     = $fields->{(FID_TERMINAL_PWD)};
692
693     # Terminal passwords are different from account login
694     # passwords, but I have no idea what to do with them.  So,
695     # I'll just ignore them for now.
696         
697         # FIXME ???
698
699     $ils->check_inst_id($inst_id, "block_patron");
700     $patron = $ils->find_patron($patron_id);
701
702     # The correct response for a "Block Patron" message is a
703     # "Patron Status Response", so use that handler to generate
704     # the message, but then return the correct code from here.
705     #
706     # Normally, the language is provided by the "Patron Status"
707     # fixed field, but since we're not responding to one of those
708     # we'll just say, "Unspecified", as per the spec.  Let the
709     # terminal default to something that, one hopes, will be
710     # intelligible
711         if ($patron) {
712                 # Valid patron id
713                 $patron->block($card_retained, $blocked_card_msg);
714         }
715
716     $resp = build_patron_status($patron, $patron->language, $fields);
717     $self->write_msg($resp);
718     return(BLOCK_PATRON);
719 }
720
721 sub handle_sc_status {
722     my ($self, $server) = @_;
723         ($server) or warn "handle_sc_status error: no \$server argument received.";
724         my ($status, $print_width, $sc_protocol_version) = @{$self->{fixed_fields}};
725         my ($new_proto);
726
727         if ($sc_protocol_version =~ /^1\./) {
728                 $new_proto = 1;
729         } elsif ($sc_protocol_version =~ /^2\./) {
730                 $new_proto = 2;
731         } else {
732                 syslog("LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version);
733                 $new_proto = 1;
734         }
735
736         if ($new_proto != $protocol_version) {
737                 syslog("LOG_INFO", "Setting protocol level to $new_proto");
738                 $protocol_version = $new_proto;
739         }
740
741     if ($status == SC_STATUS_PAPER) {
742         syslog("LOG_WARNING", "Self-Check unit '%s@%s' out of paper",
743                $self->{account}->{id}, $self->{account}->{institution});
744     } elsif ($status == SC_STATUS_SHUTDOWN) {
745         syslog("LOG_WARNING", "Self-Check unit '%s@%s' shutting down",
746                $self->{account}->{id}, $self->{account}->{institution});
747     }
748
749     $self->{account}->{print_width} = $print_width;
750     return (send_acs_status($self, $server) ? SC_STATUS : '');
751 }
752
753 sub handle_request_acs_resend {
754     my ($self, $server) = @_;
755
756     if (!$last_response) {
757         # We haven't sent anything yet, so respond with a
758         # REQUEST_SC_RESEND msg (p. 16)
759         $self->write_msg(REQUEST_SC_RESEND);
760     } elsif ((length($last_response) < 9)
761              || substr($last_response, -9, 2) ne 'AY') {
762         # When resending a message, we aren't supposed to include
763         # a sequence number, even if the original had one (p. 4).
764         # If the last message didn't have a sequence number, then
765         # we can just send it.
766         print("$last_response\r");      # not write_msg?
767     } else {
768         # Cut out the sequence number and checksum, since the old
769         # checksum is wrong for the resent message.
770         my $rebuilt = substr($last_response, 0, -9);
771         $self->write_msg($rebuilt);
772     }
773
774     return REQUEST_ACS_RESEND;
775 }
776
777 sub login_core ($$$) {
778         my $server = shift or return undef;
779         my $uid = shift;
780         my $pwd = shift;
781     my $status = 1;             # Assume it all works
782     if (!exists($server->{config}->{accounts}->{$uid})) {
783                 syslog("LOG_WARNING", "MsgType::login_core: Unknown login '$uid'");
784                 $status = 0;
785     } elsif ($server->{config}->{accounts}->{$uid}->{password} ne $pwd) {
786                 syslog("LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'");
787                 $status = 0;
788     } else {
789         # Store the active account someplace handy for everybody else to find.
790                 $server->{account} = $server->{config}->{accounts}->{$uid};
791                 my $inst = $server->{account}->{institution};
792                 $server->{institution} = $server->{config}->{institutions}->{$inst};
793                 $server->{policy} = $server->{institution}->{policy};
794                 $server->{sip_username} = $uid;
795                 $server->{sip_password} = $pwd;
796
797                 my $auth_status = api_auth($uid,$pwd);
798                 if (!$auth_status or $auth_status !~ /^ok$/i) {
799                         syslog("LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s",
800                                                 $uid, $inst, ($auth_status||'unknown'));
801                         $status = 0;
802                 } else {
803                         syslog("LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst);
804                         #
805                         # initialize connection to ILS
806                         #
807                         my $module = $server->{config}->{institutions}->{$inst}->{implementation};
808                         syslog("LOG_DEBUG", 'login_core: ' . Dumper($module));
809                         $module->use;
810                         if ($@) {
811                                 syslog("LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed",
812                                                 $server->{service}, $module, $inst);
813                                 die("Failed to load ILS implementation '$module' for $inst");
814                         }
815
816                         # like   ILS->new(), I think.
817                         $server->{ils} = $module->new($server->{institution}, $server->{account});
818                         if (!$server->{ils}) {
819                             syslog("LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst);
820                             die("Unable to connect to ILS '$inst'");
821                         }
822                 }
823         }
824         return $status;
825 }
826
827 sub handle_login {
828     my ($self, $server) = @_;
829     my ($uid_algorithm, $pwd_algorithm);
830     my ($uid, $pwd);
831     my $inst;
832     my $fields;
833     my $status = 1;             # Assume it all works
834
835     $fields = $self->{fields};
836     ($uid_algorithm, $pwd_algorithm) = @{$self->{fixed_fields}};
837
838     $uid = $fields->{(FID_LOGIN_UID)}; # Terminal ID, not patron ID.
839     $pwd = $fields->{(FID_LOGIN_PWD)}; # Terminal PWD, not patron PWD.
840
841     if ($uid_algorithm || $pwd_algorithm) {
842                 syslog("LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm");
843                 $status = 0;
844     }
845         else { $status = login_core($server,$uid,$pwd); }
846
847         $self->write_msg(LOGIN_RESP . $status);
848     return $status ? LOGIN : '';
849 }
850
851 #
852 # Build the detailed summary information for the Patron
853 # Information Response message based on the first 'Y' that appears
854 # in the 'summary' field of the Patron Information reqest.  The
855 # specification says that only one 'Y' can appear in that field,
856 # and we're going to believe it.
857 #
858 sub summary_info {
859     my ($ils, $patron, $summary, $start, $end) = @_;
860     my $resp = '';
861     my $summary_type;
862     #
863     # Map from offsets in the "summary" field of the Patron Information
864     # message to the corresponding field and handler
865     #
866     my @summary_map = (
867         { func => $patron->can(   "hold_items"), fid => FID_HOLD_ITEMS             },
868         { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS          },
869         { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS          },
870         { func => $patron->can(   "fine_items"), fid => FID_FINE_ITEMS             },
871         { func => $patron->can( "recall_items"), fid => FID_RECALL_ITEMS           },
872         { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
873     );
874
875     if (($summary_type = index($summary, 'Y')) == -1) {
876         return '';  # No detailed information required
877     }
878
879     syslog("LOG_DEBUG", "Summary_info: index == '%d', field '%s'",
880         $summary_type, $summary_map[$summary_type]->{fid});
881
882     my $func = $summary_map[$summary_type]->{func};
883     my $fid  = $summary_map[$summary_type]->{fid};
884     my $itemlist = &$func($patron, $start, $end);
885
886     syslog("LOG_DEBUG", "summary_info: list = (%s)", join(", ", @{$itemlist}));
887     foreach my $i (@{$itemlist}) {
888         $resp .= add_field($fid, $i->{barcode});
889     }
890
891     return $resp;
892 }
893
894 sub handle_patron_info {
895     my ($self, $server) = @_;
896     my $ils = $server->{ils};
897     my ($lang, $trans_date, $summary) = @{$self->{fixed_fields}};
898     my $fields = $self->{fields};
899     my ($inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end);
900     my ($resp, $patron, $count);
901
902     $inst_id      = $fields->{(FID_INST_ID)};
903     $patron_id    = $fields->{(FID_PATRON_ID)};
904     $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
905     $patron_pwd   = $fields->{(FID_PATRON_PWD)};
906     $start        = $fields->{(FID_START_ITEM)};
907     $end          = $fields->{(FID_END_ITEM)};
908
909     $patron = $ils->find_patron($patron_id);
910
911     $resp = (PATRON_INFO_RESP);
912     if ($patron) {
913         $resp .= patron_status_string($patron);
914         $resp .= (defined($lang) and length($lang) ==3) ? $lang : $patron->language;
915         $resp .= Sip::timestamp();
916
917         $resp .= add_count('patron_info/hold_items',
918                            scalar @{$patron->hold_items});
919         $resp .= add_count('patron_info/overdue_items',
920                            scalar @{$patron->overdue_items});
921         $resp .= add_count('patron_info/charged_items',
922                            scalar @{$patron->charged_items});
923         $resp .= add_count('patron_info/fine_items',
924                            scalar @{$patron->fine_items});
925         $resp .= add_count('patron_info/recall_items',
926                            scalar @{$patron->recall_items});
927         $resp .= add_count('patron_info/unavail_holds',
928                            scalar @{$patron->unavail_holds});
929
930     # FID_INST_ID added last (order irrelevant for fields w/ identifiers)
931
932         # while the patron ID we got from the SC is valid, let's
933         # use the one returned from the ILS, just in case...
934         $resp .= add_field(FID_PATRON_ID,     $patron->id);
935         $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
936
937         # TODO: add code for the fields
938         #   hold items limit
939         #   overdue items limit
940         #   charged items limit
941
942         $resp .= add_field(FID_VALID_PATRON, 'Y');
943         if (defined($patron_pwd)) {
944             # If patron password was provided, report whether it was right or not.
945             $resp .= add_field(FID_VALID_PATRON_PWD,
946                                sipbool($patron->check_password($patron_pwd)));
947         }
948
949         $resp .= maybe_add(FID_CURRENCY,   $patron->currency);
950         $resp .= maybe_add(FID_FEE_AMT,    $patron->fee_amount);
951         $resp .= add_field(FID_FEE_LMT,    $patron->fee_limit);
952
953     # TODO: zero or more item details for 2.0 can go here:
954     #          hold_items
955     #       overdue_items
956     #       charged_items
957     #          fine_items
958     #        recall_items
959
960         $resp .= summary_info($ils, $patron, $summary, $start, $end);
961
962         $resp .= maybe_add(FID_HOME_ADDR,  $patron->address);
963         $resp .= maybe_add(FID_EMAIL,      $patron->email_addr);
964         $resp .= maybe_add(FID_HOME_PHONE, $patron->home_phone);
965
966         # SIP 2.0 extensions used by Envisionware
967         # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
968         $resp .= maybe_add(FID_PATRON_BIRTHDATE, $patron->birthdate);
969         $resp .= maybe_add(FID_PATRON_CLASS,     $patron->ptype);
970
971         # Custom protocol extension to report patron internet privileges
972         $resp .= maybe_add(FID_INET_PROFILE,     $patron->inet_privileges);
973
974         $resp .= maybe_add(FID_SCREEN_MSG,       $patron->screen_msg);
975         $resp .= maybe_add(FID_PRINT_LINE,       $patron->print_line);
976     } else {
977         # Invalid patron ID:
978         # no privileges, no items associated,
979         # no personal name, and is invalid (if we're using 2.00)
980         $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
981         $resp .= '0000' x 6;
982
983         # patron ID is invalid, but field is required, so just echo it back
984         $resp .= add_field(FID_PATRON_ID,     $fields->{(FID_PATRON_ID)});
985         $resp .= add_field(FID_PERSONAL_NAME, '');
986
987         if ($protocol_version >= 2) {
988             $resp .= add_field(FID_VALID_PATRON, 'N');
989         }
990     }
991
992     $resp .= add_field(FID_INST_ID,       ($ils->institution_id || 'SIP2'));
993     $self->write_msg($resp);
994     return(PATRON_INFO);
995 }
996
997 sub handle_end_patron_session {
998     my ($self, $server) = @_;
999     my $ils = $server->{ils};
1000     my $trans_date;
1001     my $fields = $self->{fields};
1002     my $resp = END_SESSION_RESP;
1003     my ($status, $screen_msg, $print_line);
1004
1005     ($trans_date) = @{$self->{fixed_fields}};
1006
1007     $ils->check_inst_id($fields->{FID_INST_ID}, "handle_end_patron_session");
1008
1009     ($status, $screen_msg, $print_line) = $ils->end_patron_session($fields->{(FID_PATRON_ID)});
1010
1011     $resp .= $status ? 'Y' : 'N';
1012     $resp .= Sip::timestamp();
1013
1014     $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
1015     $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
1016
1017     $resp .= maybe_add(FID_SCREEN_MSG, $screen_msg);
1018     $resp .= maybe_add(FID_PRINT_LINE, $print_line);
1019
1020     $self->write_msg($resp);
1021
1022     return(END_PATRON_SESSION);
1023 }
1024
1025 sub handle_fee_paid {
1026     my ($self, $server) = @_;
1027     my $ils = $server->{ils};
1028     my ($trans_date, $fee_type, $pay_type, $currency) = $self->{fixed_fields};
1029     my $fields = $self->{fields};
1030     my ($fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd);
1031     my ($fee_id, $trans_id);
1032     my $status;
1033     my $resp = FEE_PAID_RESP;
1034
1035     $fee_amt = $fields->{(FID_FEE_AMT)};
1036     $inst_id = $fields->{(FID_INST_ID)};
1037     $patron_id = $fields->{(FID_PATRON_ID)};
1038     $patron_pwd = $fields->{(FID_PATRON_PWD)};
1039     $fee_id = $fields->{(FID_FEE_ID)};
1040     $trans_id = $fields->{(FID_TRANSACTION_ID)};
1041
1042     $ils->check_inst_id($inst_id, "handle_fee_paid");
1043
1044     $status = $ils->pay_fee($patron_id, $patron_pwd, $fee_amt, $fee_type,
1045                            $pay_type, $fee_id, $trans_id, $currency);
1046
1047     $resp .= ($status->ok ? 'Y' : 'N') . Sip::timestamp;
1048     $resp .= add_field(FID_INST_ID, $inst_id);
1049     $resp .= add_field(FID_PATRON_ID, $patron_id);
1050     $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
1051     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1052     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1053
1054     $self->write_msg($resp);
1055
1056     return(FEE_PAID);
1057 }
1058
1059 sub handle_item_information {
1060     my ($self, $server) = @_;
1061     my $ils = $server->{ils};
1062     my $trans_date;
1063     my $fields = $self->{fields};
1064     my $resp = ITEM_INFO_RESP;
1065     my $item;
1066     my $i;
1067
1068     ($trans_date) = @{$self->{fixed_fields}};
1069
1070     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_item_information");
1071
1072     $item =  $ils->find_item($fields->{(FID_ITEM_ID)});
1073
1074     if (!defined($item)) {
1075         # Invalid Item ID
1076         # "Other" circ stat, "Other" security marker, "Unknown" fee type
1077         $resp .= "010101";
1078         $resp .= Sip::timestamp;
1079         # Just echo back the invalid item id
1080         $resp .= add_field(FID_ITEM_ID, $fields->{(FID_ITEM_ID)});
1081         # title id is required, but we don't have one
1082         $resp .= add_field(FID_TITLE_ID, '');
1083     } else {
1084         # Valid Item ID, send the good stuff
1085         $resp .= $item->sip_circulation_status;
1086         $resp .= $item->sip_security_marker;
1087         $resp .= $item->sip_fee_type;
1088         $resp .= Sip::timestamp;
1089
1090         $resp .= add_field(FID_ITEM_ID,  $item->id);
1091         $resp .= add_field(FID_TITLE_ID, $item->title_id);
1092
1093         $resp .= maybe_add(FID_MEDIA_TYPE,   $item->sip_media_type);
1094         $resp .= maybe_add(FID_PERM_LOCN,    $item->permanent_location);
1095         $resp .= maybe_add(FID_CURRENT_LOCN, $item->current_location);
1096         $resp .= maybe_add(FID_ITEM_PROPS,   $item->sip_item_properties);
1097
1098         if (($i = $item->fee) != 0) {
1099             $resp .= add_field(FID_CURRENCY, $item->fee_currency);
1100             $resp .= add_field(FID_FEE_AMT, $i);
1101         }
1102         $resp .= maybe_add(FID_OWNER, $item->owner);
1103
1104         if (($i = scalar @{$item->hold_queue}) > 0) {
1105             $resp .= add_field(FID_HOLD_QUEUE_LEN, $i);
1106         }
1107         if (($i = $item->due_date) != 0) {
1108             $resp .= add_field(FID_DUE_DATE, Sip::timestamp($i));
1109         }
1110         if (($i = $item->recall_date) != 0) {
1111             $resp .= add_field(FID_RECALL_DATE, Sip::timestamp($i));
1112         }
1113         if (($i = $item->hold_pickup_date) != 0) {
1114             $resp .= add_field(FID_HOLD_PICKUP_DATE, Sip::timestamp($i));
1115         }
1116
1117         $resp .= maybe_add(FID_SCREEN_MSG, $item->screen_msg);
1118         $resp .= maybe_add(FID_PRINT_LINE, $item->print_line);
1119     }
1120
1121     $self->write_msg($resp);
1122
1123     return(ITEM_INFORMATION);
1124 }
1125
1126 sub handle_item_status_update {
1127     my ($self, $server) = @_;
1128     my $ils = $server->{ils};
1129     my ($trans_date, $item_id, $terminal_pwd, $item_props);
1130     my $fields = $self->{fields};
1131     my $status;
1132     my $item;
1133     my $resp = ITEM_STATUS_UPDATE_RESP;
1134
1135     ($trans_date) = @{$self->{fixed_fields}};
1136
1137     $ils->check_inst_id($fields->{(FID_INST_ID)});
1138
1139     $item_id = $fields->{(FID_ITEM_ID)};
1140     $item_props = $fields->{(FID_ITEM_PROPS)};
1141
1142         if (!defined($item_id)) {
1143                 syslog("LOG_WARNING",
1144                         "handle_item_status: received message without Item ID field");
1145     } else {
1146                 $item = $ils->find_item($item_id);
1147         }
1148
1149     if (!$item) {
1150         # Invalid Item ID
1151         $resp .= '0';
1152         $resp .= Sip::timestamp;
1153         $resp .= add_field(FID_ITEM_ID, $item_id);
1154     } else {
1155         # Valid Item ID
1156
1157         $status = $item->status_update($item_props);
1158
1159         $resp .= $status->ok ? '1' : '0';
1160         $resp .= Sip::timestamp;
1161
1162         $resp .= add_field(FID_ITEM_ID, $item->id);
1163         $resp .= add_field(FID_TITLE_ID, $item->title_id);
1164         $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
1165     }
1166
1167     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1168     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1169
1170     $self->write_msg($resp);
1171
1172     return(ITEM_STATUS_UPDATE);
1173 }
1174
1175 sub handle_patron_enable {
1176     my ($self, $server) = @_;
1177     my $ils = $server->{ils};
1178     my $fields = $self->{fields};
1179     my ($trans_date, $patron_id, $terminal_pwd, $patron_pwd);
1180     my ($status, $patron);
1181     my $resp = PATRON_ENABLE_RESP;
1182
1183     ($trans_date) = @{$self->{fixed_fields}};
1184     $patron_id = $fields->{(FID_PATRON_ID)};
1185     $patron_pwd = $fields->{(FID_PATRON_PWD)};
1186
1187     syslog("LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'",
1188            $patron_id, $patron_pwd);
1189
1190     $patron = $ils->find_patron($patron_id);
1191
1192     if (!defined($patron)) {
1193         # Invalid patron ID
1194         $resp .= 'YYYY' . (' ' x 10) . '000' . Sip::timestamp();
1195         $resp .= add_field(FID_PATRON_ID, $patron_id);
1196         $resp .= add_field(FID_PERSONAL_NAME, '');
1197         $resp .= add_field(FID_VALID_PATRON, 'N');
1198         $resp .= add_field(FID_VALID_PATRON_PWD, 'N');
1199     } else {
1200         # valid patron
1201         if (!defined($patron_pwd) || $patron->check_password($patron_pwd)) {
1202             # Don't enable the patron if there was an invalid password
1203             $status = $patron->enable;
1204         }
1205         $resp .= patron_status_string($patron);
1206         $resp .= $patron->language . Sip::timestamp();
1207
1208         $resp .= add_field(FID_PATRON_ID, $patron->id);
1209         $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
1210         if (defined($patron_pwd)) {
1211             $resp .= add_field(FID_VALID_PATRON_PWD,
1212                                sipbool($patron->check_password($patron_pwd)));
1213         }
1214         $resp .= add_field(FID_VALID_PATRON, 'Y');
1215         $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
1216         $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
1217     }
1218
1219     $resp .= add_field(FID_INST_ID, $ils->institution);
1220
1221     $self->write_msg($resp);
1222
1223     return(PATRON_ENABLE);
1224 }
1225
1226 sub handle_hold {
1227     my ($self, $server) = @_;
1228     my $ils = $server->{ils};
1229     my ($hold_mode, $trans_date);
1230     my ($expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd);
1231     my ($item_id, $title_id, $fee_ack);
1232     my $fields = $self->{fields};
1233     my $status;
1234     my $resp = HOLD_RESP;
1235
1236     ($hold_mode, $trans_date) = @{$self->{fixed_fields}};
1237
1238     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_hold");
1239
1240     $patron_id   = $fields->{(FID_PATRON_ID)  };
1241     $expiry_date = $fields->{(FID_EXPIRATION) } || '';
1242     $pickup_locn = $fields->{(FID_PICKUP_LOCN)} || '';
1243     $hold_type   = $fields->{(FID_HOLD_TYPE)  } || '2'; # Any copy of title
1244     $patron_pwd  = $fields->{(FID_PATRON_PWD) };
1245     $item_id     = $fields->{(FID_ITEM_ID)    } || '';
1246     $title_id    = $fields->{(FID_TITLE_ID)   } || '';
1247     $fee_ack     = $fields->{(FID_FEE_ACK)    } || 'N';
1248
1249     if ($hold_mode eq '+') {
1250         $status = $ils->add_hold($patron_id, $patron_pwd, $item_id, $title_id,
1251                                                 $expiry_date, $pickup_locn, $hold_type, $fee_ack);
1252     } elsif ($hold_mode eq '-') {
1253         $status = $ils->cancel_hold($patron_id, $patron_pwd, $item_id, $title_id);
1254     } elsif ($hold_mode eq '*') {
1255         $status = $ils->alter_hold($patron_id, $patron_pwd, $item_id, $title_id,
1256                                                 $expiry_date, $pickup_locn, $hold_type, $fee_ack);
1257     } else {
1258         syslog("LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'",
1259                $hold_mode, $server->{account}->{id});
1260         $status = $ils->Transaction::Hold;              # new?
1261         $status->screen_msg("System error. Please contact library staff.");
1262     }
1263
1264     $resp .= $status->ok;
1265     $resp .= sipbool($status->item  &&  $status->item->available($patron_id));
1266     $resp .= Sip::timestamp;
1267
1268     if ($status->ok) {
1269         $resp .= add_field(FID_PATRON_ID,   $status->patron->id);
1270
1271         ($status->expiration_date) and
1272         $resp .= maybe_add(FID_EXPIRATION,
1273                                      Sip::timestamp($status->expiration_date));
1274         $resp .= maybe_add(FID_QUEUE_POS,   $status->queue_position);
1275         $resp .= maybe_add(FID_PICKUP_LOCN, $status->pickup_location);
1276         $resp .= maybe_add(FID_ITEM_ID,     $status->item->id);
1277         $resp .= maybe_add(FID_TITLE_ID,    $status->item->title_id);
1278     } else {
1279         # Not ok.  still need required fields
1280         $resp .= add_field(FID_PATRON_ID,   $patron_id);
1281     }
1282
1283     $resp .= add_field(FID_INST_ID,     $ils->institution);
1284     $resp .= maybe_add(FID_SCREEN_MSG,  $status->screen_msg);
1285     $resp .= maybe_add(FID_PRINT_LINE,  $status->print_line);
1286
1287     $self->write_msg($resp);
1288
1289     return(HOLD);
1290 }
1291
1292 sub handle_renew {
1293     my ($self, $server) = @_;
1294     my $ils = $server->{ils};
1295     my ($third_party, $no_block, $trans_date, $nb_due_date);
1296     my ($patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack);
1297     my $fields = $self->{fields};
1298     my $status;
1299     my ($patron, $item);
1300     my $resp = RENEW_RESP;
1301
1302     ($third_party, $no_block, $trans_date, $nb_due_date) =
1303         @{$self->{fixed_fields}};
1304
1305     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew");
1306
1307     if ($no_block eq 'Y') {
1308         syslog("LOG_WARNING",
1309                "handle_renew: recieved 'no block' renewal from terminal '%s'",
1310                $server->{account}->{id});
1311     }
1312
1313     $patron_id  = $fields->{(FID_PATRON_ID)};
1314     $patron_pwd = $fields->{(FID_PATRON_PWD)};
1315     $item_id    = $fields->{(FID_ITEM_ID)};
1316     $title_id   = $fields->{(FID_TITLE_ID)};
1317     $item_props = $fields->{(FID_ITEM_PROPS)};
1318     $fee_ack    = $fields->{(FID_FEE_ACK)};
1319
1320     $status = $ils->renew($patron_id, $patron_pwd, $item_id, $title_id,
1321                           $no_block, $nb_due_date, $third_party,
1322                           $item_props, $fee_ack);
1323
1324     $patron = $status->patron;
1325     $item   = $status->item;
1326
1327     if ($status->ok) {
1328         $resp .= '1';
1329         $resp .= $status->renewal_ok ? 'Y' : 'N';
1330         if ($ils->supports('magnetic media')) {
1331             $resp .= sipbool($item->magnetic);
1332         } else {
1333             $resp .= 'U';
1334         }
1335         $resp .= sipbool($status->desensitize);
1336         $resp .= Sip::timestamp;
1337         $resp .= add_field(FID_PATRON_ID, $patron->id);
1338         $resp .= add_field(FID_ITEM_ID,  $item->id);
1339         $resp .= add_field(FID_TITLE_ID, $item->title_id);
1340         $resp .= add_field(FID_DUE_DATE, Sip::timestamp($item->due_date));
1341         if ($ils->supports('security inhibit')) {
1342             $resp .= add_field(FID_SECURITY_INHIBIT,
1343                                $status->security_inhibit);
1344         }
1345         $resp .= add_field(FID_MEDIA_TYPE, $item->sip_media_type);
1346         $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
1347     } else {
1348         # renew failed for some reason
1349         # not OK, renewal not OK, Unknown media type (why bother checking?)
1350         $resp .= '0NUN';
1351         $resp .= Sip::timestamp;
1352         # If we found the patron or the item, the return the ILS
1353         # information, otherwise echo back the infomation we received
1354         # from the terminal
1355         $resp .= add_field(FID_PATRON_ID, $patron ? $patron->id     : $patron_id);
1356         $resp .= add_field(FID_ITEM_ID,     $item ? $item->id       : $item_id  );
1357         $resp .= add_field(FID_TITLE_ID,    $item ? $item->title_id : $title_id );
1358         $resp .= add_field(FID_DUE_DATE, '');
1359     }
1360
1361     if ($status->fee_amount) {
1362         $resp .= add_field(FID_FEE_AMT,  $status->fee_amount);
1363         $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
1364         $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
1365         $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
1366     }
1367
1368     $resp .= add_field(FID_INST_ID, $ils->institution);
1369     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1370     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1371
1372     $self->write_msg($resp);
1373
1374     return(RENEW);
1375 }
1376
1377 sub handle_renew_all {
1378     # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1379
1380     my ($self, $server) = @_;
1381     my $ils = $server->{ils};
1382     my ($trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack);
1383     my $fields = $self->{fields};
1384     my $resp = RENEW_ALL_RESP;
1385     my $status;
1386     my (@renewed, @unrenewed);
1387
1388     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew_all");
1389
1390     ($trans_date) = @{$self->{fixed_fields}};
1391
1392     $patron_id    = $fields->{(FID_PATRON_ID)};
1393     $patron_pwd   = $fields->{(FID_PATRON_PWD)};
1394     $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
1395     $fee_ack      = $fields->{(FID_FEE_ACK)};
1396
1397     $status = $ils->renew_all($patron_id, $patron_pwd, $fee_ack);
1398
1399     $resp .= $status->ok ? '1' : '0';
1400
1401         if (!$status->ok) {
1402                 $resp .= add_count("renew_all/renewed_count"  , 0);
1403                 $resp .= add_count("renew_all/unrenewed_count", 0);
1404                 @renewed = ();
1405                 @unrenewed = ();
1406         } else {
1407                 @renewed   = (@{$status->renewed});
1408                 @unrenewed = (@{$status->unrenewed});
1409                 $resp .= add_count("renew_all/renewed_count"  , scalar @renewed  );
1410                 $resp .= add_count("renew_all/unrenewed_count", scalar @unrenewed);
1411         }
1412
1413     $resp .= Sip::timestamp;
1414     $resp .= add_field(FID_INST_ID, $ils->institution);
1415
1416     $resp .= join('', map(add_field(FID_RENEWED_ITEMS  , $_), @renewed  ));
1417     $resp .= join('', map(add_field(FID_UNRENEWED_ITEMS, $_), @unrenewed));
1418
1419     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1420     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1421
1422     $self->write_msg($resp);
1423
1424     return(RENEW_ALL);
1425 }
1426
1427 #
1428 # send_acs_status($self, $server)
1429 #
1430 # Send an ACS Status message, which is contains lots of little fields
1431 # of information gleaned from all sorts of places.
1432 #
1433
1434 my @message_type_names = (
1435                           "patron status request",
1436                           "checkout",
1437                           "checkin",
1438                           "block patron",
1439                           "acs status",
1440                           "request sc/acs resend",
1441                           "login",
1442                           "patron information",
1443                           "end patron session",
1444                           "fee paid",
1445                           "item information",
1446                           "item status update",
1447                           "patron enable",
1448                           "hold",
1449                           "renew",
1450                           "renew all",
1451                          );
1452
1453 sub send_acs_status {
1454     my ($self, $server, $screen_msg, $print_line) = @_;
1455     my $msg = ACS_STATUS;
1456         ($server) or die "send_acs_status error: no \$server argument received";
1457     my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1458     my $policy  = $server->{policy}  or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1459     my $ils     = $server->{ils}     or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1460     my ($online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy);
1461     my ($status_update_ok, $offline_ok, $timeout, $retries);
1462
1463     $online_status = 'Y';
1464     $checkout_ok = sipbool($ils->checkout_ok);
1465     $checkin_ok  = sipbool($ils->checkin_ok);
1466     $ACS_renewal_policy = sipbool($policy->{renewal});
1467     $status_update_ok   = sipbool($ils->status_update_ok);
1468     $offline_ok = sipbool($ils->offline_ok);
1469     $timeout = sprintf("%03d", $policy->{timeout});
1470     $retries = sprintf("%03d", $policy->{retries});
1471
1472     if (length($timeout) != 3) {
1473         syslog("LOG_ERR", "handle_acs_status: timeout field wrong size: '%s'",
1474                $timeout);
1475         $timeout = '000';
1476     }
1477
1478     if (length($retries) != 3) {
1479         syslog("LOG_ERR", "handle_acs_status: retries field wrong size: '%s'",
1480                $retries);
1481         $retries = '000';
1482     }
1483
1484     $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1485     $msg .= "$status_update_ok$offline_ok$timeout$retries";
1486     $msg .= Sip::timestamp();
1487
1488     if ($protocol_version == 1) {
1489         $msg .= '1.00';
1490     } elsif ($protocol_version == 2) {
1491         $msg .= '2.00';
1492     } else {
1493         syslog("LOG_ERR",
1494                'Bad setting for $protocol_version, "%s" in send_acs_status',
1495                $protocol_version);
1496         $msg .= '1.00';
1497     }
1498
1499     # Institution ID
1500     $msg .= add_field(FID_INST_ID, $account->{institution});
1501
1502     if ($protocol_version >= 2) {
1503         # Supported messages: we do it all
1504         my $supported_msgs = '';
1505
1506         foreach my $msg_name (@message_type_names) {
1507             if ($msg_name eq 'request sc/acs resend') {
1508                 $supported_msgs .= Sip::sipbool(1);
1509             } else {
1510                 $supported_msgs .= Sip::sipbool($ils->supports($msg_name));
1511             }
1512         }
1513         if (length($supported_msgs) < 16) {
1514             syslog("LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs);
1515         }
1516         $msg .= add_field(FID_SUPPORTED_MSGS, $supported_msgs);
1517     }
1518
1519     $msg .= maybe_add(FID_SCREEN_MSG, $screen_msg);
1520
1521     if (defined($account->{print_width}) && defined($print_line)
1522         && $account->{print_width} < length($print_line)) {
1523         syslog("LOG_WARNING", "send_acs_status: print line '%s' too long.  Truncating",
1524                $print_line);
1525         $print_line = substr($print_line, 0, $account->{print_width});
1526     }
1527
1528     $msg .= maybe_add(FID_PRINT_LINE, $print_line);
1529
1530     # Do we want to tell the terminal its location?
1531
1532     $self->write_msg($msg);
1533     return 1;
1534 }
1535
1536 #
1537 # build_patron_status: create the 14-char patron status
1538 # string for the Patron Status message
1539 #
1540 sub patron_status_string {
1541     my $patron = shift;
1542     my $patron_status;
1543
1544     syslog("LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok);
1545     $patron_status = sprintf(
1546         '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1547         denied($patron->charge_ok),
1548         denied($patron->renew_ok),
1549         denied($patron->recall_ok),
1550         denied($patron->hold_ok),
1551         boolspace($patron->card_lost),
1552         boolspace($patron->too_many_charged),
1553         boolspace($patron->too_many_overdue),
1554         boolspace($patron->too_many_renewal),
1555         boolspace($patron->too_many_claim_return),
1556         boolspace($patron->too_many_lost),
1557         boolspace($patron->excessive_fines),
1558         boolspace($patron->excessive_fees),
1559         boolspace($patron->recall_overdue),
1560         boolspace($patron->too_many_billed)
1561     );
1562     return $patron_status;
1563 }
1564
1565 sub api_auth($$) {
1566         # AUTH
1567         my ($username,$password) = (shift,shift);
1568         $ENV{REMOTE_USER} = $username;
1569         my $query = CGI->new();
1570         $query->param(userid   => $username);
1571         $query->param(password => $password);
1572         my ($status, $cookie, $sessionID) = check_api_auth($query, {circulate=>1}, "intranet");
1573         print STDERR "check_api_auth returns " . ($status || 'undef') . "\n";
1574         # print "api_auth userenv = " . &dump_userenv;
1575         return $status;
1576 }
1577
1578 1;
1579 __END__
1580