Major SIP components reworked.
[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 use UNIVERSAL qw(can);
14
15 use Sip qw(:all);
16 use Sip::Constants qw(:all);
17 use Sip::Checksum qw(verify_cksum);
18
19 use Data::Dumper;
20 use CGI;
21 use C4::Auth qw(&check_api_auth);
22
23 use vars qw(@ISA $VERSION @EXPORT_OK);
24
25 BEGIN {
26         $VERSION = 1.00;
27         @ISA = qw(Exporter);
28         @EXPORT_OK = qw(handle);
29 }
30
31 # Predeclare handler subroutines
32 use subs qw(handle_patron_status handle_checkout handle_checkin
33             handle_block_patron handle_sc_status handle_request_acs_resend
34             handle_login handle_patron_info handle_end_patron_session
35             handle_fee_paid handle_item_information handle_item_status_update
36             handle_patron_enable handle_hold handle_renew handle_renew_all);
37
38 #
39 # For the most part, Version 2.00 of the protocol just adds new
40 # variable fields, but sometimes it changes the fixed header.
41 #
42 # In general, if there's no '2.00' protocol entry for a handler, that's
43 # because 2.00 didn't extend the 1.00 version of the protocol.  This will
44 # be handled by the module initialization code following the declaration,
45 # which goes through the handlers table and creates a '2.00' entry that
46 # points to the same place as the '1.00' entry.  If there's a 2.00 entry
47 # but no 1.00 entry, then that means that it's a completely new service
48 # in 2.00, so 1.00 shouldn't recognize it.
49
50 my %handlers = (
51                 (PATRON_STATUS_REQ) => {
52                     name => "Patron Status Request",
53                     handler => \&handle_patron_status,
54                     protocol => {
55                         1 => {
56                             template => "A3A18",
57                             template_len => 21,
58                             fields => [(FID_INST_ID), (FID_PATRON_ID),
59                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
60                         }
61                     }
62                 },
63                 (CHECKOUT) => {
64                     name => "Checkout",
65                     handler => \&handle_checkout,
66                     protocol => {
67                         1 => {
68                             template => "CCA18A18",
69                             template_len => 38,
70                             fields => [(FID_INST_ID), (FID_PATRON_ID),
71                                        (FID_ITEM_ID), (FID_TERMINAL_PWD)],
72                         },
73                         2 => {
74                             template => "CCA18A18",
75                             template_len => 38,
76                             fields => [(FID_INST_ID), (FID_PATRON_ID),
77                                        (FID_ITEM_ID), (FID_TERMINAL_PWD),
78                                        (FID_ITEM_PROPS), (FID_PATRON_PWD),
79                                        (FID_FEE_ACK), (FID_CANCEL)],
80                         },
81                     }
82                 },
83                 (CHECKIN) => {
84                     name => "Checkin",
85                     handler => \&handle_checkin,
86                     protocol => {
87                         1 => {
88                             template => "CA18A18",
89                             template_len => 37,
90                             fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
91                                        (FID_ITEM_ID), (FID_TERMINAL_PWD)],
92                         },
93                         2 => {
94                             template => "CA18A18",
95                             template_len => 37,
96                             fields => [(FID_CURRENT_LOCN), (FID_INST_ID),
97                                        (FID_ITEM_ID), (FID_TERMINAL_PWD),
98                                        (FID_ITEM_PROPS), (FID_CANCEL)],
99                         }
100                     }
101                 },
102                 (BLOCK_PATRON) => {
103                     name => "Block Patron",
104                     handler => \&handle_block_patron,
105                     protocol => {
106                         1 => {
107                             template => "CA18",
108                             template_len => 19,
109                             fields => [(FID_INST_ID), (FID_BLOCKED_CARD_MSG),
110                                        (FID_PATRON_ID), (FID_TERMINAL_PWD)],
111                         },
112                     }
113                 },
114                 (SC_STATUS) => {
115                     name => "SC Status",
116                     handler => \&handle_sc_status,
117                     protocol => {
118                         1 => {
119                             template =>"CA3A4",
120                             template_len => 8,
121                             fields => [],
122                         }
123                     }
124                 },
125                 (REQUEST_ACS_RESEND) => {
126                     name => "Request ACS Resend",
127                     handler => \&handle_request_acs_resend,
128                     protocol => {
129                         1 => {
130                             template => "",
131                             template_len => 0,
132                             fields => [],
133                         }
134                     }
135                 },
136                 (LOGIN) => {
137                     name => "Login",
138                     handler => \&handle_login,
139                     protocol => {
140                         2 => {
141                             template => "A1A1",
142                             template_len => 2,
143                             fields => [(FID_LOGIN_UID), (FID_LOGIN_PWD),
144                                        (FID_LOCATION_CODE)],
145                         }
146                     }
147                 },
148                 (PATRON_INFO) => {
149                     name => "Patron Info",
150                     handler => \&handle_patron_info,
151                     protocol => {
152                         2 => {
153                             template => "A3A18A10",
154                             template_len => 31,
155                             fields => [(FID_INST_ID), (FID_PATRON_ID),
156                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD),
157                                        (FID_START_ITEM), (FID_END_ITEM)],
158                         }
159                     }
160                 },
161                 (END_PATRON_SESSION) => {
162                     name => "End Patron Session",
163                     handler => \&handle_end_patron_session,
164                     protocol => {
165                         2 => {
166                             template => "A18",
167                             template_len => 18,
168                             fields => [(FID_INST_ID), (FID_PATRON_ID),
169                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
170                         }
171                     }
172                 },
173                 (FEE_PAID) => {
174                     name => "Fee Paid",
175                     handler => \&handle_fee_paid,
176                     protocol => {
177                         2 => {
178                             template => "A18A2A3",
179                             template_len => 0,
180                             fields => [(FID_FEE_AMT), (FID_INST_ID),
181                                        (FID_PATRON_ID), (FID_TERMINAL_PWD),
182                                        (FID_PATRON_PWD), (FID_FEE_ID),
183                                        (FID_TRANSACTION_ID)],
184                         }
185                     }
186                 },
187                 (ITEM_INFORMATION) => {
188                     name => "Item Information",
189                     handler => \&handle_item_information,
190                     protocol => {
191                         2 => {
192                             template => "A18",
193                             template_len => 18,
194                             fields => [(FID_INST_ID), (FID_ITEM_ID),
195                                        (FID_TERMINAL_PWD)],
196                         }
197                     }
198                 },
199                 (ITEM_STATUS_UPDATE) => {
200                     name => "Item Status Update",
201                     handler => \&handle_item_status_update,
202                     protocol => {
203                         2 => {
204                             template => "A18",
205                             template_len => 18,
206                             fields => [(FID_INST_ID), (FID_PATRON_ID),
207                                        (FID_ITEM_ID), (FID_TERMINAL_PWD),
208                                        (FID_ITEM_PROPS)],
209                         }
210                     }
211                 },
212                 (PATRON_ENABLE) => {
213                     name => "Patron Enable",
214                     handler => \&handle_patron_enable,
215                     protocol => {
216                         2 => {
217                             template => "A18",
218                             template_len => 18,
219                             fields => [(FID_INST_ID), (FID_PATRON_ID),
220                                        (FID_TERMINAL_PWD), (FID_PATRON_PWD)],
221                         }
222                     }
223                 },
224                 (HOLD) => {
225                     name => "Hold",
226                     handler => \&handle_hold,
227                     protocol => {
228                         2 => {
229                             template => "AA18",
230                             template_len => 19,
231                             fields => [(FID_EXPIRATION), (FID_PICKUP_LOCN),
232                                        (FID_HOLD_TYPE), (FID_INST_ID),
233                                        (FID_PATRON_ID), (FID_PATRON_PWD),
234                                        (FID_ITEM_ID), (FID_TITLE_ID),
235                                        (FID_TERMINAL_PWD), (FID_FEE_ACK)],
236                         }
237                     }
238                 },
239                 (RENEW) => {
240                     name => "Renew",
241                     handler => \&handle_renew,
242                     protocol => {
243                         2 => {
244                             template => "CCA18A18",
245                             template_len => 38,
246                             fields => [(FID_INST_ID), (FID_PATRON_ID),
247                                        (FID_PATRON_PWD), (FID_ITEM_ID),
248                                        (FID_TITLE_ID), (FID_TERMINAL_PWD),
249                                        (FID_ITEM_PROPS), (FID_FEE_ACK)],
250                         }
251                     }
252                 },
253                 (RENEW_ALL) => {
254                     name => "Renew All",
255                     handler => \&handle_renew_all,
256                     protocol => {
257                         2 => {
258                             template => "A18",
259                             template_len => 18,
260                             fields => [(FID_INST_ID), (FID_PATRON_ID),
261                                        (FID_PATRON_PWD), (FID_TERMINAL_PWD),
262                                        (FID_FEE_ACK)],
263                         }
264                     }
265                 }
266                 );
267
268 #
269 # Now, initialize some of the missing bits of %handlers
270 #
271 foreach my $i (keys(%handlers)) {
272     if (!exists($handlers{$i}->{protocol}->{2})) {
273         $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
274     }
275 }
276
277 sub new {
278     my ($class, $msg, $seqno) = @_;
279     my $self = {};
280     my $msgtag = substr($msg, 0, 2);
281
282     if ($msgtag eq LOGIN) {
283         # If the client is using the 2.00-style "Login" message
284         # to authenticate to the server, then we get the Login message
285         # _before_ the client has indicated that it supports 2.00, but
286         # it's using the 2.00 login process, so it must support 2.00.
287                 $protocol_version = 2;
288     }
289     syslog("LOG_DEBUG", "Sip::MsgType::new('%s', '%s...', '%s'): msgtag '%s', protocol %s",
290                 $class, substr($msg, 0, 10), $msgtag, $seqno, $protocol_version);
291         # warn "SIP PROTOCOL: $protocol_version";       
292     if (!exists($handlers{$msgtag})) {
293                 syslog("LOG_WARNING", "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'",
294                $msgtag, $msg);
295                 return(undef);
296     } elsif (!exists($handlers{$msgtag}->{protocol}->{$protocol_version})) {
297                 syslog("LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'",
298                $msgtag, $protocol_version);
299                 return(undef);
300     }
301
302     bless $self, $class;
303
304     $self->{seqno} = $seqno;
305     $self->_initialize(substr($msg,2), $handlers{$msgtag});
306
307     return($self);
308 }
309
310 sub _initialize {
311         my ($self, $msg, $control_block) = @_;
312         my ($fs, $fn, $fe);
313         my $proto = $control_block->{protocol}->{$protocol_version};
314
315         $self->{name}    = $control_block->{name};
316         $self->{handler} = $control_block->{handler};
317
318         $self->{fields}       = {};
319         $self->{fixed_fields} = [];
320
321         chomp($msg);
322         $msg =~ tr/\cM//d;
323         $msg =~ s/\^M$//;
324         chomp($msg);
325
326         # syslog("LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s...')", $self->{name}, substr($msg, 0, 20));
327
328         foreach my $field (@{$proto->{fields}}) {
329                 $self->{fields}->{$field} = undef;
330         }
331
332     syslog("LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...)",
333                 $self->{name}, $msg, $proto->{template}, $proto->{template_len});
334
335     $self->{fixed_fields} = [ unpack($proto->{template}, $msg) ];
336
337     # Skip over the fixed fields and the split the rest of
338     # the message into fields based on the delimiter and parse them
339     foreach my $field (split(quotemeta($field_delimiter), substr($msg, $proto->{template_len}))) {
340                 $fn = substr($field, 0, 2);
341
342         if (!exists($self->{fields}->{$fn})) {
343                 syslog("LOG_WARNING", "Unsupported field '%s' in %s message '%s'",
344                         $fn, $self->{name}, $msg);
345         } elsif (defined($self->{fields}->{$fn})) {
346                 syslog("LOG_WARNING", "Duplicate field '%s' (previous value '%s') in %s message '%s'",
347                         $fn, $self->{fields}->{$fn}, $self->{name}, $msg);
348         } else {
349                 $self->{fields}->{$fn} = substr($field, 2);
350         }
351         }
352
353         return($self);
354 }
355
356 sub handle {
357     my ($msg, $server, $req) = @_;
358     my $config = $server->{config};
359     my $self;
360
361     #
362     # What's the field delimiter for variable length fields?
363     # This can't be based on the account, since we need to know
364     # the field delimiter to parse a SIP login message
365     #
366         if (defined($server->{config}->{delimiter})) {
367                 $field_delimiter = $server->{config}->{delimiter};
368         }
369
370     # error detection is active if this is a REQUEST_ACS_RESEND
371     # message with a checksum, or if the message is long enough
372     # and the last nine characters begin with a sequence number
373     # field
374     if ($msg eq REQUEST_ACS_RESEND_CKSUM) {
375                 # Special case
376                 $error_detection = 1;
377                 $self = new Sip::MsgType ((REQUEST_ACS_RESEND), 0);
378     } elsif((length($msg) > 11) && (substr($msg, -9, 2) eq "AY")) {
379                 $error_detection = 1;
380
381         if (!verify_cksum($msg)) {
382             syslog("LOG_WARNING", "Checksum failed on message '%s'", $msg);
383             # REQUEST_SC_RESEND with error detection
384             $last_response = REQUEST_SC_RESEND_CKSUM;
385             print("$last_response\r");
386             return REQUEST_ACS_RESEND;
387         } else {
388             # Save the sequence number, then strip off the
389             # error detection data to process the message
390             $self = new Sip::MsgType (substr($msg, 0, -9), substr($msg, -7, 1));
391         }
392     } elsif ($error_detection) {
393         # We received a non-ED message when ED is supposed to be active.
394         # Warn about this problem, then process the message anyway.
395                 syslog("LOG_WARNING",
396                "Received message without error detection: '%s'", $msg);
397                 $error_detection = 0;
398                 $self = new Sip::MsgType ($msg, 0);
399     } else {
400                 $self = new Sip::MsgType ($msg, 0);
401     }
402
403         if ((substr($msg, 0, 2) ne REQUEST_ACS_RESEND) &&
404                 $req && (substr($msg, 0, 2) ne $req)) {
405                 return substr($msg, 0, 2);
406         }
407         unless ($self->{handler}) {
408                 syslog("LOG_WARNING", "No handler defined for '%s'", $msg);
409                 return undef;
410         }
411     return($self->{handler}->($self, $server));  # FIXME
412         # FIXME: Use of uninitialized value in subroutine entry
413         # Can't use string ("") as a subroutine ref while "strict refs" in use
414 }
415
416 ##
417 ## Message Handlers
418 ##
419
420 #
421 # Patron status messages are produced in response to both
422 # "Request Patron Status" and "Block Patron"
423 #
424 # Request Patron Status requires a patron password, but
425 # Block Patron doesn't (since the patron may never have
426 # provided one before attempting some illegal action).
427
428 # ASSUMPTION: If the patron password field is present in the
429 # message, then it must match, otherwise incomplete patron status
430 # information will be returned to the terminal.
431
432 sub build_patron_status {
433     my ($patron, $lang, $fields)= @_;
434     my $patron_pwd = $fields->{(FID_PATRON_PWD)};
435     my $resp = (PATRON_STATUS_RESP);
436
437     if ($patron) {
438         $resp .= patron_status_string($patron);
439         $resp .= $lang . Sip::timestamp();
440         $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
441
442         # while the patron ID we got from the SC is valid, let's
443         # use the one returned from the ILS, just in case...
444         $resp .= add_field(FID_PATRON_ID, $patron->id);
445         if ($protocol_version >= 2) {
446             $resp .= add_field(FID_VALID_PATRON, 'Y');
447             # If the patron password field doesn't exist, then
448             # we can't report that the password was valid, now can
449             # we?  But if it does exist, then we know it's valid.
450             if (defined($patron_pwd)) {
451                 $resp .= add_field(FID_VALID_PATRON_PWD,
452                                    sipbool($patron->check_password($patron_pwd)));
453             }
454             $resp .= maybe_add(FID_CURRENCY, $patron->currency);
455             $resp .= maybe_add(FID_FEE_AMT, $patron->fee_amount);
456         }
457
458         $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
459         $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
460     } else {
461         # Invalid patron id.  Report that the user has no privs.,
462         # no personal name, and is invalid (if we're using 2.00)
463         $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
464         $resp .= add_field(FID_PERSONAL_NAME, '');
465
466         # the patron ID is invalid, but it's a required field, so
467         # just echo it back
468         $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
469
470         ($protocol_version >= 2) and 
471                 $resp .= add_field(FID_VALID_PATRON, 'N');
472     }
473
474     $resp .= add_field(FID_INST_ID, $fields->{(FID_INST_ID)});
475     return $resp;
476 }
477
478 sub handle_patron_status {
479         my ($self, $server) = @_;
480         #warn Dumper($server);  
481         my $ils = $server->{ils};
482         my $patron;
483         my $resp = (PATRON_STATUS_RESP);
484         my $account = $server->{account};
485     my ($lang, $date) = @{$self->{fixed_fields}};
486     my $fields = $self->{fields};
487         #warn Dumper($fields);
488         #warn FID_INST_ID;
489         #warn $fields->{(FID_INST_ID)};
490     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_patron_status");
491     $patron = $ils->find_patron($fields->{(FID_PATRON_ID)});
492     $resp = build_patron_status($patron, $lang, $fields);
493     $self->write_msg($resp);
494     return (PATRON_STATUS_REQ);
495 }
496
497 sub handle_checkout {
498     my ($self, $server) = @_;
499     my $account = $server->{account};
500     my $ils = $server->{ils};
501     my $inst = $ils->institution;
502     my ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date);
503     my $fields;
504     my ($patron_id, $item_id, $status);
505     my ($item, $patron);
506     my $resp;
507
508     ($sc_renewal_policy, $no_block, $trans_date, $nb_due_date) =
509         @{$self->{fixed_fields}};
510     $fields = $self->{fields};
511
512     $patron_id = $fields->{(FID_PATRON_ID)};
513     $item_id   = $fields->{(FID_ITEM_ID)};
514
515
516     if ($no_block eq 'Y') {
517         # Off-line transactions need to be recorded, but there's
518         # not a lot we can do about it
519         syslog("LOG_WARNING", "received no-block checkout from terminal '%s'",
520                $account->{id});
521
522         $status = $ils->checkout_no_block($patron_id, $item_id,
523                                           $sc_renewal_policy,
524                                           $trans_date, $nb_due_date);
525     } else {
526         # Does the transaction date really matter for items that are
527         # checkout out while the terminal is online?  I'm guessing 'no'
528                 $status = $ils->checkout($patron_id, $item_id, $sc_renewal_policy);
529     }
530
531     $item = $status->item;
532     $patron = $status->patron;
533
534     if ($status->ok) {
535         # Item successfully checked out
536         # Fixed fields
537         $resp = CHECKOUT_RESP . '1';
538         $resp .= sipbool($status->renew_ok);
539         if ($ils->supports('magnetic media')) {
540             $resp .= sipbool($item->magnetic);
541         } else {
542             $resp .= 'U';
543         }
544         # We never return the obsolete 'U' value for 'desensitize'
545         $resp .= sipbool($status->desensitize);
546         $resp .= Sip::timestamp;
547
548         # Now for the variable fields
549         $resp .= add_field(FID_INST_ID, $inst);
550         $resp .= add_field(FID_PATRON_ID, $patron_id);
551         $resp .= add_field(FID_ITEM_ID, $item_id);
552         $resp .= add_field(FID_TITLE_ID, $item->title_id);
553         $resp .= add_field(FID_DUE_DATE, $item->due_date);
554
555         $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
556         $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
557
558         if ($protocol_version >= 2) {
559             if ($ils->supports('security inhibit')) {
560                 $resp .= add_field(FID_SECURITY_INHIBIT,
561                                    $status->security_inhibit);
562             }
563             $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
564             $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
565
566             # Financials
567             if ($status->fee_amount) {
568                 $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
569                 $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
570                 $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
571                 $resp .= maybe_add(FID_TRANSACTION_ID,
572                                    $status->transaction_id);
573             }
574         }
575
576     } else {
577         # Checkout failed
578         # Checkout Response: not ok, no renewal, don't know mag. media,
579         # no desensitize
580         $resp = sprintf("120NUN%s", Sip::timestamp);
581         $resp .= add_field(FID_INST_ID, $inst);
582         $resp .= add_field(FID_PATRON_ID, $patron_id);
583         $resp .= add_field(FID_ITEM_ID, $item_id);
584
585         # If the item is valid, provide the title, otherwise
586         # leave it blank
587         $resp .= add_field(FID_TITLE_ID, $item ? $item->title_id : '');
588         # Due date is required.  Since it didn't get checked out,
589         # it's not due, so leave the date blank
590         $resp .= add_field(FID_DUE_DATE, '');
591
592         $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
593         $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
594
595         if ($protocol_version >= 2) {
596             # Is the patron ID valid?
597             $resp .= add_field(FID_VALID_PATRON, sipbool($patron));
598
599             if ($patron && exists($fields->{FID_PATRON_PWD})) {
600                 # Password provided, so we can tell if it was valid or not
601                 $resp .= add_field(FID_VALID_PATRON_PWD,
602                                    sipbool($patron->check_password($fields->{(FID_PATRON_PWD)})));
603             }
604         }
605     }
606
607     $self->write_msg($resp);
608     return(CHECKOUT);
609 }
610
611 sub handle_checkin {
612     my ($self, $server) = @_;
613     my $account = $server->{account};
614     my $ils     = $server->{ils};
615     my ($current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel);
616     my ($patron, $item, $status);
617     my $resp = CHECKIN_RESP;
618     my ($no_block, $trans_date, $return_date) = @{$self->{fixed_fields}};
619         my $fields = $self->{fields};
620
621         $current_loc = $fields->{(FID_CURRENT_LOCN)};
622         $inst_id     = $fields->{(FID_INST_ID)};
623         $item_id     = $fields->{(FID_ITEM_ID)};
624         $item_props  = $fields->{(FID_ITEM_PROPS)};
625         $cancel      = $fields->{(FID_CANCEL)};
626
627     $ils->check_inst_id($inst_id, "handle_checkin");
628
629     if ($no_block eq 'Y') {
630         # Off-line transactions, ick.
631         syslog("LOG_WARNING", "received no-block checkin from terminal '%s'",
632                $account->{id});
633         $status = $ils->checkin_no_block($item_id, $trans_date,
634                                          $return_date, $item_props, $cancel);
635     } else {
636         $status = $ils->checkin($item_id, $trans_date, $return_date,
637                                 $current_loc, $item_props, $cancel);
638     }
639
640     $patron = $status->patron;
641     $item   = $status->item;
642
643     $resp .= $status->ok ? '1' : '0';
644     $resp .= $status->resensitize ? 'Y' : 'N';
645     if ($item && $ils->supports('magnetic media')) {
646                 $resp .= sipbool($item->magnetic);
647     } else {
648         # The item barcode was invalid or the system doesn't support
649         # the 'magnetic media' indicator
650                 $resp .= 'U';
651     }
652     $resp .= $status->alert ? 'Y' : 'N';
653     $resp .= Sip::timestamp;
654     $resp .= add_field(FID_INST_ID, $inst_id);
655     $resp .= add_field(FID_ITEM_ID, $item_id);
656
657     if ($item) {
658         $resp .= add_field(FID_PERM_LOCN, $item->permanent_location);
659         $resp .= maybe_add(FID_TITLE_ID, $item->title_id);
660     }
661
662     if ($protocol_version >= 2) {
663         $resp .= maybe_add(FID_SORT_BIN, $status->sort_bin);
664         if ($patron) {
665             $resp .= add_field(FID_PATRON_ID, $patron->id);
666         }
667         if ($item) {
668             $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
669             $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
670         }
671     }
672
673     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
674     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
675
676     $self->write_msg($resp);
677
678     return(CHECKIN);
679 }
680
681 sub handle_block_patron {
682     my ($self, $server) = @_;
683     my $account = $server->{account};
684     my $ils = $server->{ils};
685     my ($card_retained, $trans_date);
686     my ($inst_id, $blocked_card_msg, $patron_id, $terminal_pwd);
687     my ($fields,$resp,$patron);
688
689     ($card_retained, $trans_date) = @{$self->{fixed_fields}};
690     $fields = $self->{fields};
691     $inst_id          = $fields->{(FID_INST_ID)};
692     $blocked_card_msg = $fields->{(FID_BLOCKED_CARD_MSG)};
693     $patron_id        = $fields->{(FID_PATRON_ID)};
694     $terminal_pwd     = $fields->{(FID_TERMINAL_PWD)};
695
696     # Terminal passwords are different from account login
697     # passwords, but I have no idea what to do with them.  So,
698     # I'll just ignore them for now.
699         
700         # FIXME ???
701
702     $ils->check_inst_id($inst_id, "block_patron");
703     $patron = $ils->find_patron($patron_id);
704
705     # The correct response for a "Block Patron" message is a
706     # "Patron Status Response", so use that handler to generate
707     # the message, but then return the correct code from here.
708     #
709     # Normally, the language is provided by the "Patron Status"
710     # fixed field, but since we're not responding to one of those
711     # we'll just say, "Unspecified", as per the spec.  Let the
712     # terminal default to something that, one hopes, will be
713     # intelligible
714         if ($patron) {
715                 # Valid patron id
716                 $patron->block($card_retained, $blocked_card_msg);
717         }
718
719     $resp = build_patron_status($patron, $patron->language, $fields);
720     $self->write_msg($resp);
721     return(BLOCK_PATRON);
722 }
723
724 sub handle_sc_status {
725     my ($self, $server) = @_;
726         ($server) or warn "handle_sc_status error: no \$server argument received.";
727         my ($status, $print_width, $sc_protocol_version) = @{$self->{fixed_fields}};
728         my ($new_proto);
729
730         if ($sc_protocol_version =~ /^1\./) {
731                 $new_proto = 1;
732         } elsif ($sc_protocol_version =~ /^2\./) {
733                 $new_proto = 2;
734         } else {
735                 syslog("LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version);
736                 $new_proto = 1;
737         }
738
739         if ($new_proto != $protocol_version) {
740                 syslog("LOG_INFO", "Setting protocol level to $new_proto");
741                 $protocol_version = $new_proto;
742         }
743
744     if ($status == SC_STATUS_PAPER) {
745         syslog("LOG_WARNING", "Self-Check unit '%s@%s' out of paper",
746                $self->{account}->{id}, $self->{account}->{institution});
747     } elsif ($status == SC_STATUS_SHUTDOWN) {
748         syslog("LOG_WARNING", "Self-Check unit '%s@%s' shutting down",
749                $self->{account}->{id}, $self->{account}->{institution});
750     }
751
752     $self->{account}->{print_width} = $print_width;
753     return (send_acs_status($self, $server) ? SC_STATUS : '');
754 }
755
756 sub handle_request_acs_resend {
757     my ($self, $server) = @_;
758
759     if (!$last_response) {
760         # We haven't sent anything yet, so respond with a
761         # REQUEST_SC_RESEND msg (p. 16)
762         $self->write_msg(REQUEST_SC_RESEND);
763     } elsif ((length($last_response) < 9)
764              || substr($last_response, -9, 2) ne 'AY') {
765         # When resending a message, we aren't supposed to include
766         # a sequence number, even if the original had one (p. 4).
767         # If the last message didn't have a sequence number, then
768         # we can just send it.
769         print("$last_response\r");      # not write_msg?
770     } else {
771         # Cut out the sequence number and checksum, since the old
772         # checksum is wrong for the resent message.
773         my $rebuilt = substr($last_response, 0, -9);
774         $self->write_msg($rebuilt);
775     }
776
777     return REQUEST_ACS_RESEND;
778 }
779
780 sub handle_login {
781     my ($self, $server) = @_;
782     my ($uid_algorithm, $pwd_algorithm);
783     my ($uid, $pwd);
784     my $inst;
785     my $fields;
786     my $status = 1;             # Assume it all works
787
788     $fields = $self->{fields};
789     ($uid_algorithm, $pwd_algorithm) = @{$self->{fixed_fields}};
790
791     $uid = $fields->{(FID_LOGIN_UID)};
792     $pwd = $fields->{(FID_LOGIN_PWD)};
793
794     if ($uid_algorithm || $pwd_algorithm) {
795                 syslog("LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm");
796                 $status = 0;
797     }
798
799     if (!exists($server->{config}->{accounts}->{$uid})) {
800                 syslog("LOG_WARNING", "MsgType::handle_login: Unknown login '$uid'");
801                 $status = 0;
802     } elsif ($server->{config}->{accounts}->{$uid}->{password} ne $pwd) {
803                 syslog("LOG_WARNING", "MsgType::handle_login: Invalid password for login '$uid'");
804                 $status = 0;
805     } else {
806         # Store the active account someplace handy for everybody else to find.
807                 $server->{account} = $server->{config}->{accounts}->{$uid};
808                 $inst = $server->{account}->{institution};
809                 $server->{institution} = $server->{config}->{institutions}->{$inst};
810                 $server->{policy} = $server->{institution}->{policy};
811                 $server->{sip_username} = $uid;
812                 $server->{sip_password} = $pwd;
813
814                 my $auth_status = api_auth($uid,$pwd);
815                 if (!$auth_status or $auth_status !~ /^ok$/i) {
816                         syslog("LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s",
817                                                 $uid, $inst, ($auth_status||'unknown'));
818                         $status = 0;
819                 } else {
820                         syslog("LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst);
821                         #
822                         # initialize connection to ILS
823                         #
824                         my $module = $server->{config}->{institutions}->{$inst}->{implementation};
825                         syslog("LOG_DEBUG", 'handle_login: ' . Dumper($module));
826                         $module->use;
827                         if ($@) {
828                                 syslog("LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed",
829                                                 $server->{service}, $module, $inst);
830                                 die("Failed to load ILS implementation '$module' for $inst");
831                         }
832
833                         # like   ILS->new(), I think.
834                         $server->{ils} = $module->new($server->{institution}, $server->{account});
835                         if (!$server->{ils}) {
836                             syslog("LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst);
837                             die("Unable to connect to ILS '$inst'");
838                         }
839                 }
840         }
841     $self->write_msg(LOGIN_RESP . $status);
842     return $status ? LOGIN : '';
843 }
844
845 #
846 # Build the detailed summary information for the Patron
847 # Information Response message based on the first 'Y' that appears
848 # in the 'summary' field of the Patron Information reqest.  The
849 # specification says that only one 'Y' can appear in that field,
850 # and we're going to believe it.
851 #
852 sub summary_info {
853     my ($ils, $patron, $summary, $start, $end) = @_;
854     my $resp = '';
855     my $itemlist;
856     my $summary_type;
857     my ($func, $fid);
858     #
859     # Map from offsets in the "summary" field of the Patron Information
860     # message to the corresponding field and handler
861     #
862     my @summary_map = (
863                        { func => $patron->can("hold_items"),
864                          fid => FID_HOLD_ITEMS },
865                        { func => $patron->can("overdue_items"),
866                          fid => FID_OVERDUE_ITEMS },
867                        { func => $patron->can("charged_items"),
868                          fid => FID_CHARGED_ITEMS },
869                        { func => $patron->can("fine_items"),
870                          fid => FID_FINE_ITEMS },
871                        { func => $patron->can("recall_items"),
872                          fid => FID_RECALL_ITEMS },
873                        { func => $patron->can("unavail_holds"),
874                          fid => FID_UNAVAILABLE_HOLD_ITEMS },
875                       );
876
877
878     if (($summary_type = index($summary, 'Y')) == -1) {
879         # No detailed information required
880         return '';
881     }
882
883     syslog("LOG_DEBUG", "Summary_info: index == '%d', field '%s'",
884            $summary_type, $summary_map[$summary_type]->{fid});
885
886     $func = $summary_map[$summary_type]->{func};
887     $fid  = $summary_map[$summary_type]->{fid};
888     $itemlist = &$func($patron, $start, $end);
889
890     syslog("LOG_DEBUG", "summary_info: list = (%s)", join(", ", @{$itemlist}));
891     foreach my $i (@{$itemlist}) {
892         $resp .= add_field($fid, $i);
893     }
894
895     return $resp;
896 }
897
898 sub handle_patron_info {
899     my ($self, $server) = @_;
900     my $ils = $server->{ils};
901     my ($lang, $trans_date, $summary) = @{$self->{fixed_fields}};
902     my $fields = $self->{fields};
903     my ($inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end);
904     my ($resp, $patron, $count);
905
906     $inst_id      = $fields->{(FID_INST_ID)};
907     $patron_id    = $fields->{(FID_PATRON_ID)};
908     $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
909     $patron_pwd   = $fields->{(FID_PATRON_PWD)};
910     $start        = $fields->{(FID_START_ITEM)};
911     $end          = $fields->{(FID_END_ITEM)};
912
913     $patron = $ils->find_patron($patron_id);
914
915     $resp = (PATRON_INFO_RESP);
916     if ($patron) {
917         $resp .= patron_status_string($patron);
918         $resp .= $lang . Sip::timestamp();
919
920         $resp .= add_count('patron_info/hold_items',
921                            scalar @{$patron->hold_items});
922         $resp .= add_count('patron_info/overdue_items',
923                            scalar @{$patron->overdue_items});
924         $resp .= add_count('patron_info/charged_items',
925                            scalar @{$patron->charged_items});
926         $resp .= add_count('patron_info/fine_items',
927                            scalar @{$patron->fine_items});
928         $resp .= add_count('patron_info/recall_items',
929                            scalar @{$patron->recall_items});
930         $resp .= add_count('patron_info/unavail_holds',
931                            scalar @{$patron->unavail_holds});
932
933         # while the patron ID we got from the SC is valid, let's
934         # use the one returned from the ILS, just in case...
935         $resp .= add_field(FID_PATRON_ID, $patron->id);
936
937         $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
938
939         # TODO: add code for the fields
940         # hold items limit
941         # overdue items limit
942         # charged items limit
943         # fee limit
944
945         $resp .= maybe_add(FID_CURRENCY, $patron->currency);
946         $resp .= maybe_add(FID_FEE_AMT, $patron->fee_amount);
947
948         $resp .= maybe_add(FID_HOME_ADDR,$patron->address);
949         $resp .= maybe_add(FID_EMAIL, $patron->email_addr);
950         $resp .= maybe_add(FID_HOME_PHONE, $patron->home_phone);
951
952         $resp .= summary_info($ils, $patron, $summary, $start, $end);
953
954         $resp .= add_field(FID_VALID_PATRON, 'Y');
955         if (defined($patron_pwd)) {
956             # If the patron password was provided, report on if
957             # it was right.
958             $resp .= add_field(FID_VALID_PATRON_PWD,
959                                sipbool($patron->check_password($patron_pwd)));
960         }
961
962         # SIP 2.0 extensions used by Envisionware
963         # Other types of terminals will ignore the fields, if
964         # they don't recognize the codes
965         $resp .= maybe_add(FID_PATRON_BIRTHDATE, $patron->sip_birthdate);
966         $resp .= maybe_add(FID_PATRON_CLASS, $patron->ptype);
967
968         # Custom protocol extension to report patron internet privileges
969         $resp .= maybe_add(FID_INET_PROFILE, $patron->inet_privileges);
970
971         $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
972         $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
973     } else {
974         # Invalid patron ID
975         # He has no privileges, no items associated with him,
976         # no personal name, and is invalid (if we're using 2.00)
977         $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
978         $resp .= '0000' x 6;
979         $resp .= add_field(FID_PERSONAL_NAME, '');
980
981         # the patron ID is invalid, but it's a required field, so
982         # just echo it back
983         $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
984
985         if ($protocol_version >= 2) {
986             $resp .= add_field(FID_VALID_PATRON, 'N');
987         }
988     }
989
990     $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
991
992     $self->write_msg($resp);
993
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,
1545            $patron->charge_ok);
1546     $patron_status = sprintf('%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     return $patron_status;
1562 }
1563
1564 sub api_auth($$) {
1565         # AUTH
1566         my ($username,$password) = (shift,shift);
1567         $ENV{REMOTE_USER} = $username;
1568         my $query = CGI->new();
1569         $query->param(userid   => $username);
1570         $query->param(password => $password);
1571         my ($status, $cookie, $sessionID) = check_api_auth($query, {circulate=>1}, "intranet");
1572         print STDERR "check_api_auth returns " . ($status || 'undef') . "\n";
1573         # print "api_auth userenv = " . &dump_userenv;
1574         return $status;
1575 }
1576
1577 1;
1578 __END__
1579