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