Merge remote-tracking branch 'origin/new/bug_7818'
[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 = 3.07.00.049;
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 => "A18A2A2A3",
180                             template_len => 25,
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;
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;
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;
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_media);
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     if ($item->due_date) {
548         $resp .= add_field(FID_DUE_DATE, Sip::timestamp($item->due_date));
549     } else {
550         $resp .= add_field(FID_DUE_DATE, q{});
551     }
552
553         $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
554         $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
555
556         if ($protocol_version >= 2) {
557             if ($ils->supports('security inhibit')) {
558                 $resp .= add_field(FID_SECURITY_INHIBIT,
559                                    $status->security_inhibit);
560             }
561             $resp .= maybe_add(FID_MEDIA_TYPE, $item->sip_media_type);
562             $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
563
564             # Financials
565             if ($status->fee_amount) {
566                 $resp .= add_field(FID_FEE_AMT, $status->fee_amount);
567                 $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
568                 $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
569                 $resp .= maybe_add(FID_TRANSACTION_ID,
570                                    $status->transaction_id);
571             }
572         }
573
574     } else {
575         # Checkout failed
576         # Checkout Response: not ok, no renewal, don't know mag. media,
577         # no desensitize
578         $resp = sprintf("120NUN%s", Sip::timestamp);
579         $resp .= add_field(FID_INST_ID, $inst);
580         $resp .= add_field(FID_PATRON_ID, $patron_id);
581         $resp .= add_field(FID_ITEM_ID, $item_id);
582
583         # If the item is valid, provide the title, otherwise
584         # leave it blank
585         $resp .= add_field(FID_TITLE_ID, $item ? $item->title_id : '');
586         # Due date is required.  Since it didn't get checked out,
587         # it's not due, so leave the date blank
588         $resp .= add_field(FID_DUE_DATE, '');
589
590         $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
591         $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
592
593         if ($protocol_version >= 2) {
594             # Is the patron ID valid?
595             $resp .= add_field(FID_VALID_PATRON, sipbool($patron));
596
597             if ($patron && exists($fields->{FID_PATRON_PWD})) {
598                 # Password provided, so we can tell if it was valid or not
599                 $resp .= add_field(FID_VALID_PATRON_PWD,
600                                    sipbool($patron->check_password($fields->{(FID_PATRON_PWD)})));
601             }
602         }
603     }
604
605     $self->write_msg($resp);
606     return(CHECKOUT);
607 }
608
609 sub handle_checkin {
610     my ($self, $server) = @_;
611     my $account = $server->{account};
612     my $ils     = $server->{ils};
613     my $my_branch = $ils->institution;
614     my ($current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel);
615     my ($patron, $item, $status);
616     my $resp = CHECKIN_RESP;
617     my ($no_block, $trans_date, $return_date) = @{$self->{fixed_fields}};
618         my $fields = $self->{fields};
619
620         $current_loc = $fields->{(FID_CURRENT_LOCN)};
621         $inst_id     = $fields->{(FID_INST_ID)};
622         $item_id     = $fields->{(FID_ITEM_ID)};
623         $item_props  = $fields->{(FID_ITEM_PROPS)};
624         $cancel      = $fields->{(FID_CANCEL)};
625     if ($current_loc) {
626         $my_branch = $current_loc;# most scm do not set $current_loc
627     }
628
629     $ils->check_inst_id($inst_id, "handle_checkin");
630
631     if ($no_block eq 'Y') {
632         # Off-line transactions, ick.
633         syslog("LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id});
634         $status = $ils->checkin_no_block($item_id, $trans_date, $return_date, $item_props, $cancel);
635     } else {
636         $status = $ils->checkin($item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel);
637     }
638
639     $patron = $status->patron;
640     $item   = $status->item;
641
642     $resp .= $status->ok ? '1' : '0';
643     $resp .= $status->resensitize ? 'Y' : 'N';
644     if ($item && $ils->supports('magnetic media')) {
645                 $resp .= sipbool($item->magnetic_media);
646     } else {
647         # item barcode is invalid or system doesn't support 'magnetic media' indicator
648                 $resp .= 'U';
649     }
650
651     # apparently we can't trust the returns from Checkin yet (because C4::Circulation::AddReturn is faulty)
652     # So we reproduce the alert logic here.
653     if (not $status->alert) {
654         if ($item->destination_loc and $item->destination_loc ne $my_branch) {
655             $status->alert(1);
656             $status->alert_type('04');  # no hold, just send it
657         }
658     }
659     $resp .= $status->alert ? 'Y' : 'N';
660     $resp .= Sip::timestamp;
661     $resp .= add_field(FID_INST_ID, $inst_id);
662     $resp .= add_field(FID_ITEM_ID, $item_id);
663
664     if ($item) {
665         $resp .= add_field(FID_PERM_LOCN, $item->permanent_location);
666         $resp .= maybe_add(FID_TITLE_ID,  $item->title_id);
667     }
668
669     if ($protocol_version >= 2) {
670         $resp .= maybe_add(FID_SORT_BIN, $status->sort_bin);
671         if ($patron) {
672             $resp .= add_field(FID_PATRON_ID, $patron->id);
673         }
674         if ($item) {
675             $resp .= maybe_add(FID_MEDIA_TYPE,           $item->sip_media_type     );
676             $resp .= maybe_add(FID_ITEM_PROPS,           $item->sip_item_properties);
677             $resp .= maybe_add(FID_COLLECTION_CODE,      $item->collection_code    );
678             $resp .= maybe_add(FID_CALL_NUMBER,          $item->call_number        );
679             $resp .= maybe_add(FID_DESTINATION_LOCATION, $item->destination_loc    );
680             $resp .= maybe_add(FID_HOLD_PATRON_ID,       $item->hold_patron_bcode     );
681             $resp .= maybe_add(FID_HOLD_PATRON_NAME,     $item->hold_patron_name   );
682             if ($status->hold and $status->hold->{branchcode} ne $item->destination_loc) {
683                 warn 'SIP hold mismatch: $status->hold->{branchcode}=' . $status->hold->{branchcode} . '; $item->destination_loc=' . $item->destination_loc;
684                 # just me being paranoid.
685             }
686         }
687     }
688
689     $resp .= maybe_add(FID_ALERT_TYPE, $status->alert_type) if $status->alert;
690     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
691     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
692
693     $self->write_msg($resp);
694
695     return(CHECKIN);
696 }
697
698 sub handle_block_patron {
699     my ($self, $server) = @_;
700     my $account = $server->{account};
701     my $ils = $server->{ils};
702     my ($card_retained, $trans_date);
703     my ($inst_id, $blocked_card_msg, $patron_id, $terminal_pwd);
704     my ($fields,$resp,$patron);
705
706     ($card_retained, $trans_date) = @{$self->{fixed_fields}};
707     $fields = $self->{fields};
708     $inst_id          = $fields->{(FID_INST_ID)};
709     $blocked_card_msg = $fields->{(FID_BLOCKED_CARD_MSG)};
710     $patron_id        = $fields->{(FID_PATRON_ID)};
711     $terminal_pwd     = $fields->{(FID_TERMINAL_PWD)};
712
713     # Terminal passwords are different from account login
714     # passwords, but I have no idea what to do with them.  So,
715     # I'll just ignore them for now.
716         
717         # FIXME ???
718
719     $ils->check_inst_id($inst_id, "block_patron");
720     $patron = $ils->find_patron($patron_id);
721
722     # The correct response for a "Block Patron" message is a
723     # "Patron Status Response", so use that handler to generate
724     # the message, but then return the correct code from here.
725     #
726     # Normally, the language is provided by the "Patron Status"
727     # fixed field, but since we're not responding to one of those
728     # we'll just say, "Unspecified", as per the spec.  Let the
729     # terminal default to something that, one hopes, will be
730     # intelligible
731         if ($patron) {
732                 # Valid patron id
733                 $patron->block($card_retained, $blocked_card_msg);
734         }
735
736     $resp = build_patron_status($patron, $patron->language, $fields);
737     $self->write_msg($resp);
738     return(BLOCK_PATRON);
739 }
740
741 sub handle_sc_status {
742     my ($self, $server) = @_;
743         ($server) or warn "handle_sc_status error: no \$server argument received.";
744         my ($status, $print_width, $sc_protocol_version) = @{$self->{fixed_fields}};
745         my ($new_proto);
746
747         if ($sc_protocol_version =~ /^1\./) {
748                 $new_proto = 1;
749         } elsif ($sc_protocol_version =~ /^2\./) {
750                 $new_proto = 2;
751         } else {
752                 syslog("LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version);
753                 $new_proto = 1;
754         }
755
756         if ($new_proto != $protocol_version) {
757                 syslog("LOG_INFO", "Setting protocol level to $new_proto");
758                 $protocol_version = $new_proto;
759         }
760
761     if ($status == SC_STATUS_PAPER) {
762         syslog("LOG_WARNING", "Self-Check unit '%s@%s' out of paper",
763                $self->{account}->{id}, $self->{account}->{institution});
764     } elsif ($status == SC_STATUS_SHUTDOWN) {
765         syslog("LOG_WARNING", "Self-Check unit '%s@%s' shutting down",
766                $self->{account}->{id}, $self->{account}->{institution});
767     }
768
769     $self->{account}->{print_width} = $print_width;
770     return (send_acs_status($self, $server) ? SC_STATUS : '');
771 }
772
773 sub handle_request_acs_resend {
774     my ($self, $server) = @_;
775
776     if (!$last_response) {
777         # We haven't sent anything yet, so respond with a
778         # REQUEST_SC_RESEND msg (p. 16)
779         $self->write_msg(REQUEST_SC_RESEND);
780     } elsif ((length($last_response) < 9)
781              || substr($last_response, -9, 2) ne 'AY') {
782         # When resending a message, we aren't supposed to include
783         # a sequence number, even if the original had one (p. 4).
784         # If the last message didn't have a sequence number, then
785         # we can just send it.
786         print("$last_response\r");      # not write_msg?
787     } else {
788         # Cut out the sequence number and checksum, since the old
789         # checksum is wrong for the resent message.
790         my $rebuilt = substr($last_response, 0, -9);
791         $self->write_msg($rebuilt);
792     }
793
794     return REQUEST_ACS_RESEND;
795 }
796
797 sub login_core  {
798     my $server = shift or return;
799         my $uid = shift;
800         my $pwd = shift;
801     my $status = 1;             # Assume it all works
802     if (!exists($server->{config}->{accounts}->{$uid})) {
803                 syslog("LOG_WARNING", "MsgType::login_core: Unknown login '$uid'");
804                 $status = 0;
805     } elsif ($server->{config}->{accounts}->{$uid}->{password} ne $pwd) {
806                 syslog("LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'");
807                 $status = 0;
808     } else {
809         # Store the active account someplace handy for everybody else to find.
810                 $server->{account} = $server->{config}->{accounts}->{$uid};
811                 my $inst = $server->{account}->{institution};
812                 $server->{institution} = $server->{config}->{institutions}->{$inst};
813                 $server->{policy} = $server->{institution}->{policy};
814                 $server->{sip_username} = $uid;
815                 $server->{sip_password} = $pwd;
816
817         my $auth_status = api_auth($uid,$pwd,$inst);
818                 if (!$auth_status or $auth_status !~ /^ok$/i) {
819                         syslog("LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s",
820                                                 $uid, $inst, ($auth_status||'unknown'));
821                         $status = 0;
822                 } else {
823                         syslog("LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst);
824                         #
825                         # initialize connection to ILS
826                         #
827                         my $module = $server->{config}->{institutions}->{$inst}->{implementation};
828                         syslog("LOG_DEBUG", 'login_core: ' . Dumper($module));
829                         $module->use;
830                         if ($@) {
831                                 syslog("LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed",
832                                                 $server->{service}, $module, $inst);
833                                 die("Failed to load ILS implementation '$module' for $inst");
834                         }
835
836                         # like   ILS->new(), I think.
837                         $server->{ils} = $module->new($server->{institution}, $server->{account});
838                         if (!$server->{ils}) {
839                             syslog("LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst);
840                             die("Unable to connect to ILS '$inst'");
841                         }
842                 }
843         }
844         return $status;
845 }
846
847 sub handle_login {
848     my ($self, $server) = @_;
849     my ($uid_algorithm, $pwd_algorithm);
850     my ($uid, $pwd);
851     my $inst;
852     my $fields;
853     my $status = 1;             # Assume it all works
854
855     $fields = $self->{fields};
856     ($uid_algorithm, $pwd_algorithm) = @{$self->{fixed_fields}};
857
858     $uid = $fields->{(FID_LOGIN_UID)}; # Terminal ID, not patron ID.
859     $pwd = $fields->{(FID_LOGIN_PWD)}; # Terminal PWD, not patron PWD.
860
861     if ($uid_algorithm || $pwd_algorithm) {
862                 syslog("LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm");
863                 $status = 0;
864     }
865         else { $status = login_core($server,$uid,$pwd); }
866
867         $self->write_msg(LOGIN_RESP . $status);
868     return $status ? LOGIN : '';
869 }
870
871 #
872 # Build the detailed summary information for the Patron
873 # Information Response message based on the first 'Y' that appears
874 # in the 'summary' field of the Patron Information reqest.  The
875 # specification says that only one 'Y' can appear in that field,
876 # and we're going to believe it.
877 #
878 sub summary_info {
879     my ($ils, $patron, $summary, $start, $end) = @_;
880     my $resp = '';
881     my $summary_type;
882     #
883     # Map from offsets in the "summary" field of the Patron Information
884     # message to the corresponding field and handler
885     #
886     my @summary_map = (
887         { func => $patron->can(   "hold_items"), fid => FID_HOLD_ITEMS             },
888         { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS          },
889         { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS          },
890         { func => $patron->can(   "fine_items"), fid => FID_FINE_ITEMS             },
891         { func => $patron->can( "recall_items"), fid => FID_RECALL_ITEMS           },
892         { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
893     );
894
895     if (($summary_type = index($summary, 'Y')) == -1) {
896         return '';  # No detailed information required
897     }
898
899     syslog("LOG_DEBUG", "Summary_info: index == '%d', field '%s'",
900         $summary_type, $summary_map[$summary_type]->{fid});
901
902     my $func = $summary_map[$summary_type]->{func};
903     my $fid  = $summary_map[$summary_type]->{fid};
904     my $itemlist = &$func($patron, $start, $end);
905
906     syslog("LOG_DEBUG", "summary_info: list = (%s)", join(", ", @{$itemlist}));
907     foreach my $i (@{$itemlist}) {
908         $resp .= add_field($fid, $i->{barcode});
909     }
910
911     return $resp;
912 }
913
914 sub handle_patron_info {
915     my ($self, $server) = @_;
916     my $ils = $server->{ils};
917     my ($lang, $trans_date, $summary) = @{$self->{fixed_fields}};
918     my $fields = $self->{fields};
919     my ($inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end);
920     my ($resp, $patron, $count);
921
922     $inst_id      = $fields->{(FID_INST_ID)};
923     $patron_id    = $fields->{(FID_PATRON_ID)};
924     $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
925     $patron_pwd   = $fields->{(FID_PATRON_PWD)};
926     $start        = $fields->{(FID_START_ITEM)};
927     $end          = $fields->{(FID_END_ITEM)};
928
929     $patron = $ils->find_patron($patron_id);
930
931     $resp = (PATRON_INFO_RESP);
932     if ($patron) {
933         $resp .= patron_status_string($patron);
934         $resp .= (defined($lang) and length($lang) ==3) ? $lang : $patron->language;
935         $resp .= Sip::timestamp();
936
937         $resp .= add_count('patron_info/hold_items',
938             scalar @{$patron->hold_items});
939         $resp .= add_count('patron_info/overdue_items',
940             scalar @{$patron->overdue_items});
941         $resp .= add_count('patron_info/charged_items',
942             scalar @{$patron->charged_items});
943         $resp .= add_count('patron_info/fine_items',
944             scalar @{$patron->fine_items});
945         $resp .= add_count('patron_info/recall_items',
946             scalar @{$patron->recall_items});
947         $resp .= add_count('patron_info/unavail_holds',
948             scalar @{$patron->unavail_holds});
949
950         $resp .= add_field(FID_INST_ID,       ($ils->institution_id || 'SIP2'));
951
952         # while the patron ID we got from the SC is valid, let's
953         # use the one returned from the ILS, just in case...
954         $resp .= add_field(FID_PATRON_ID,     $patron->id);
955         $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
956
957         # TODO: add code for the fields
958         #   hold items limit
959         #   overdue items limit
960         #   charged items limit
961
962         $resp .= add_field(FID_VALID_PATRON, 'Y');
963         if (defined($patron_pwd)) {
964             # If patron password was provided, report whether it was right or not.
965             $resp .= add_field(FID_VALID_PATRON_PWD,
966                 sipbool($patron->check_password($patron_pwd)));
967         }
968
969         $resp .= maybe_add(FID_CURRENCY,   $patron->currency);
970         $resp .= maybe_add(FID_FEE_AMT,    $patron->fee_amount);
971         $resp .= add_field(FID_FEE_LMT,    $patron->fee_limit);
972
973         # TODO: zero or more item details for 2.0 can go here:
974         #          hold_items
975         #       overdue_items
976         #       charged_items
977         #          fine_items
978         #        recall_items
979
980         $resp .= summary_info($ils, $patron, $summary, $start, $end);
981
982         $resp .= maybe_add(FID_HOME_ADDR,  $patron->address);
983         $resp .= maybe_add(FID_EMAIL,      $patron->email_addr);
984         $resp .= maybe_add(FID_HOME_PHONE, $patron->home_phone);
985
986         # SIP 2.0 extensions used by Envisionware
987         # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
988         $resp .= maybe_add(FID_PATRON_BIRTHDATE, $patron->birthdate);
989         $resp .= maybe_add(FID_PATRON_CLASS,     $patron->ptype);
990
991         # Custom protocol extension to report patron internet privileges
992         $resp .= maybe_add(FID_INET_PROFILE,     $patron->inet_privileges);
993
994         $resp .= maybe_add(FID_SCREEN_MSG,       $patron->screen_msg);
995         $resp .= maybe_add(FID_PRINT_LINE,       $patron->print_line);
996     } else {
997         # Invalid patron ID:
998         # no privileges, no items associated,
999         # no personal name, and is invalid (if we're using 2.00)
1000         $resp .= 'YYYY' . (' ' x 10) . $lang . Sip::timestamp();
1001         $resp .= '0000' x 6;
1002
1003         $resp .= add_field(FID_INST_ID,       ($ils->institution_id || 'SIP2'));
1004         # patron ID is invalid, but field is required, so just echo it back
1005         $resp .= add_field(FID_PATRON_ID,     $fields->{(FID_PATRON_ID)});
1006         $resp .= add_field(FID_PERSONAL_NAME, '');
1007
1008         if ($protocol_version >= 2) {
1009             $resp .= add_field(FID_VALID_PATRON, 'N');
1010         }
1011     }
1012
1013     $self->write_msg($resp);
1014     return(PATRON_INFO);
1015 }
1016
1017 sub handle_end_patron_session {
1018     my ($self, $server) = @_;
1019     my $ils = $server->{ils};
1020     my $trans_date;
1021     my $fields = $self->{fields};
1022     my $resp = END_SESSION_RESP;
1023     my ($status, $screen_msg, $print_line);
1024
1025     ($trans_date) = @{$self->{fixed_fields}};
1026
1027     $ils->check_inst_id($fields->{(FID_INST_ID)}, 'handle_end_patron_session');
1028
1029     ($status, $screen_msg, $print_line) = $ils->end_patron_session($fields->{(FID_PATRON_ID)});
1030
1031     $resp .= $status ? 'Y' : 'N';
1032     $resp .= Sip::timestamp();
1033
1034     $resp .= add_field(FID_INST_ID, $server->{ils}->institution);
1035     $resp .= add_field(FID_PATRON_ID, $fields->{(FID_PATRON_ID)});
1036
1037     $resp .= maybe_add(FID_SCREEN_MSG, $screen_msg);
1038     $resp .= maybe_add(FID_PRINT_LINE, $print_line);
1039
1040     $self->write_msg($resp);
1041
1042     return(END_PATRON_SESSION);
1043 }
1044
1045 sub handle_fee_paid {
1046     my ($self, $server) = @_;
1047     my $ils = $server->{ils};
1048     my ($trans_date, $fee_type, $pay_type, $currency) = $self->{fixed_fields};
1049     my $fields = $self->{fields};
1050     my ($fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd);
1051     my ($fee_id, $trans_id);
1052     my $status;
1053     my $resp = FEE_PAID_RESP;
1054
1055     $fee_amt = $fields->{(FID_FEE_AMT)};
1056     $inst_id = $fields->{(FID_INST_ID)};
1057     $patron_id = $fields->{(FID_PATRON_ID)};
1058     $patron_pwd = $fields->{(FID_PATRON_PWD)};
1059     $fee_id = $fields->{(FID_FEE_ID)};
1060     $trans_id = $fields->{(FID_TRANSACTION_ID)};
1061
1062     $ils->check_inst_id($inst_id, "handle_fee_paid");
1063
1064     $status = $ils->pay_fee($patron_id, $patron_pwd, $fee_amt, $fee_type,
1065                            $pay_type, $fee_id, $trans_id, $currency);
1066
1067     $resp .= ($status->ok ? 'Y' : 'N') . Sip::timestamp;
1068     $resp .= add_field(FID_INST_ID, $inst_id);
1069     $resp .= add_field(FID_PATRON_ID, $patron_id);
1070     $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
1071     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1072     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1073
1074     $self->write_msg($resp);
1075
1076     return(FEE_PAID);
1077 }
1078
1079 sub handle_item_information {
1080     my ($self, $server) = @_;
1081     my $ils = $server->{ils};
1082     my $trans_date;
1083     my $fields = $self->{fields};
1084     my $resp = ITEM_INFO_RESP;
1085     my $item;
1086     my $i;
1087
1088     ($trans_date) = @{$self->{fixed_fields}};
1089
1090     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_item_information");
1091
1092     $item =  $ils->find_item($fields->{(FID_ITEM_ID)});
1093
1094     if (!defined($item)) {
1095         # Invalid Item ID
1096         # "Other" circ stat, "Other" security marker, "Unknown" fee type
1097         $resp .= "010101";
1098         $resp .= Sip::timestamp;
1099         # Just echo back the invalid item id
1100         $resp .= add_field(FID_ITEM_ID, $fields->{(FID_ITEM_ID)});
1101         # title id is required, but we don't have one
1102         $resp .= add_field(FID_TITLE_ID, '');
1103     } else {
1104         # Valid Item ID, send the good stuff
1105         $resp .= $item->sip_circulation_status;
1106         $resp .= $item->sip_security_marker;
1107         $resp .= $item->sip_fee_type;
1108         $resp .= Sip::timestamp;
1109
1110         $resp .= add_field(FID_ITEM_ID,  $item->id);
1111         $resp .= add_field(FID_TITLE_ID, $item->title_id);
1112
1113         $resp .= maybe_add(FID_MEDIA_TYPE,   $item->sip_media_type);
1114         $resp .= maybe_add(FID_PERM_LOCN,    $item->permanent_location);
1115         $resp .= maybe_add(FID_CURRENT_LOCN, $item->current_location);
1116         $resp .= maybe_add(FID_ITEM_PROPS,   $item->sip_item_properties);
1117
1118         if (($i = $item->fee) != 0) {
1119             $resp .= add_field(FID_CURRENCY, $item->fee_currency);
1120             $resp .= add_field(FID_FEE_AMT, $i);
1121         }
1122         $resp .= maybe_add(FID_OWNER, $item->owner);
1123
1124         if (($i = scalar @{$item->hold_queue}) > 0) {
1125             $resp .= add_field(FID_HOLD_QUEUE_LEN, $i);
1126         }
1127         if ($item->due_date) {
1128             $resp .= add_field(FID_DUE_DATE, Sip::timestamp($item->due_date));
1129         }
1130         if (($i = $item->recall_date) != 0) {
1131             $resp .= add_field(FID_RECALL_DATE, Sip::timestamp($i));
1132         }
1133         if (($i = $item->hold_pickup_date) != 0) {
1134             $resp .= add_field(FID_HOLD_PICKUP_DATE, Sip::timestamp($i));
1135         }
1136
1137         $resp .= maybe_add(FID_SCREEN_MSG, $item->screen_msg);
1138         $resp .= maybe_add(FID_PRINT_LINE, $item->print_line);
1139     }
1140
1141     $self->write_msg($resp);
1142
1143     return(ITEM_INFORMATION);
1144 }
1145
1146 sub handle_item_status_update {
1147     my ($self, $server) = @_;
1148     my $ils = $server->{ils};
1149     my ($trans_date, $item_id, $terminal_pwd, $item_props);
1150     my $fields = $self->{fields};
1151     my $status;
1152     my $item;
1153     my $resp = ITEM_STATUS_UPDATE_RESP;
1154
1155     ($trans_date) = @{$self->{fixed_fields}};
1156
1157     $ils->check_inst_id($fields->{(FID_INST_ID)});
1158
1159     $item_id = $fields->{(FID_ITEM_ID)};
1160     $item_props = $fields->{(FID_ITEM_PROPS)};
1161
1162         if (!defined($item_id)) {
1163                 syslog("LOG_WARNING",
1164                         "handle_item_status: received message without Item ID field");
1165     } else {
1166                 $item = $ils->find_item($item_id);
1167         }
1168
1169     if (!$item) {
1170         # Invalid Item ID
1171         $resp .= '0';
1172         $resp .= Sip::timestamp;
1173         $resp .= add_field(FID_ITEM_ID, $item_id);
1174     } else {
1175         # Valid Item ID
1176
1177         $status = $item->status_update($item_props);
1178
1179         $resp .= $status->ok ? '1' : '0';
1180         $resp .= Sip::timestamp;
1181
1182         $resp .= add_field(FID_ITEM_ID, $item->id);
1183         $resp .= add_field(FID_TITLE_ID, $item->title_id);
1184         $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
1185     }
1186
1187     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1188     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1189
1190     $self->write_msg($resp);
1191
1192     return(ITEM_STATUS_UPDATE);
1193 }
1194
1195 sub handle_patron_enable {
1196     my ($self, $server) = @_;
1197     my $ils = $server->{ils};
1198     my $fields = $self->{fields};
1199     my ($trans_date, $patron_id, $terminal_pwd, $patron_pwd);
1200     my ($status, $patron);
1201     my $resp = PATRON_ENABLE_RESP;
1202
1203     ($trans_date) = @{$self->{fixed_fields}};
1204     $patron_id = $fields->{(FID_PATRON_ID)};
1205     $patron_pwd = $fields->{(FID_PATRON_PWD)};
1206
1207     syslog("LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'",
1208            $patron_id, $patron_pwd);
1209
1210     $patron = $ils->find_patron($patron_id);
1211
1212     if (!defined($patron)) {
1213         # Invalid patron ID
1214         $resp .= 'YYYY' . (' ' x 10) . '000' . Sip::timestamp();
1215         $resp .= add_field(FID_PATRON_ID, $patron_id);
1216         $resp .= add_field(FID_PERSONAL_NAME, '');
1217         $resp .= add_field(FID_VALID_PATRON, 'N');
1218         $resp .= add_field(FID_VALID_PATRON_PWD, 'N');
1219     } else {
1220         # valid patron
1221         if (!defined($patron_pwd) || $patron->check_password($patron_pwd)) {
1222             # Don't enable the patron if there was an invalid password
1223             $status = $patron->enable;
1224         }
1225         $resp .= patron_status_string($patron);
1226         $resp .= $patron->language . Sip::timestamp();
1227
1228         $resp .= add_field(FID_PATRON_ID, $patron->id);
1229         $resp .= add_field(FID_PERSONAL_NAME, $patron->name);
1230         if (defined($patron_pwd)) {
1231             $resp .= add_field(FID_VALID_PATRON_PWD,
1232                                sipbool($patron->check_password($patron_pwd)));
1233         }
1234         $resp .= add_field(FID_VALID_PATRON, 'Y');
1235         $resp .= maybe_add(FID_SCREEN_MSG, $patron->screen_msg);
1236         $resp .= maybe_add(FID_PRINT_LINE, $patron->print_line);
1237     }
1238
1239     $resp .= add_field(FID_INST_ID, $ils->institution);
1240
1241     $self->write_msg($resp);
1242
1243     return(PATRON_ENABLE);
1244 }
1245
1246 sub handle_hold {
1247     my ($self, $server) = @_;
1248     my $ils = $server->{ils};
1249     my ($hold_mode, $trans_date);
1250     my ($expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd);
1251     my ($item_id, $title_id, $fee_ack);
1252     my $fields = $self->{fields};
1253     my $status;
1254     my $resp = HOLD_RESP;
1255
1256     ($hold_mode, $trans_date) = @{$self->{fixed_fields}};
1257
1258     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_hold");
1259
1260     $patron_id   = $fields->{(FID_PATRON_ID)  };
1261     $expiry_date = $fields->{(FID_EXPIRATION) } || '';
1262     $pickup_locn = $fields->{(FID_PICKUP_LOCN)} || '';
1263     $hold_type   = $fields->{(FID_HOLD_TYPE)  } || '2'; # Any copy of title
1264     $patron_pwd  = $fields->{(FID_PATRON_PWD) };
1265     $item_id     = $fields->{(FID_ITEM_ID)    } || '';
1266     $title_id    = $fields->{(FID_TITLE_ID)   } || '';
1267     $fee_ack     = $fields->{(FID_FEE_ACK)    } || 'N';
1268
1269     if ($hold_mode eq '+') {
1270         $status = $ils->add_hold($patron_id, $patron_pwd, $item_id, $title_id,
1271                                                 $expiry_date, $pickup_locn, $hold_type, $fee_ack);
1272     } elsif ($hold_mode eq '-') {
1273         $status = $ils->cancel_hold($patron_id, $patron_pwd, $item_id, $title_id);
1274     } elsif ($hold_mode eq '*') {
1275         $status = $ils->alter_hold($patron_id, $patron_pwd, $item_id, $title_id,
1276                                                 $expiry_date, $pickup_locn, $hold_type, $fee_ack);
1277     } else {
1278         syslog("LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'",
1279                $hold_mode, $server->{account}->{id});
1280         $status = $ils->Transaction::Hold;              # new?
1281         $status->screen_msg("System error. Please contact library staff.");
1282     }
1283
1284     $resp .= $status->ok;
1285     $resp .= sipbool($status->item  &&  $status->item->available($patron_id));
1286     $resp .= Sip::timestamp;
1287
1288     if ($status->ok) {
1289         $resp .= add_field(FID_PATRON_ID,   $status->patron->id);
1290
1291         ($status->expiration_date) and
1292         $resp .= maybe_add(FID_EXPIRATION,
1293                                      Sip::timestamp($status->expiration_date));
1294         $resp .= maybe_add(FID_QUEUE_POS,   $status->queue_position);
1295         $resp .= maybe_add(FID_PICKUP_LOCN, $status->pickup_location);
1296         $resp .= maybe_add(FID_ITEM_ID,     $status->item->id);
1297         $resp .= maybe_add(FID_TITLE_ID,    $status->item->title_id);
1298     } else {
1299         # Not ok.  still need required fields
1300         $resp .= add_field(FID_PATRON_ID,   $patron_id);
1301     }
1302
1303     $resp .= add_field(FID_INST_ID,     $ils->institution);
1304     $resp .= maybe_add(FID_SCREEN_MSG,  $status->screen_msg);
1305     $resp .= maybe_add(FID_PRINT_LINE,  $status->print_line);
1306
1307     $self->write_msg($resp);
1308
1309     return(HOLD);
1310 }
1311
1312 sub handle_renew {
1313     my ($self, $server) = @_;
1314     my $ils = $server->{ils};
1315     my ($third_party, $no_block, $trans_date, $nb_due_date);
1316     my ($patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack);
1317     my $fields = $self->{fields};
1318     my $status;
1319     my ($patron, $item);
1320     my $resp = RENEW_RESP;
1321
1322     ($third_party, $no_block, $trans_date, $nb_due_date) =
1323         @{$self->{fixed_fields}};
1324
1325     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew");
1326
1327     if ($no_block eq 'Y') {
1328         syslog("LOG_WARNING",
1329                "handle_renew: recieved 'no block' renewal from terminal '%s'",
1330                $server->{account}->{id});
1331     }
1332
1333     $patron_id  = $fields->{(FID_PATRON_ID)};
1334     $patron_pwd = $fields->{(FID_PATRON_PWD)};
1335     $item_id    = $fields->{(FID_ITEM_ID)};
1336     $title_id   = $fields->{(FID_TITLE_ID)};
1337     $item_props = $fields->{(FID_ITEM_PROPS)};
1338     $fee_ack    = $fields->{(FID_FEE_ACK)};
1339
1340     $status = $ils->renew($patron_id, $patron_pwd, $item_id, $title_id,
1341                           $no_block, $nb_due_date, $third_party,
1342                           $item_props, $fee_ack);
1343
1344     $patron = $status->patron;
1345     $item   = $status->item;
1346
1347     if ($status->ok) {
1348         $resp .= '1';
1349         $resp .= $status->renewal_ok ? 'Y' : 'N';
1350         if ($ils->supports('magnetic media')) {
1351             $resp .= sipbool($item->magnetic_media);
1352         } else {
1353             $resp .= 'U';
1354         }
1355         $resp .= sipbool($status->desensitize);
1356         $resp .= Sip::timestamp;
1357         $resp .= add_field(FID_PATRON_ID, $patron->id);
1358         $resp .= add_field(FID_ITEM_ID,  $item->id);
1359         $resp .= add_field(FID_TITLE_ID, $item->title_id);
1360         $resp .= add_field(FID_DUE_DATE, Sip::timestamp($item->due_date));
1361         if ($ils->supports('security inhibit')) {
1362             $resp .= add_field(FID_SECURITY_INHIBIT,
1363                                $status->security_inhibit);
1364         }
1365         $resp .= add_field(FID_MEDIA_TYPE, $item->sip_media_type);
1366         $resp .= maybe_add(FID_ITEM_PROPS, $item->sip_item_properties);
1367     } else {
1368         # renew failed for some reason
1369         # not OK, renewal not OK, Unknown media type (why bother checking?)
1370         $resp .= '0NUN';
1371         $resp .= Sip::timestamp;
1372         # If we found the patron or the item, the return the ILS
1373         # information, otherwise echo back the infomation we received
1374         # from the terminal
1375         $resp .= add_field(FID_PATRON_ID, $patron ? $patron->id     : $patron_id);
1376         $resp .= add_field(FID_ITEM_ID,     $item ? $item->id       : $item_id  );
1377         $resp .= add_field(FID_TITLE_ID,    $item ? $item->title_id : $title_id );
1378         $resp .= add_field(FID_DUE_DATE, '');
1379     }
1380
1381     if ($status->fee_amount) {
1382         $resp .= add_field(FID_FEE_AMT,  $status->fee_amount);
1383         $resp .= maybe_add(FID_CURRENCY, $status->sip_currency);
1384         $resp .= maybe_add(FID_FEE_TYPE, $status->sip_fee_type);
1385         $resp .= maybe_add(FID_TRANSACTION_ID, $status->transaction_id);
1386     }
1387
1388     $resp .= add_field(FID_INST_ID, $ils->institution);
1389     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1390     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1391
1392     $self->write_msg($resp);
1393
1394     return(RENEW);
1395 }
1396
1397 sub handle_renew_all {
1398     # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1399
1400     my ($self, $server) = @_;
1401     my $ils = $server->{ils};
1402     my ($trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack);
1403     my $fields = $self->{fields};
1404     my $resp = RENEW_ALL_RESP;
1405     my $status;
1406     my (@renewed, @unrenewed);
1407
1408     $ils->check_inst_id($fields->{(FID_INST_ID)}, "handle_renew_all");
1409
1410     ($trans_date) = @{$self->{fixed_fields}};
1411
1412     $patron_id    = $fields->{(FID_PATRON_ID)};
1413     $patron_pwd   = $fields->{(FID_PATRON_PWD)};
1414     $terminal_pwd = $fields->{(FID_TERMINAL_PWD)};
1415     $fee_ack      = $fields->{(FID_FEE_ACK)};
1416
1417     $status = $ils->renew_all($patron_id, $patron_pwd, $fee_ack);
1418
1419     $resp .= $status->ok ? '1' : '0';
1420
1421         if (!$status->ok) {
1422                 $resp .= add_count("renew_all/renewed_count"  , 0);
1423                 $resp .= add_count("renew_all/unrenewed_count", 0);
1424                 @renewed = ();
1425                 @unrenewed = ();
1426         } else {
1427                 @renewed   = (@{$status->renewed});
1428                 @unrenewed = (@{$status->unrenewed});
1429                 $resp .= add_count("renew_all/renewed_count"  , scalar @renewed  );
1430                 $resp .= add_count("renew_all/unrenewed_count", scalar @unrenewed);
1431         }
1432
1433     $resp .= Sip::timestamp;
1434     $resp .= add_field(FID_INST_ID, $ils->institution);
1435
1436     $resp .= join('', map(add_field(FID_RENEWED_ITEMS  , $_), @renewed  ));
1437     $resp .= join('', map(add_field(FID_UNRENEWED_ITEMS, $_), @unrenewed));
1438
1439     $resp .= maybe_add(FID_SCREEN_MSG, $status->screen_msg);
1440     $resp .= maybe_add(FID_PRINT_LINE, $status->print_line);
1441
1442     $self->write_msg($resp);
1443
1444     return(RENEW_ALL);
1445 }
1446
1447 #
1448 # send_acs_status($self, $server)
1449 #
1450 # Send an ACS Status message, which is contains lots of little fields
1451 # of information gleaned from all sorts of places.
1452 #
1453
1454 my @message_type_names = (
1455                           "patron status request",
1456                           "checkout",
1457                           "checkin",
1458                           "block patron",
1459                           "acs status",
1460                           "request sc/acs resend",
1461                           "login",
1462                           "patron information",
1463                           "end patron session",
1464                           "fee paid",
1465                           "item information",
1466                           "item status update",
1467                           "patron enable",
1468                           "hold",
1469                           "renew",
1470                           "renew all",
1471                          );
1472
1473 sub send_acs_status {
1474     my ($self, $server, $screen_msg, $print_line) = @_;
1475     my $msg = ACS_STATUS;
1476         ($server) or die "send_acs_status error: no \$server argument received";
1477     my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1478     my $policy  = $server->{policy}  or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1479     my $ils     = $server->{ils}     or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1480     my ($online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy);
1481     my ($status_update_ok, $offline_ok, $timeout, $retries);
1482
1483     $online_status = 'Y';
1484     $checkout_ok = sipbool($ils->checkout_ok);
1485     $checkin_ok  = sipbool($ils->checkin_ok);
1486     $ACS_renewal_policy = sipbool($policy->{renewal});
1487     $status_update_ok   = sipbool($ils->status_update_ok);
1488     $offline_ok = sipbool($ils->offline_ok);
1489     $timeout = sprintf("%03d", $policy->{timeout});
1490     $retries = sprintf("%03d", $policy->{retries});
1491
1492     if (length($timeout) != 3) {
1493         syslog("LOG_ERR", "handle_acs_status: timeout field wrong size: '%s'",
1494                $timeout);
1495         $timeout = '000';
1496     }
1497
1498     if (length($retries) != 3) {
1499         syslog("LOG_ERR", "handle_acs_status: retries field wrong size: '%s'",
1500                $retries);
1501         $retries = '000';
1502     }
1503
1504     $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1505     $msg .= "$status_update_ok$offline_ok$timeout$retries";
1506     $msg .= Sip::timestamp();
1507
1508     if ($protocol_version == 1) {
1509         $msg .= '1.00';
1510     } elsif ($protocol_version == 2) {
1511         $msg .= '2.00';
1512     } else {
1513         syslog("LOG_ERR",
1514                'Bad setting for $protocol_version, "%s" in send_acs_status',
1515                $protocol_version);
1516         $msg .= '1.00';
1517     }
1518
1519     # Institution ID
1520     $msg .= add_field(FID_INST_ID, $account->{institution});
1521
1522     if ($protocol_version >= 2) {
1523         # Supported messages: we do it all
1524         my $supported_msgs = '';
1525
1526         foreach my $msg_name (@message_type_names) {
1527             if ($msg_name eq 'request sc/acs resend') {
1528                 $supported_msgs .= Sip::sipbool(1);
1529             } else {
1530                 $supported_msgs .= Sip::sipbool($ils->supports($msg_name));
1531             }
1532         }
1533         if (length($supported_msgs) < 16) {
1534             syslog("LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs);
1535         }
1536         $msg .= add_field(FID_SUPPORTED_MSGS, $supported_msgs);
1537     }
1538
1539     $msg .= maybe_add(FID_SCREEN_MSG, $screen_msg);
1540
1541     if (defined($account->{print_width}) && defined($print_line)
1542         && $account->{print_width} < length($print_line)) {
1543         syslog("LOG_WARNING", "send_acs_status: print line '%s' too long.  Truncating",
1544                $print_line);
1545         $print_line = substr($print_line, 0, $account->{print_width});
1546     }
1547
1548     $msg .= maybe_add(FID_PRINT_LINE, $print_line);
1549
1550     # Do we want to tell the terminal its location?
1551
1552     $self->write_msg($msg);
1553     return 1;
1554 }
1555
1556 #
1557 # build_patron_status: create the 14-char patron status
1558 # string for the Patron Status message
1559 #
1560 sub patron_status_string {
1561     my $patron = shift;
1562     my $patron_status;
1563
1564     syslog("LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok);
1565     $patron_status = sprintf(
1566         '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1567         denied($patron->charge_ok),
1568         denied($patron->renew_ok),
1569         denied($patron->recall_ok),
1570         denied($patron->hold_ok),
1571         boolspace($patron->card_lost),
1572         boolspace($patron->too_many_charged),
1573         boolspace($patron->too_many_overdue),
1574         boolspace($patron->too_many_renewal),
1575         boolspace($patron->too_many_claim_return),
1576         boolspace($patron->too_many_lost),
1577         boolspace($patron->excessive_fines),
1578         boolspace($patron->excessive_fees),
1579         boolspace($patron->recall_overdue),
1580         boolspace($patron->too_many_billed)
1581     );
1582     return $patron_status;
1583 }
1584
1585 sub api_auth {
1586     my ($username,$password, $branch) = @_;
1587     $ENV{REMOTE_USER} = $username;
1588     my $query = CGI->new();
1589     $query->param(userid   => $username);
1590     $query->param(password => $password);
1591     if ($branch) {
1592         $query->param(branch => $branch);
1593     }
1594     my ($status, $cookie, $sessionID) = check_api_auth($query, {circulate=>1}, 'intranet');
1595     return $status;
1596 }
1597
1598 1;
1599 __END__
1600