Bug 16011: $VERSION - Remove the $VERSION init
[koha.git] / C4 / SIP / Sip / MsgType.pm
blob14d64d7a838706012f55b5aedf1a64e0ff2462df
2 # Sip::MsgType.pm
4 # A Class for handing SIP messages
7 package C4::SIP::Sip::MsgType;
9 use strict;
10 use warnings;
11 use Exporter;
12 use Sys::Syslog qw(syslog);
14 use C4::SIP::Sip qw(:all);
15 use C4::SIP::Sip::Constants qw(:all);
16 use C4::SIP::Sip::Checksum qw(verify_cksum);
18 use Data::Dumper;
19 use CGI qw ( -utf8 );
20 use C4::Auth qw(&check_api_auth);
22 use UNIVERSAL::can;
24 use vars qw(@ISA @EXPORT_OK);
26 use constant INVALID_CARD => 'Invalid cardnumber';
27 use constant INVALID_PW => 'Invalid password';
29 BEGIN {
30 @ISA = qw(Exporter);
31 @EXPORT_OK = qw(handle login_core);
34 # Predeclare handler subroutines
35 use subs qw(handle_patron_status handle_checkout handle_checkin
36 handle_block_patron handle_sc_status handle_request_acs_resend
37 handle_login handle_patron_info handle_end_patron_session
38 handle_fee_paid handle_item_information handle_item_status_update
39 handle_patron_enable handle_hold handle_renew handle_renew_all);
42 # For the most part, Version 2.00 of the protocol just adds new
43 # variable fields, but sometimes it changes the fixed header.
45 # In general, if there's no '2.00' protocol entry for a handler, that's
46 # because 2.00 didn't extend the 1.00 version of the protocol. This will
47 # be handled by the module initialization code following the declaration,
48 # which goes through the handlers table and creates a '2.00' entry that
49 # points to the same place as the '1.00' entry. If there's a 2.00 entry
50 # but no 1.00 entry, then that means that it's a completely new service
51 # in 2.00, so 1.00 shouldn't recognize it.
53 my %handlers = (
54 (PATRON_STATUS_REQ) => {
55 name => "Patron Status Request",
56 handler => \&handle_patron_status,
57 protocol => {
58 1 => {
59 template => "A3A18",
60 template_len => 21,
61 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
65 (CHECKOUT) => {
66 name => "Checkout",
67 handler => \&handle_checkout,
68 protocol => {
69 1 => {
70 template => "CCA18A18",
71 template_len => 38,
72 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
74 2 => {
75 template => "CCA18A18",
76 template_len => 38,
77 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_PATRON_PWD), (FID_FEE_ACK), (FID_CANCEL) ],
81 (CHECKIN) => {
82 name => "Checkin",
83 handler => \&handle_checkin,
84 protocol => {
85 1 => {
86 template => "CA18A18",
87 template_len => 37,
88 fields => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
90 2 => {
91 template => "CA18A18",
92 template_len => 37,
93 fields => [ (FID_CURRENT_LOCN), (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_CANCEL) ],
97 (BLOCK_PATRON) => {
98 name => "Block Patron",
99 handler => \&handle_block_patron,
100 protocol => {
101 1 => {
102 template => "CA18",
103 template_len => 19,
104 fields => [ (FID_INST_ID), (FID_BLOCKED_CARD_MSG), (FID_PATRON_ID), (FID_TERMINAL_PWD) ],
108 (SC_STATUS) => {
109 name => "SC Status",
110 handler => \&handle_sc_status,
111 protocol => {
112 1 => {
113 template => "CA3A4",
114 template_len => 8,
115 fields => [],
119 (REQUEST_ACS_RESEND) => {
120 name => "Request ACS Resend",
121 handler => \&handle_request_acs_resend,
122 protocol => {
123 1 => {
124 template => "",
125 template_len => 0,
126 fields => [],
130 (LOGIN) => {
131 name => "Login",
132 handler => \&handle_login,
133 protocol => {
134 2 => {
135 template => "A1A1",
136 template_len => 2,
137 fields => [ (FID_LOGIN_UID), (FID_LOGIN_PWD), (FID_LOCATION_CODE) ],
141 (PATRON_INFO) => {
142 name => "Patron Info",
143 handler => \&handle_patron_info,
144 protocol => {
145 2 => {
146 template => "A3A18A10",
147 template_len => 31,
148 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_START_ITEM), (FID_END_ITEM) ],
152 (END_PATRON_SESSION) => {
153 name => "End Patron Session",
154 handler => \&handle_end_patron_session,
155 protocol => {
156 2 => {
157 template => "A18",
158 template_len => 18,
159 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
163 (FEE_PAID) => {
164 name => "Fee Paid",
165 handler => \&handle_fee_paid,
166 protocol => {
167 2 => {
168 template => "A18A2A2A3",
169 template_len => 25,
170 fields => [ (FID_FEE_AMT), (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD), (FID_FEE_ID), (FID_TRANSACTION_ID) ],
174 (ITEM_INFORMATION) => {
175 name => "Item Information",
176 handler => \&handle_item_information,
177 protocol => {
178 2 => {
179 template => "A18",
180 template_len => 18,
181 fields => [ (FID_INST_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD) ],
185 (ITEM_STATUS_UPDATE) => {
186 name => "Item Status Update",
187 handler => \&handle_item_status_update,
188 protocol => {
189 2 => {
190 template => "A18",
191 template_len => 18,
192 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_ITEM_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS) ],
196 (PATRON_ENABLE) => {
197 name => "Patron Enable",
198 handler => \&handle_patron_enable,
199 protocol => {
200 2 => {
201 template => "A18",
202 template_len => 18,
203 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_TERMINAL_PWD), (FID_PATRON_PWD) ],
207 (HOLD) => {
208 name => "Hold",
209 handler => \&handle_hold,
210 protocol => {
211 2 => {
212 template => "AA18",
213 template_len => 19,
214 fields => [
215 (FID_EXPIRATION), (FID_PICKUP_LOCN), (FID_HOLD_TYPE), (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD),
216 (FID_ITEM_ID), (FID_TITLE_ID), (FID_TERMINAL_PWD), (FID_FEE_ACK)
221 (RENEW) => {
222 name => "Renew",
223 handler => \&handle_renew,
224 protocol => {
225 2 => {
226 template => "CCA18A18",
227 template_len => 38,
228 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD), (FID_ITEM_ID), (FID_TITLE_ID), (FID_TERMINAL_PWD), (FID_ITEM_PROPS), (FID_FEE_ACK) ],
232 (RENEW_ALL) => {
233 name => "Renew All",
234 handler => \&handle_renew_all,
235 protocol => {
236 2 => {
237 template => "A18",
238 template_len => 18,
239 fields => [ (FID_INST_ID), (FID_PATRON_ID), (FID_PATRON_PWD), (FID_TERMINAL_PWD), (FID_FEE_ACK) ],
246 # Now, initialize some of the missing bits of %handlers
248 foreach my $i ( keys(%handlers) ) {
249 if ( !exists( $handlers{$i}->{protocol}->{2} ) ) {
250 $handlers{$i}->{protocol}->{2} = $handlers{$i}->{protocol}->{1};
254 sub new {
255 my ( $class, $msg, $seqno ) = @_;
256 my $self = {};
257 my $msgtag = substr( $msg, 0, 2 );
259 if ( $msgtag eq LOGIN ) {
261 # If the client is using the 2.00-style "Login" message
262 # to authenticate to the server, then we get the Login message
263 # _before_ the client has indicated that it supports 2.00, but
264 # it's using the 2.00 login process, so it must support 2.00.
265 $protocol_version = 2;
267 syslog( "LOG_DEBUG", "Sip::MsgType::new('%s', '%s...', '%s'): seq.no '%s', protocol %s", $class, substr( $msg, 0, 10 ), $msgtag, $seqno, $protocol_version );
269 # warn "SIP PROTOCOL: $protocol_version";
270 if ( !exists( $handlers{$msgtag} ) ) {
271 syslog( "LOG_WARNING", "new Sip::MsgType: Skipping message of unknown type '%s' in '%s'", $msgtag, $msg );
272 return;
273 } elsif ( !exists( $handlers{$msgtag}->{protocol}->{$protocol_version} ) ) {
274 syslog( "LOG_WARNING", "new Sip::MsgType: Skipping message '%s' unsupported by protocol rev. '%d'", $msgtag, $protocol_version );
275 return;
278 bless $self, $class;
280 $self->{seqno} = $seqno;
281 $self->_initialize( substr( $msg, 2 ), $handlers{$msgtag} );
283 return ($self);
286 sub _initialize {
287 my ( $self, $msg, $control_block ) = @_;
288 my $fn;
289 my $proto = $control_block->{protocol}->{$protocol_version};
291 $self->{name} = $control_block->{name};
292 $self->{handler} = $control_block->{handler};
294 $self->{fields} = {};
295 $self->{fixed_fields} = [];
297 chomp($msg); # These four are probably unnecessary now.
298 $msg =~ tr/\cM//d;
299 $msg =~ s/\^M$//;
300 chomp($msg);
302 foreach my $field ( @{ $proto->{fields} } ) {
303 $self->{fields}->{$field} = undef;
306 syslog( "LOG_DEBUG", "Sip::MsgType::_initialize('%s', '%s', '%s', '%s', ...)", $self->{name}, $msg, $proto->{template}, $proto->{template_len} );
308 $self->{fixed_fields} = [ unpack( $proto->{template}, $msg ) ]; # see http://perldoc.perl.org/5.8.8/functions/unpack.html
310 # Skip over the fixed fields and the split the rest of
311 # the message into fields based on the delimiter and parse them
312 foreach my $field ( split( quotemeta($field_delimiter), substr( $msg, $proto->{template_len} ) ) ) {
313 $fn = substr( $field, 0, 2 );
315 if ( !exists( $self->{fields}->{$fn} ) ) {
316 syslog( "LOG_WARNING", "Unsupported field '%s' in %s message '%s'", $fn, $self->{name}, $msg );
317 } elsif ( defined( $self->{fields}->{$fn} ) ) {
318 syslog( "LOG_WARNING", "Duplicate field '%s' (previous value '%s') in %s message '%s'", $fn, $self->{fields}->{$fn}, $self->{name}, $msg );
319 } else {
320 $self->{fields}->{$fn} = substr( $field, 2 );
324 return ($self);
327 sub handle {
328 my ( $msg, $server, $req ) = @_;
329 my $config = $server->{config};
330 my $self;
333 # What's the field delimiter for variable length fields?
334 # This can't be based on the account, since we need to know
335 # the field delimiter to parse a SIP login message
337 if ( defined( $server->{config}->{delimiter} ) ) {
338 $field_delimiter = $server->{config}->{delimiter};
341 # error detection is active if this is a REQUEST_ACS_RESEND
342 # message with a checksum, or if the message is long enough
343 # and the last nine characters begin with a sequence number
344 # field
345 if ( $msg eq REQUEST_ACS_RESEND_CKSUM ) {
347 # Special case
348 $error_detection = 1;
349 $self = C4::SIP::Sip::MsgType->new( (REQUEST_ACS_RESEND), 0 );
350 } elsif ( ( length($msg) > 11 ) && ( substr( $msg, -9, 2 ) eq "AY" ) ) {
351 $error_detection = 1;
353 if ( !verify_cksum($msg) ) {
354 syslog( "LOG_WARNING", "Checksum failed on message '%s'", $msg );
356 # REQUEST_SC_RESEND with error detection
357 $last_response = REQUEST_SC_RESEND_CKSUM;
358 print("$last_response\r");
359 return REQUEST_ACS_RESEND;
360 } else {
362 # Save the sequence number, then strip off the
363 # error detection data to process the message
364 $self = C4::SIP::Sip::MsgType->new( substr( $msg, 0, -9 ), substr( $msg, -7, 1 ) );
366 } elsif ($error_detection) {
368 # We received a non-ED message when ED is supposed to be active.
369 # Warn about this problem, then process the message anyway.
370 syslog( "LOG_WARNING", "Received message without error detection: '%s'", $msg );
371 $error_detection = 0;
372 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
373 } else {
374 $self = C4::SIP::Sip::MsgType->new( $msg, 0 );
377 if ( ( substr( $msg, 0, 2 ) ne REQUEST_ACS_RESEND )
378 && $req
379 && ( substr( $msg, 0, 2 ) ne $req ) ) {
380 return substr( $msg, 0, 2 );
382 unless ( $self->{handler} ) {
383 syslog( "LOG_WARNING", "No handler defined for '%s'", $msg );
384 $last_response = REQUEST_SC_RESEND;
385 print("$last_response\r");
386 return REQUEST_ACS_RESEND;
388 return ( $self->{handler}->( $self, $server ) ); # FIXME
389 # FIXME: Use of uninitialized value in subroutine entry
390 # Can't use string ("") as a subroutine ref while "strict refs" in use
394 ## Message Handlers
398 # Patron status messages are produced in response to both
399 # "Request Patron Status" and "Block Patron"
401 # Request Patron Status requires a patron password, but
402 # Block Patron doesn't (since the patron may never have
403 # provided one before attempting some illegal action).
405 # ASSUMPTION: If the patron password field is present in the
406 # message, then it must match, otherwise incomplete patron status
407 # information will be returned to the terminal.
409 sub build_patron_status {
410 my ( $patron, $lang, $fields, $server ) = @_;
412 my $patron_pwd = $fields->{ (FID_PATRON_PWD) };
413 my $resp = (PATRON_STATUS_RESP);
414 my $password_rc;
416 if ( $patron ) {
417 if ($patron_pwd) {
418 $password_rc = $patron->check_password($patron_pwd);
421 $resp .= patron_status_string($patron);
422 $resp .= $lang . timestamp();
423 $resp .= add_field( FID_PERSONAL_NAME, $patron->name );
425 # while the patron ID we got from the SC is valid, let's
426 # use the one returned from the ILS, just in case...
427 $resp .= add_field( FID_PATRON_ID, $patron->id );
429 if ( $protocol_version >= 2 ) {
430 $resp .= add_field( FID_VALID_PATRON, 'Y' );
432 # Patron password is a required field.
433 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool($password_rc) );
434 $resp .= maybe_add( FID_CURRENCY, $patron->currency );
435 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount );
438 my $msg = $patron->screen_msg;
439 $msg .= ' -- '. INVALID_PW if $patron_pwd && !$password_rc;
440 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
442 $resp .= maybe_add( FID_SCREEN_MSG, $patron->{branchcode}, $server )
443 if ( $server->{account}->{send_patron_home_library_in_af} );
444 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
446 } else {
447 # Invalid patron (cardnumber)
448 # Report that the user has no privs.
450 # no personal name, and is invalid (if we're using 2.00)
451 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
452 $resp .= add_field( FID_PERSONAL_NAME, '' );
454 # the patron ID is invalid, but it's a required field, so
455 # just echo it back
456 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
458 ( $protocol_version >= 2 )
459 and $resp .= add_field( FID_VALID_PATRON, 'N' );
461 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
464 $resp .= add_field( FID_INST_ID, $fields->{ (FID_INST_ID) } );
465 return $resp;
468 sub handle_patron_status {
469 my ( $self, $server ) = @_;
470 warn "handle_patron_status server: " . Dumper( \$server );
471 my $ils = $server->{ils};
472 my $patron;
473 my $resp = (PATRON_STATUS_RESP);
474 my $account = $server->{account};
475 my ( $lang, $date ) = @{ $self->{fixed_fields} };
476 my $fields = $self->{fields};
478 #warn Dumper($fields);
479 #warn FID_INST_ID;
480 #warn $fields->{(FID_INST_ID)};
481 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_patron_status" );
482 $patron = $ils->find_patron( $fields->{ (FID_PATRON_ID) } );
483 $resp = build_patron_status( $patron, $lang, $fields, $server );
484 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
485 return (PATRON_STATUS_REQ);
488 sub handle_checkout {
489 my ( $self, $server ) = @_;
490 my $account = $server->{account};
491 my $ils = $server->{ils};
492 my $inst = $ils->institution;
493 my ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date );
494 my $fields;
495 my ( $patron_id, $item_id, $status );
496 my ( $item, $patron );
497 my $resp;
499 ( $sc_renewal_policy, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
500 $fields = $self->{fields};
502 $patron_id = $fields->{ (FID_PATRON_ID) };
503 $item_id = $fields->{ (FID_ITEM_ID) };
504 my $fee_ack = $fields->{ (FID_FEE_ACK) };
506 if ( $no_block eq 'Y' ) {
508 # Off-line transactions need to be recorded, but there's
509 # not a lot we can do about it
510 syslog( "LOG_WARNING", "received no-block checkout from terminal '%s'", $account->{id} );
512 $status = $ils->checkout_no_block( $patron_id, $item_id, $sc_renewal_policy, $trans_date, $nb_due_date );
513 } else {
515 # Does the transaction date really matter for items that are
516 # checkout out while the terminal is online? I'm guessing 'no'
517 $status = $ils->checkout( $patron_id, $item_id, $sc_renewal_policy, $fee_ack );
520 $item = $status->item;
521 $patron = $status->patron;
523 if ( $status->ok ) {
525 # Item successfully checked out
526 # Fixed fields
527 $resp = CHECKOUT_RESP . '1';
528 $resp .= sipbool( $status->renew_ok );
529 if ( $ils->supports('magnetic media') ) {
530 $resp .= sipbool( $item->magnetic_media );
531 } else {
532 $resp .= 'U';
535 # We never return the obsolete 'U' value for 'desensitize'
536 $resp .= sipbool( $status->desensitize );
537 $resp .= timestamp;
539 # Now for the variable fields
540 $resp .= add_field( FID_INST_ID, $inst );
541 $resp .= add_field( FID_PATRON_ID, $patron_id );
542 $resp .= add_field( FID_ITEM_ID, $item_id );
543 $resp .= add_field( FID_TITLE_ID, $item->title_id );
544 if ( $item->due_date ) {
545 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
546 } else {
547 $resp .= add_field( FID_DUE_DATE, q{} );
550 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
551 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
553 if ( $protocol_version >= 2 ) {
554 if ( $ils->supports('security inhibit') ) {
555 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit );
557 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
558 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
563 else {
565 # Checkout failed
566 # Checkout Response: not ok, no renewal, don't know mag. media,
567 # no desensitize
568 $resp = sprintf( "120NUN%s", timestamp );
569 $resp .= add_field( FID_INST_ID, $inst );
570 $resp .= add_field( FID_PATRON_ID, $patron_id );
571 $resp .= add_field( FID_ITEM_ID, $item_id );
573 # If the item is valid, provide the title, otherwise
574 # leave it blank
575 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '' );
577 # Due date is required. Since it didn't get checked out,
578 # it's not due, so leave the date blank
579 $resp .= add_field( FID_DUE_DATE, '' );
581 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
582 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
584 if ( $protocol_version >= 2 ) {
586 # Is the patron ID valid?
587 $resp .= add_field( FID_VALID_PATRON, sipbool($patron) );
589 if ( $patron && exists( $fields->{FID_PATRON_PWD} ) ) {
591 # Password provided, so we can tell if it was valid or not
592 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password( $fields->{ (FID_PATRON_PWD) } ) ) );
597 if ( $protocol_version >= 2 ) {
599 # Financials : return irrespective of ok status
600 if ( $status->fee_amount ) {
601 $resp .= add_field( FID_FEE_AMT, $status->fee_amount );
602 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency );
603 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type );
604 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
608 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
609 return (CHECKOUT);
612 sub handle_checkin {
613 my ( $self, $server ) = @_;
614 my $account = $server->{account};
615 my $ils = $server->{ils};
616 my $my_branch = $ils->institution;
617 my ( $current_loc, $inst_id, $item_id, $terminal_pwd, $item_props, $cancel );
618 my ( $patron, $item, $status );
619 my $resp = CHECKIN_RESP;
620 my ( $no_block, $trans_date, $return_date ) = @{ $self->{fixed_fields} };
621 my $fields = $self->{fields};
623 $current_loc = $fields->{ (FID_CURRENT_LOCN) };
624 $inst_id = $fields->{ (FID_INST_ID) };
625 $item_id = $fields->{ (FID_ITEM_ID) };
626 $item_props = $fields->{ (FID_ITEM_PROPS) };
627 $cancel = $fields->{ (FID_CANCEL) };
628 if ($current_loc) {
629 $my_branch = $current_loc; # most scm do not set $current_loc
632 $ils->check_inst_id( $inst_id, "handle_checkin" );
634 if ( $no_block eq 'Y' ) {
636 # Off-line transactions, ick.
637 syslog( "LOG_WARNING", "received no-block checkin from terminal '%s'", $account->{id} );
638 $status = $ils->checkin_no_block( $item_id, $trans_date, $return_date, $item_props, $cancel );
639 } else {
640 $status = $ils->checkin( $item_id, $trans_date, $return_date, $my_branch, $item_props, $cancel, $account->{checked_in_ok} );
643 $patron = $status->patron;
644 $item = $status->item;
646 $resp .= $status->ok ? '1' : '0';
647 $resp .= $status->resensitize ? 'Y' : 'N';
648 if ( $item && $ils->supports('magnetic media') ) {
649 $resp .= sipbool( $item->magnetic_media );
650 } else {
652 # item barcode is invalid or system doesn't support 'magnetic media' indicator
653 $resp .= 'U';
656 # apparently we can't trust the returns from Checkin yet (because C4::Circulation::AddReturn is faulty)
657 # So we reproduce the alert logic here.
658 if ( not $status->alert ) {
659 if ( $item->destination_loc and $item->destination_loc ne $my_branch ) {
660 $status->alert(1);
661 $status->alert_type('04'); # no hold, just send it
664 $resp .= $status->alert ? 'Y' : 'N';
665 $resp .= timestamp;
666 $resp .= add_field( FID_INST_ID, $inst_id );
667 $resp .= add_field( FID_ITEM_ID, $item_id );
669 if ($item) {
670 $resp .= add_field( FID_PERM_LOCN, $item->permanent_location );
671 $resp .= maybe_add( FID_TITLE_ID, $item->title_id );
674 if ( $protocol_version >= 2 ) {
675 $resp .= maybe_add( FID_SORT_BIN, $status->sort_bin );
676 if ($patron) {
677 $resp .= add_field( FID_PATRON_ID, $patron->id );
679 if ($item) {
680 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
681 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
682 $resp .= maybe_add( FID_COLLECTION_CODE, $item->collection_code );
683 $resp .= maybe_add( FID_CALL_NUMBER, $item->call_number );
684 $resp .= maybe_add( FID_DESTINATION_LOCATION, $item->destination_loc );
685 $resp .= maybe_add( FID_HOLD_PATRON_ID, $item->hold_patron_bcode );
686 $resp .= maybe_add( FID_HOLD_PATRON_NAME, $item->hold_patron_name );
687 if ( $status->hold and $status->hold->{branchcode} ne $item->destination_loc ) {
688 warn 'SIP hold mismatch: $status->hold->{branchcode}=' . $status->hold->{branchcode} . '; $item->destination_loc=' . $item->destination_loc;
690 # just me being paranoid.
695 $resp .= maybe_add( FID_ALERT_TYPE, $status->alert_type ) if $status->alert;
696 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
697 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
699 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
701 return (CHECKIN);
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 );
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) };
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.
723 # FIXME ???
725 $ils->check_inst_id( $inst_id, "block_patron" );
726 $patron = $ils->find_patron($patron_id);
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.
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) {
739 # Valid patron id
740 $patron->block( $card_retained, $blocked_card_msg );
743 $resp = build_patron_status( $patron, $patron->language, $fields, $server );
744 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
745 return (BLOCK_PATRON);
748 sub handle_sc_status {
749 my ( $self, $server ) = @_;
750 ($server) or warn "handle_sc_status error: no \$server argument received.";
751 my ( $status, $print_width, $sc_protocol_version ) = @{ $self->{fixed_fields} };
752 my ($new_proto);
754 if ( $sc_protocol_version =~ /^1\./ ) {
755 $new_proto = 1;
756 } elsif ( $sc_protocol_version =~ /^2\./ ) {
757 $new_proto = 2;
758 } else {
759 syslog( "LOG_WARNING", "Unrecognized protocol revision '%s', falling back to '1'", $sc_protocol_version );
760 $new_proto = 1;
763 if ( $new_proto != $protocol_version ) {
764 syslog( "LOG_INFO", "Setting protocol level to $new_proto" );
765 $protocol_version = $new_proto;
768 if ( $status == SC_STATUS_PAPER ) {
769 syslog( "LOG_WARNING", "Self-Check unit '%s@%s' out of paper", $self->{account}->{id}, $self->{account}->{institution} );
770 } elsif ( $status == SC_STATUS_SHUTDOWN ) {
771 syslog( "LOG_WARNING", "Self-Check unit '%s@%s' shutting down", $self->{account}->{id}, $self->{account}->{institution} );
774 $self->{account}->{print_width} = $print_width;
775 return ( send_acs_status( $self, $server ) ? SC_STATUS : '' );
778 sub handle_request_acs_resend {
779 my ( $self, $server ) = @_;
781 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' ) {
789 # When resending a message, we aren't supposed to include
790 # a sequence number, even if the original had one (p. 4).
791 # If the last message didn't have a sequence number, then
792 # we can just send it.
793 print("$last_response\r"); # not write_msg?
794 } else {
796 # Cut out the sequence number and checksum, since the old
797 # checksum is wrong for the resent message.
798 my $rebuilt = substr( $last_response, 0, -9 );
799 $self->write_msg( $rebuilt, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
802 return REQUEST_ACS_RESEND;
805 sub login_core {
806 my $server = shift or return;
807 my $uid = shift;
808 my $pwd = shift;
809 my $status = 1; # Assume it all works
810 if ( !exists( $server->{config}->{accounts}->{$uid} ) ) {
811 syslog( "LOG_WARNING", "MsgType::login_core: Unknown login '$uid'" );
812 $status = 0;
813 } elsif ( $server->{config}->{accounts}->{$uid}->{password} ne $pwd ) {
814 syslog( "LOG_WARNING", "MsgType::login_core: Invalid password for login '$uid'" );
815 $status = 0;
816 } else {
818 # Store the active account someplace handy for everybody else to find.
819 $server->{account} = $server->{config}->{accounts}->{$uid};
820 my $inst = $server->{account}->{institution};
821 $server->{institution} = $server->{config}->{institutions}->{$inst};
822 $server->{policy} = $server->{institution}->{policy};
823 $server->{sip_username} = $uid;
824 $server->{sip_password} = $pwd;
826 my $auth_status = api_auth( $uid, $pwd, $inst );
827 if ( !$auth_status or $auth_status !~ /^ok$/i ) {
828 syslog( "LOG_WARNING", "api_auth failed for SIP terminal '%s' of '%s': %s", $uid, $inst, ( $auth_status || 'unknown' ) );
829 $status = 0;
830 } else {
831 syslog( "LOG_INFO", "Successful login/auth for '%s' of '%s'", $server->{account}->{id}, $inst );
834 # initialize connection to ILS
836 my $module = $server->{config}->{institutions}->{$inst}->{implementation};
837 syslog( "LOG_DEBUG", 'login_core: ' . Dumper($module) );
839 # Suspect this is always ILS but so we don't break any eccentic install (for now)
840 if ( $module eq 'ILS' ) {
841 $module = 'C4::SIP::ILS';
843 $module->use;
844 if ($@) {
845 syslog( "LOG_ERR", "%s: Loading ILS implementation '%s' for institution '%s' failed", $server->{service}, $module, $inst );
846 die("Failed to load ILS implementation '$module' for $inst");
849 # like ILS->new(), I think.
850 $server->{ils} = $module->new( $server->{institution}, $server->{account} );
851 if ( !$server->{ils} ) {
852 syslog( "LOG_ERR", "%s: ILS connection to '%s' failed", $server->{service}, $inst );
853 die("Unable to connect to ILS '$inst'");
857 return $status;
860 sub handle_login {
861 my ( $self, $server ) = @_;
862 my ( $uid_algorithm, $pwd_algorithm );
863 my ( $uid, $pwd );
864 my $inst;
865 my $fields;
866 my $status = 1; # Assume it all works
868 $fields = $self->{fields};
869 ( $uid_algorithm, $pwd_algorithm ) = @{ $self->{fixed_fields} };
871 $uid = $fields->{ (FID_LOGIN_UID) }; # Terminal ID, not patron ID.
872 $pwd = $fields->{ (FID_LOGIN_PWD) }; # Terminal PWD, not patron PWD.
874 if ( $uid_algorithm || $pwd_algorithm ) {
875 syslog( "LOG_ERR", "LOGIN: Unsupported non-zero encryption method(s): uid = $uid_algorithm, pwd = $pwd_algorithm" );
876 $status = 0;
877 } else {
878 $status = login_core( $server, $uid, $pwd );
881 $self->write_msg( LOGIN_RESP . $status, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
882 return $status ? LOGIN : '';
886 # Build the detailed summary information for the Patron
887 # Information Response message based on the first 'Y' that appears
888 # in the 'summary' field of the Patron Information request. The
889 # specification says that only one 'Y' can appear in that field,
890 # and we're going to believe it.
892 sub summary_info {
893 my ( $ils, $patron, $summary, $start, $end ) = @_;
894 my $resp = '';
895 my $summary_type;
898 # Map from offsets in the "summary" field of the Patron Information
899 # message to the corresponding field and handler
901 my @summary_map = (
902 { func => $patron->can("hold_items"), fid => FID_HOLD_ITEMS },
903 { func => $patron->can("overdue_items"), fid => FID_OVERDUE_ITEMS },
904 { func => $patron->can("charged_items"), fid => FID_CHARGED_ITEMS },
905 { func => $patron->can("fine_items"), fid => FID_FINE_ITEMS },
906 { func => $patron->can("recall_items"), fid => FID_RECALL_ITEMS },
907 { func => $patron->can("unavail_holds"), fid => FID_UNAVAILABLE_HOLD_ITEMS },
910 if ( ( $summary_type = index( $summary, 'Y' ) ) == -1 ) {
911 return ''; # No detailed information required
914 syslog( "LOG_DEBUG", "Summary_info: index == '%d', field '%s'", $summary_type, $summary_map[$summary_type]->{fid} );
916 my $func = $summary_map[$summary_type]->{func};
917 my $fid = $summary_map[$summary_type]->{fid};
918 my $itemlist = &$func( $patron, $start, $end );
920 syslog( "LOG_DEBUG", "summary_info: list = (%s)", join( ", ", @{$itemlist} ) );
921 foreach my $i ( @{$itemlist} ) {
922 $resp .= add_field( $fid, $i->{barcode} );
925 return $resp;
928 sub handle_patron_info {
929 my ( $self, $server ) = @_;
930 my $ils = $server->{ils};
931 my ( $lang, $trans_date, $summary ) = @{ $self->{fixed_fields} };
932 my $fields = $self->{fields};
933 my ( $inst_id, $patron_id, $terminal_pwd, $patron_pwd, $start, $end );
934 my ( $resp, $patron );
936 $inst_id = $fields->{ (FID_INST_ID) };
937 $patron_id = $fields->{ (FID_PATRON_ID) };
938 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
939 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
940 $start = $fields->{ (FID_START_ITEM) };
941 $end = $fields->{ (FID_END_ITEM) };
943 $patron = $ils->find_patron($patron_id);
945 $resp = (PATRON_INFO_RESP);
946 if ($patron) {
947 $resp .= patron_status_string($patron);
948 $resp .= ( defined($lang) and length($lang) == 3 ) ? $lang : $patron->language;
949 $resp .= timestamp();
951 $resp .= add_count( 'patron_info/hold_items', scalar @{ $patron->hold_items } );
952 $resp .= add_count( 'patron_info/overdue_items', scalar @{ $patron->overdue_items } );
953 $resp .= add_count( 'patron_info/charged_items', scalar @{ $patron->charged_items } );
954 $resp .= add_count( 'patron_info/fine_items', scalar @{ $patron->fine_items } );
955 $resp .= add_count( 'patron_info/recall_items', scalar @{ $patron->recall_items } );
956 $resp .= add_count( 'patron_info/unavail_holds', scalar @{ $patron->unavail_holds } );
958 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ) );
960 # while the patron ID we got from the SC is valid, let's
961 # use the one returned from the ILS, just in case...
962 $resp .= add_field( FID_PATRON_ID, $patron->id );
963 $resp .= add_field( FID_PERSONAL_NAME, $patron->name );
965 # TODO: add code for the fields
966 # hold items limit
967 # overdue items limit
968 # charged items limit
970 $resp .= add_field( FID_VALID_PATRON, 'Y' );
971 my $password_rc;
972 if ( defined($patron_pwd) ) {
974 # If patron password was provided, report whether it was right or not.
975 $password_rc = $patron->check_password($patron_pwd);
976 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $password_rc ) );
979 $resp .= maybe_add( FID_CURRENCY, $patron->currency );
980 $resp .= maybe_add( FID_FEE_AMT, $patron->fee_amount );
981 $resp .= add_field( FID_FEE_LMT, $patron->fee_limit );
983 # TODO: zero or more item details for 2.0 can go here:
984 # hold_items
985 # overdue_items
986 # charged_items
987 # fine_items
988 # recall_items
990 $resp .= summary_info( $ils, $patron, $summary, $start, $end );
992 $resp .= maybe_add( FID_HOME_ADDR, $patron->address );
993 $resp .= maybe_add( FID_EMAIL, $patron->email_addr );
994 $resp .= maybe_add( FID_HOME_PHONE, $patron->home_phone );
996 # SIP 2.0 extensions used by Envisionware
997 # Other terminals will ignore unrecognized fields (unrecognized field identifiers)
998 $resp .= maybe_add( FID_PATRON_BIRTHDATE, $patron->birthdate );
999 $resp .= maybe_add( FID_PATRON_CLASS, $patron->ptype );
1001 # Custom protocol extension to report patron internet privileges
1002 $resp .= maybe_add( FID_INET_PROFILE, $patron->inet_privileges );
1004 my $msg = $patron->screen_msg;
1005 if( defined( $patron_pwd ) && !$password_rc ) {
1006 $msg .= ' -- ' . INVALID_PW;
1008 if ( $server->{account}->{send_patron_home_library_in_af} ) {
1009 $msg .= ' -- ' . $patron->{branchcode};
1011 $resp .= maybe_add( FID_SCREEN_MSG, $msg, $server );
1012 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
1013 } else {
1015 # Invalid patron ID:
1016 # no privileges, no items associated,
1017 # no personal name, and is invalid (if we're using 2.00)
1018 $resp .= 'YYYY' . ( ' ' x 10 ) . $lang . timestamp();
1019 $resp .= '0000' x 6;
1021 $resp .= add_field( FID_INST_ID, ( $ils->institution_id || 'SIP2' ) );
1023 # patron ID is invalid, but field is required, so just echo it back
1024 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
1025 $resp .= add_field( FID_PERSONAL_NAME, '' );
1027 if ( $protocol_version >= 2 ) {
1028 $resp .= add_field( FID_VALID_PATRON, 'N' );
1030 $resp .= maybe_add( FID_SCREEN_MSG, INVALID_CARD, $server );
1033 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1034 return (PATRON_INFO);
1037 sub handle_end_patron_session {
1038 my ( $self, $server ) = @_;
1039 my $ils = $server->{ils};
1040 my $trans_date;
1041 my $fields = $self->{fields};
1042 my $resp = END_SESSION_RESP;
1043 my ( $status, $screen_msg, $print_line );
1045 ($trans_date) = @{ $self->{fixed_fields} };
1047 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, 'handle_end_patron_session' );
1049 ( $status, $screen_msg, $print_line ) = $ils->end_patron_session( $fields->{ (FID_PATRON_ID) } );
1051 $resp .= $status ? 'Y' : 'N';
1052 $resp .= timestamp();
1054 $resp .= add_field( FID_INST_ID, $server->{ils}->institution );
1055 $resp .= add_field( FID_PATRON_ID, $fields->{ (FID_PATRON_ID) } );
1057 $resp .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1058 $resp .= maybe_add( FID_PRINT_LINE, $print_line );
1060 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1062 return (END_PATRON_SESSION);
1065 sub handle_fee_paid {
1066 my ( $self, $server ) = @_;
1067 my $ils = $server->{ils};
1068 my ( $trans_date, $fee_type, $pay_type, $currency ) = @{ $self->{fixed_fields} };
1069 my $fields = $self->{fields};
1070 my ( $fee_amt, $inst_id, $patron_id, $terminal_pwd, $patron_pwd );
1071 my ( $fee_id, $trans_id );
1072 my $status;
1073 my $resp = FEE_PAID_RESP;
1075 $fee_amt = $fields->{ (FID_FEE_AMT) };
1076 $inst_id = $fields->{ (FID_INST_ID) };
1077 $patron_id = $fields->{ (FID_PATRON_ID) };
1078 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1079 $fee_id = $fields->{ (FID_FEE_ID) };
1080 $trans_id = $fields->{ (FID_TRANSACTION_ID) };
1082 $ils->check_inst_id( $inst_id, "handle_fee_paid" );
1084 $status = $ils->pay_fee( $patron_id, $patron_pwd, $fee_amt, $fee_type, $pay_type, $fee_id, $trans_id, $currency );
1086 $resp .= ( $status->ok ? 'Y' : 'N' ) . timestamp;
1087 $resp .= add_field( FID_INST_ID, $inst_id );
1088 $resp .= add_field( FID_PATRON_ID, $patron_id );
1089 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
1090 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1091 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1093 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1095 return (FEE_PAID);
1098 sub handle_item_information {
1099 my ( $self, $server ) = @_;
1100 my $ils = $server->{ils};
1101 my $trans_date;
1102 my $fields = $self->{fields};
1103 my $resp = ITEM_INFO_RESP;
1104 my $item;
1105 my $i;
1107 ($trans_date) = @{ $self->{fixed_fields} };
1109 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_item_information" );
1111 $item = $ils->find_item( $fields->{ (FID_ITEM_ID) } );
1113 if ( !defined($item) ) {
1115 # Invalid Item ID
1116 # "Other" circ stat, "Other" security marker, "Unknown" fee type
1117 $resp .= "010101";
1118 $resp .= timestamp;
1120 # Just echo back the invalid item id
1121 $resp .= add_field( FID_ITEM_ID, $fields->{ (FID_ITEM_ID) } );
1123 # title id is required, but we don't have one
1124 $resp .= add_field( FID_TITLE_ID, '' );
1125 } else {
1127 # Valid Item ID, send the good stuff
1128 $resp .= $item->sip_circulation_status;
1129 $resp .= $item->sip_security_marker;
1130 $resp .= $item->sip_fee_type;
1131 $resp .= timestamp;
1133 $resp .= add_field( FID_ITEM_ID, $item->id );
1134 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1136 $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type );
1137 $resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location );
1138 $resp .= maybe_add( FID_CURRENT_LOCN, $item->current_location );
1139 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1141 if ( ( $i = $item->fee ) != 0 ) {
1142 $resp .= add_field( FID_CURRENCY, $item->fee_currency );
1143 $resp .= add_field( FID_FEE_AMT, $i );
1145 $resp .= maybe_add( FID_OWNER, $item->owner );
1147 if ( ( $i = scalar @{ $item->hold_queue } ) > 0 ) {
1148 $resp .= add_field( FID_HOLD_QUEUE_LEN, $i );
1150 if ( $item->due_date ) {
1151 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
1153 if ( ( $i = $item->recall_date ) != 0 ) {
1154 $resp .= add_field( FID_RECALL_DATE, timestamp($i) );
1156 if ( ( $i = $item->hold_pickup_date ) != 0 ) {
1157 $resp .= add_field( FID_HOLD_PICKUP_DATE, timestamp($i) );
1160 $resp .= maybe_add( FID_SCREEN_MSG, $item->screen_msg, $server );
1161 $resp .= maybe_add( FID_PRINT_LINE, $item->print_line );
1164 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1166 return (ITEM_INFORMATION);
1169 sub handle_item_status_update {
1170 my ( $self, $server ) = @_;
1171 my $ils = $server->{ils};
1172 my ( $trans_date, $item_id, $terminal_pwd, $item_props );
1173 my $fields = $self->{fields};
1174 my $status;
1175 my $item;
1176 my $resp = ITEM_STATUS_UPDATE_RESP;
1178 ($trans_date) = @{ $self->{fixed_fields} };
1180 $ils->check_inst_id( $fields->{ (FID_INST_ID) } );
1182 $item_id = $fields->{ (FID_ITEM_ID) };
1183 $item_props = $fields->{ (FID_ITEM_PROPS) };
1185 if ( !defined($item_id) ) {
1186 syslog( "LOG_WARNING", "handle_item_status: received message without Item ID field" );
1187 } else {
1188 $item = $ils->find_item($item_id);
1191 if ( !$item ) {
1193 # Invalid Item ID
1194 $resp .= '0';
1195 $resp .= timestamp;
1196 $resp .= add_field( FID_ITEM_ID, $item_id );
1197 } else {
1199 # Valid Item ID
1201 $status = $item->status_update($item_props);
1203 $resp .= $status->ok ? '1' : '0';
1204 $resp .= timestamp;
1206 $resp .= add_field( FID_ITEM_ID, $item->id );
1207 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1208 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1211 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1212 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1214 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1216 return (ITEM_STATUS_UPDATE);
1219 sub handle_patron_enable {
1220 my ( $self, $server ) = @_;
1221 my $ils = $server->{ils};
1222 my $fields = $self->{fields};
1223 my ( $trans_date, $patron_id, $terminal_pwd, $patron_pwd );
1224 my ( $status, $patron );
1225 my $resp = PATRON_ENABLE_RESP;
1227 ($trans_date) = @{ $self->{fixed_fields} };
1228 $patron_id = $fields->{ (FID_PATRON_ID) };
1229 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1231 syslog( "LOG_DEBUG", "handle_patron_enable: patron_id: '%s', patron_pwd: '%s'", $patron_id, $patron_pwd );
1233 $patron = $ils->find_patron($patron_id);
1235 if ( !defined($patron) ) {
1237 # Invalid patron ID
1238 $resp .= 'YYYY' . ( ' ' x 10 ) . '000' . timestamp();
1239 $resp .= add_field( FID_PATRON_ID, $patron_id );
1240 $resp .= add_field( FID_PERSONAL_NAME, '' );
1241 $resp .= add_field( FID_VALID_PATRON, 'N' );
1242 $resp .= add_field( FID_VALID_PATRON_PWD, 'N' );
1243 } else {
1245 # valid patron
1246 if ( !defined($patron_pwd) || $patron->check_password($patron_pwd) ) {
1248 # Don't enable the patron if there was an invalid password
1249 $status = $patron->enable;
1251 $resp .= patron_status_string($patron);
1252 $resp .= $patron->language . timestamp();
1254 $resp .= add_field( FID_PATRON_ID, $patron->id );
1255 $resp .= add_field( FID_PERSONAL_NAME, $patron->name );
1256 if ( defined($patron_pwd) ) {
1257 $resp .= add_field( FID_VALID_PATRON_PWD, sipbool( $patron->check_password($patron_pwd) ) );
1259 $resp .= add_field( FID_VALID_PATRON, 'Y' );
1260 $resp .= maybe_add( FID_SCREEN_MSG, $patron->screen_msg, $server );
1261 $resp .= maybe_add( FID_PRINT_LINE, $patron->print_line );
1264 $resp .= add_field( FID_INST_ID, $ils->institution );
1266 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1268 return (PATRON_ENABLE);
1271 sub handle_hold {
1272 my ( $self, $server ) = @_;
1273 my $ils = $server->{ils};
1274 my ( $hold_mode, $trans_date );
1275 my ( $expiry_date, $pickup_locn, $hold_type, $patron_id, $patron_pwd );
1276 my ( $item_id, $title_id, $fee_ack );
1277 my $fields = $self->{fields};
1278 my $status;
1279 my $resp = HOLD_RESP;
1281 ( $hold_mode, $trans_date ) = @{ $self->{fixed_fields} };
1283 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_hold" );
1285 $patron_id = $fields->{ (FID_PATRON_ID) };
1286 $expiry_date = $fields->{ (FID_EXPIRATION) } || '';
1287 $pickup_locn = $fields->{ (FID_PICKUP_LOCN) } || '';
1288 $hold_type = $fields->{ (FID_HOLD_TYPE) } || '2'; # Any copy of title
1289 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1290 $item_id = $fields->{ (FID_ITEM_ID) } || '';
1291 $title_id = $fields->{ (FID_TITLE_ID) } || '';
1292 $fee_ack = $fields->{ (FID_FEE_ACK) } || 'N';
1294 if ( $hold_mode eq '+' ) {
1295 $status = $ils->add_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1296 } elsif ( $hold_mode eq '-' ) {
1297 $status = $ils->cancel_hold( $patron_id, $patron_pwd, $item_id, $title_id );
1298 } elsif ( $hold_mode eq '*' ) {
1299 $status = $ils->alter_hold( $patron_id, $patron_pwd, $item_id, $title_id, $expiry_date, $pickup_locn, $hold_type, $fee_ack );
1300 } else {
1301 syslog( "LOG_WARNING", "handle_hold: Unrecognized hold mode '%s' from terminal '%s'", $hold_mode, $server->{account}->{id} );
1302 $status = $ils->Transaction::Hold; # new?
1303 $status->screen_msg("System error. Please contact library staff.");
1306 $resp .= $status->ok;
1307 $resp .= sipbool( $status->item && $status->item->available($patron_id) );
1308 $resp .= timestamp;
1310 if ( $status->ok ) {
1311 $resp .= add_field( FID_PATRON_ID, $status->patron->id );
1313 ( $status->expiration_date )
1314 and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ) );
1315 $resp .= maybe_add( FID_QUEUE_POS, $status->queue_position );
1316 $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location );
1317 $resp .= maybe_add( FID_ITEM_ID, $status->item->id );
1318 $resp .= maybe_add( FID_TITLE_ID, $status->item->title_id );
1319 } else {
1321 # Not ok. still need required fields
1322 $resp .= add_field( FID_PATRON_ID, $patron_id );
1325 $resp .= add_field( FID_INST_ID, $ils->institution );
1326 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1327 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1329 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1331 return (HOLD);
1334 sub handle_renew {
1335 my ( $self, $server ) = @_;
1336 my $ils = $server->{ils};
1337 my ( $third_party, $no_block, $trans_date, $nb_due_date );
1338 my ( $patron_id, $patron_pwd, $item_id, $title_id, $item_props, $fee_ack );
1339 my $fields = $self->{fields};
1340 my $status;
1341 my ( $patron, $item );
1342 my $resp = RENEW_RESP;
1344 ( $third_party, $no_block, $trans_date, $nb_due_date ) = @{ $self->{fixed_fields} };
1346 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew" );
1348 if ( $no_block eq 'Y' ) {
1349 syslog( "LOG_WARNING", "handle_renew: received 'no block' renewal from terminal '%s'", $server->{account}->{id} );
1352 $patron_id = $fields->{ (FID_PATRON_ID) };
1353 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1354 $item_id = $fields->{ (FID_ITEM_ID) };
1355 $title_id = $fields->{ (FID_TITLE_ID) };
1356 $item_props = $fields->{ (FID_ITEM_PROPS) };
1357 $fee_ack = $fields->{ (FID_FEE_ACK) };
1359 $status = $ils->renew( $patron_id, $patron_pwd, $item_id, $title_id, $no_block, $nb_due_date, $third_party, $item_props, $fee_ack );
1361 $patron = $status->patron;
1362 $item = $status->item;
1364 if ( $status->renewal_ok ) {
1365 $resp .= '1';
1366 $resp .= $status->renewal_ok ? 'Y' : 'N';
1367 if ( $ils->supports('magnetic media') ) {
1368 $resp .= sipbool( $item->magnetic_media );
1369 } else {
1370 $resp .= 'U';
1372 $resp .= sipbool( $status->desensitize );
1373 $resp .= timestamp;
1374 $resp .= add_field( FID_PATRON_ID, $patron->id );
1375 $resp .= add_field( FID_ITEM_ID, $item->id );
1376 $resp .= add_field( FID_TITLE_ID, $item->title_id );
1377 if ( $item->due_date ) {
1378 $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ) );
1379 } else {
1380 $resp .= add_field( FID_DUE_DATE, q{} );
1382 if ( $ils->supports('security inhibit') ) {
1383 $resp .= add_field( FID_SECURITY_INHIBIT, $status->security_inhibit );
1385 $resp .= add_field( FID_MEDIA_TYPE, $item->sip_media_type );
1386 $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties );
1387 } else {
1389 # renew failed for some reason
1390 # not OK, renewal not OK, Unknown media type (why bother checking?)
1391 $resp .= '0NUN';
1392 $resp .= timestamp;
1394 # If we found the patron or the item, the return the ILS
1395 # information, otherwise echo back the information we received
1396 # from the terminal
1397 $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id );
1398 $resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id );
1399 $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : $title_id );
1400 $resp .= add_field( FID_DUE_DATE, '' );
1403 if ( $status->fee_amount ) {
1404 $resp .= add_field( FID_FEE_AMT, $status->fee_amount );
1405 $resp .= maybe_add( FID_CURRENCY, $status->sip_currency );
1406 $resp .= maybe_add( FID_FEE_TYPE, $status->sip_fee_type );
1407 $resp .= maybe_add( FID_TRANSACTION_ID, $status->transaction_id );
1410 $resp .= add_field( FID_INST_ID, $ils->institution );
1411 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1412 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1414 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1416 return (RENEW);
1419 sub handle_renew_all {
1421 # my ($third_party, $no_block, $nb_due_date, $fee_ack, $patron);
1423 my ( $self, $server ) = @_;
1424 my $ils = $server->{ils};
1425 my ( $trans_date, $patron_id, $patron_pwd, $terminal_pwd, $fee_ack );
1426 my $fields = $self->{fields};
1427 my $resp = RENEW_ALL_RESP;
1428 my $status;
1429 my ( @renewed, @unrenewed );
1431 $ils->check_inst_id( $fields->{ (FID_INST_ID) }, "handle_renew_all" );
1433 ($trans_date) = @{ $self->{fixed_fields} };
1435 $patron_id = $fields->{ (FID_PATRON_ID) };
1436 $patron_pwd = $fields->{ (FID_PATRON_PWD) };
1437 $terminal_pwd = $fields->{ (FID_TERMINAL_PWD) };
1438 $fee_ack = $fields->{ (FID_FEE_ACK) };
1440 $status = $ils->renew_all( $patron_id, $patron_pwd, $fee_ack );
1442 $resp .= $status->ok ? '1' : '0';
1444 if ( !$status->ok ) {
1445 $resp .= add_count( "renew_all/renewed_count", 0 );
1446 $resp .= add_count( "renew_all/unrenewed_count", 0 );
1447 @renewed = ();
1448 @unrenewed = ();
1449 } else {
1450 @renewed = ( @{ $status->renewed } );
1451 @unrenewed = ( @{ $status->unrenewed } );
1452 $resp .= add_count( "renew_all/renewed_count", scalar @renewed );
1453 $resp .= add_count( "renew_all/unrenewed_count", scalar @unrenewed );
1456 $resp .= timestamp;
1457 $resp .= add_field( FID_INST_ID, $ils->institution );
1459 $resp .= join( '', map( add_field( FID_RENEWED_ITEMS, $_ ), @renewed ) );
1460 $resp .= join( '', map( add_field( FID_UNRENEWED_ITEMS, $_ ), @unrenewed ) );
1462 $resp .= maybe_add( FID_SCREEN_MSG, $status->screen_msg, $server );
1463 $resp .= maybe_add( FID_PRINT_LINE, $status->print_line );
1465 $self->write_msg( $resp, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1467 return (RENEW_ALL);
1471 # send_acs_status($self, $server)
1473 # Send an ACS Status message, which is contains lots of little fields
1474 # of information gleaned from all sorts of places.
1477 my @message_type_names = (
1478 "patron status request",
1479 "checkout",
1480 "checkin",
1481 "block patron",
1482 "acs status",
1483 "request sc/acs resend",
1484 "login",
1485 "patron information",
1486 "end patron session",
1487 "fee paid",
1488 "item information",
1489 "item status update",
1490 "patron enable",
1491 "hold",
1492 "renew",
1493 "renew all",
1496 sub send_acs_status {
1497 my ( $self, $server, $screen_msg, $print_line ) = @_;
1498 my $msg = ACS_STATUS;
1499 ($server) or die "send_acs_status error: no \$server argument received";
1500 my $account = $server->{account} or die "send_acs_status error: no 'account' in \$server object:\n" . Dumper($server);
1501 my $policy = $server->{policy} or die "send_acs_status error: no 'policy' in \$server object:\n" . Dumper($server);
1502 my $ils = $server->{ils} or die "send_acs_status error: no 'ils' in \$server object:\n" . Dumper($server);
1503 my ( $online_status, $checkin_ok, $checkout_ok, $ACS_renewal_policy );
1504 my ( $status_update_ok, $offline_ok, $timeout, $retries );
1506 $online_status = 'Y';
1507 $checkout_ok = sipbool( $ils->checkout_ok );
1508 $checkin_ok = sipbool( $ils->checkin_ok );
1509 $ACS_renewal_policy = sipbool( $policy->{renewal} );
1510 $status_update_ok = sipbool( $ils->status_update_ok );
1511 $offline_ok = sipbool( $ils->offline_ok );
1512 $timeout = sprintf( "%03d", $policy->{timeout} );
1513 $retries = sprintf( "%03d", $policy->{retries} );
1515 if ( length($timeout) != 3 ) {
1516 syslog( "LOG_ERR", "handle_acs_status: timeout field wrong size: '%s'", $timeout );
1517 $timeout = '000';
1520 if ( length($retries) != 3 ) {
1521 syslog( "LOG_ERR", "handle_acs_status: retries field wrong size: '%s'", $retries );
1522 $retries = '000';
1525 $msg .= "$online_status$checkin_ok$checkout_ok$ACS_renewal_policy";
1526 $msg .= "$status_update_ok$offline_ok$timeout$retries";
1527 $msg .= timestamp();
1529 if ( $protocol_version == 1 ) {
1530 $msg .= '1.00';
1531 } elsif ( $protocol_version == 2 ) {
1532 $msg .= '2.00';
1533 } else {
1534 syslog( "LOG_ERR", 'Bad setting for $protocol_version, "%s" in send_acs_status', $protocol_version );
1535 $msg .= '1.00';
1538 # Institution ID
1539 $msg .= add_field( FID_INST_ID, $account->{institution} );
1541 if ( $protocol_version >= 2 ) {
1543 # Supported messages: we do it all
1544 my $supported_msgs = '';
1546 foreach my $msg_name (@message_type_names) {
1547 if ( $msg_name eq 'request sc/acs resend' ) {
1548 $supported_msgs .= sipbool(1);
1549 } else {
1550 $supported_msgs .= sipbool( $ils->supports($msg_name) );
1553 if ( length($supported_msgs) < 16 ) {
1554 syslog( "LOG_ERR", 'send_acs_status: supported messages "%s" too short', $supported_msgs );
1556 $msg .= add_field( FID_SUPPORTED_MSGS, $supported_msgs );
1559 $msg .= maybe_add( FID_SCREEN_MSG, $screen_msg, $server );
1561 if ( defined( $account->{print_width} )
1562 && defined($print_line)
1563 && $account->{print_width} < length($print_line) ) {
1564 syslog( "LOG_WARNING", "send_acs_status: print line '%s' too long. Truncating", $print_line );
1565 $print_line = substr( $print_line, 0, $account->{print_width} );
1568 $msg .= maybe_add( FID_PRINT_LINE, $print_line );
1570 # Do we want to tell the terminal its location?
1572 $self->write_msg( $msg, undef, $server->{account}->{terminator}, $server->{account}->{encoding} );
1573 return 1;
1577 # build_patron_status: create the 14-char patron status
1578 # string for the Patron Status message
1580 sub patron_status_string {
1581 my $patron = shift;
1582 my $patron_status;
1584 syslog( "LOG_DEBUG", "patron_status_string: %s charge_ok: %s", $patron->id, $patron->charge_ok );
1585 $patron_status = sprintf(
1586 '%s%s%s%s%s%s%s%s%s%s%s%s%s%s',
1587 denied( $patron->charge_ok ),
1588 denied( $patron->renew_ok ),
1589 denied( $patron->recall_ok ),
1590 denied( $patron->hold_ok ),
1591 boolspace( $patron->card_lost ),
1592 boolspace( $patron->too_many_charged ),
1593 boolspace( $patron->too_many_overdue ),
1594 boolspace( $patron->too_many_renewal ),
1595 boolspace( $patron->too_many_claim_return ),
1596 boolspace( $patron->too_many_lost ),
1597 boolspace( $patron->excessive_fines ),
1598 boolspace( $patron->excessive_fees ),
1599 boolspace( $patron->recall_overdue ),
1600 boolspace( $patron->too_many_billed )
1602 return $patron_status;
1605 sub api_auth {
1606 my ( $username, $password, $branch ) = @_;
1607 $ENV{REMOTE_USER} = $username;
1608 my $query = CGI->new();
1609 $query->param( userid => $username );
1610 $query->param( password => $password );
1611 if ($branch) {
1612 $query->param( branch => $branch );
1614 my ( $status, $cookie, $sessionID ) = check_api_auth( $query, { circulate => 1 }, 'intranet' );
1615 return $status;
1619 __END__