2 use strict; use warnings;
6 use OpenSRF::Utils::Logger qw/$logger/;
7 use OpenILS::Application::AppUtils;
9 use OpenSRF::EX qw/:try/;
11 use OpenILS::Utils::Fieldmapper;
12 use Digest::MD5 qw/md5_hex/;
13 use OpenSRF::Utils qw/:daemon/;
14 use OpenILS::Utils::OfflineStore;
15 use OpenSRF::Utils::SettingsClient;
20 my $U = "OpenILS::Application::AppUtils";
21 my $DB = "OpenILS::Utils::OfflineStore";
22 my $SES = "${DB}::Session";
23 my $SCRIPT = "OpenILS::Utils::OfflineStore::Script";
25 # --------------------------------------------------------------------
27 # --------------------------------------------------------------------
29 do '##CONFIG##/offline-config.pl';
33 my $basedir = $config{base_dir} || die "Offline config error: no base_dir defined\n";
34 my $bootstrap = $config{bootstrap} || die "Offline config error: no bootstrap defined\n";
35 my $wsname = $cgi->param('ws');
36 my $org = $cgi->param('org');
37 my $authtoken = $cgi->param('ses') || "";
38 my $seskey = $cgi->param('seskey');
39 my $action = $cgi->param('action'); # - create, load, execute, status
51 # --------------------------------------------------------------------
53 # This function should behave as a child_init might behave in case
54 # this is moved to mod_perl
55 # --------------------------------------------------------------------
58 #$DB->DBFile($config{db});
59 $DB->DBFile($config{dsn}, $config{usr}, $config{pw});
60 OpenSRF::System->bootstrap_client(config_file => $bootstrap );
61 Fieldmapper->import(IDL =>
62 OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
66 sub _ol_debug_params {
68 my @params = $cgi->param;
69 @params = sort { $a cmp $b } @params;
70 $s .= "$_=" . $cgi->param($_) . "\n" for @params;
72 warn '-'x60 ."\n$s\n";
76 # --------------------------------------------------------------------
77 # Finds the requestor and other info specific to this request
78 # --------------------------------------------------------------------
81 # fetch the requestor object
82 ($requestor, $evt) = $U->checkses($authtoken);
83 ol_handle_result($evt) if $evt;
85 # try the param, the workstation, and finally the user's ws org
87 $wsobj = ol_fetch_workstation($wsname);
88 $org = $wsobj->owning_lib if $wsobj;
89 $org = $requestor->ws_ou unless $org;
90 ol_handle_result(OpenILS::Event->new('OFFLINE_NO_ORG')) unless $org;
95 # --------------------------------------------------------------------
96 # Runs the requested action
97 # --------------------------------------------------------------------
102 if( $action eq 'create' ) {
104 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
105 ol_handle_result($evt) if $evt;
106 $payload = ol_create_session();
108 } elsif( $action eq 'load' ) {
110 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
111 ol_handle_result($evt) if $evt;
112 $payload = ol_load();
114 } elsif( $action eq 'execute' ) {
116 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_EXECUTE');
117 ol_handle_result($evt) if $evt;
118 $payload = ol_execute();
120 } elsif( $action eq 'status' ) {
122 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_VIEW');
123 ol_handle_result($evt) if $evt;
124 $payload = ol_status();
127 ol_handle_event('SUCCESS', payload => $payload );
131 # --------------------------------------------------------------------
132 # Creates a new session
133 # --------------------------------------------------------------------
134 sub ol_create_session {
136 my $desc = $cgi->param('desc') || "";
137 $seskey = time . "_${$}_" . int(rand() * 1000);
139 $logger->debug("offline: user ".$requestor->id.
140 " creating new session with key $seskey and description $desc");
146 description => $desc,
147 creator => $requestor->id,
148 create_time => CORE::time(),
157 # --------------------------------------------------------------------
158 # Holds the meta-info for a script file
159 # --------------------------------------------------------------------
160 sub ol_create_script {
163 my $session = ol_find_session($seskey);
164 my $delta = $cgi->param('delta') || 0;
166 my $script = $session->add_to_scripts(
168 requestor => $requestor->id,
169 create_time => CORE::time,
170 workstation => $wsname,
171 logfile => "$basedir/pending/$org/$seskey/$wsname.log",
172 time_delta => $delta,
178 # --------------------------------------------------------------------
179 # Finds the current session in the db
180 # --------------------------------------------------------------------
181 sub ol_find_session {
182 my $ses = $SES->retrieve($seskey);
183 ol_handle_event('OFFLINE_INVALID_SESSION', payload => $seskey) unless $ses;
187 # --------------------------------------------------------------------
188 # Finds a script object in the DB based on workstation and seskey
189 # --------------------------------------------------------------------
191 my $ws = shift || $wsname;
192 my $sk = shift || $seskey;
193 my ($script) = $SCRIPT->search( session => $seskey, workstation => $ws );
197 # --------------------------------------------------------------------
198 # Creates a new script in the database and loads the new script file
199 # --------------------------------------------------------------------
202 my $session = ol_find_session;
203 my $handle = $cgi->upload('file');
204 my $outdir = "$basedir/pending/$org/$seskey";
205 my $outfile = "$outdir/$wsname.log";
207 ol_handle_event('OFFLINE_SESSION_FILE_EXISTS') if ol_find_script();
208 ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
209 ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
211 qx/mkdir -p $outdir/;
213 open(FILE, ">>$outfile") or ol_handle_event('OFFLINE_FILE_ERROR');
214 while( <$handle> ) { print FILE; $x++;}
217 ol_create_script($x);
223 # --------------------------------------------------------------------
224 sub ol_handle_result {
226 my $json = JSON->perl2JSON($obj);
228 # Clear this so it's not remembered
231 if( $cgi->param('html')) {
232 my $html = "<html><body onload='xulG.handle_event($json)'></body></html>";
233 print "content-type: text/html\n\n";
238 print "content-type: text/plain\n\n";
245 # --------------------------------------------------------------------
246 sub ol_handle_event {
247 my( $evt, @args ) = @_;
248 ol_handle_result(OpenILS::Event->new($evt, @args));
252 # --------------------------------------------------------------------
253 sub ol_flesh_session {
257 map { $data{$_} = $session->$_ } $session->columns;
260 for my $script ($session->scripts) {
262 map { $sdata{$_} = $script->$_ } $script->columns;
264 # the client doesn't need this info
265 delete $sdata{session};
267 delete $sdata{logfile};
269 push( @{$data{scripts}}, \%sdata );
276 # --------------------------------------------------------------------
277 # Returns various information on the sessions and scripts
278 # --------------------------------------------------------------------
281 my $type = $cgi->param('status_type') || "scripts";
283 # --------------------------------------------------------------------
284 # Returns info on every script upload attached to the current session
285 # --------------------------------------------------------------------
286 if( $type eq 'scripts' ) {
287 my $session = ol_find_session();
288 ol_handle_result(ol_flesh_session($session));
291 # --------------------------------------------------------------------
292 # Returns all scripts and sessions for the given org
293 # --------------------------------------------------------------------
294 } elsif( $type eq 'sessions' ) {
295 my @sessions = $SES->search( org => $org );
297 # can I do this in the DB without raw SQL?
298 @sessions = sort { $a->create_time <=> $b->create_time } @sessions;
300 push( @data, ol_flesh_session($_) ) for @sessions;
301 ol_handle_result(\@data);
304 # --------------------------------------------------------------------
305 # Returns total commands and completed commands counts
306 # --------------------------------------------------------------------
307 } elsif( $type eq 'summary' ) {
308 my $session = ol_find_session();
310 $logger->debug("offline: retrieving summary info ".
311 "for session ".$session->key." with completed=".$session->num_complete);
314 $count += $_->count for ($session->scripts);
316 { total => $count, num_complete => $session->num_complete });
320 # --------------------------------------------------------------------
321 # Returns the list of non-SUCCESS events that have occurred so far for
322 # this set of commands
323 # --------------------------------------------------------------------
324 } elsif( $type eq 'exceptions' ) {
326 my $session = ol_find_session();
327 my $resfile = "$basedir/pending/$org/$seskey/results";
328 if( $session->end_time ) {
329 $resfile = "$basedir/archive/$org/$seskey/results";
331 my $data = ol_file_to_perl($resfile);
332 $data = [ grep { $_->{event}->{ilsevent} ne '0' } @$data ];
333 ol_handle_result($data);
338 sub ol_fetch_workstation {
340 $logger->debug("offline: Fetching workstation $name");
341 my $ws = $U->storagereq(
342 'open-ils.storage.direct.actor.workstation.search.name', $name);
343 ol_handle_result(OpenILS::Event->new('ACTOR_WORKSTATION_NOT_FOUND')) unless $ws;
350 # --------------------------------------------------------------------
351 # Sorts the script commands then forks a child to executes them.
352 # --------------------------------------------------------------------
355 my $session = ol_find_session();
356 ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
357 ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
359 my $commands = ol_collect_commands();
361 # --------------------------------------------------------------------
362 # Note that we must disconnect from opensrf before forking or the
363 # connection will be borked...
364 # --------------------------------------------------------------------
365 OpenSRF::Transport::PeerHandle->retrieve->disconnect;
371 # --------------------------------------------------------------------
372 # Tell the client all is well
373 # --------------------------------------------------------------------
374 ol_handle_event('SUCCESS'); # - this exits
379 # --------------------------------------------------------------------
380 # close stdout/stderr or apache will wait on the child to finish
381 # --------------------------------------------------------------------
385 $logger->debug("offline: child $$ processing data...");
387 # --------------------------------------------------------------------
388 # The child re-connects to the opensrf network and processes
389 # the script requests
390 # --------------------------------------------------------------------
391 OpenSRF::System->bootstrap_client(config_file => $bootstrap);
396 #Class::DBI->autoupdate(1);
400 my $sesion = ol_find_session();
401 $session->in_process(1);
402 ol_process_commands($session, $commands);
403 ol_archive_files($session);
407 $logger->error("offline: child process error $e");
412 sub ol_file_to_perl {
414 open(F, "$fname") or ol_handle_event('OFFLINE_FILE_ERROR');
417 push(@p, JSON->JSON2perl($_)) for @d;
422 # collects the commands and sorts them on timestamp+delta
423 sub ol_collect_commands {
424 my $ses = ol_find_session();
427 # cycle through every script loaded to this session
428 for my $script ($ses->scripts) {
429 my $coms = ol_file_to_perl($script->logfile);
431 # cycle through all of the commands for this script
432 for my $com (@$coms) {
433 $$com{_workstation} = $script->workstation;
434 $$com{_realtime} = $script->time_delta + $com->{timestamp};
435 push( @commands, $com );
439 # make sure thera are no blank commands
440 @commands = grep { ($_ and $_->{type}) } @commands;
443 @commands = sort { $a->{_realtime} <=> $b->{_realtime} } @commands;
445 # push user registrations to the front
446 my @regs = grep { $_->{type} eq 'register' } @commands;
447 my @others = grep { $_->{type} ne 'register' } @commands;
449 return [ @regs, @others ];
453 my $time = shift || CORE::time;
454 my (undef,undef, undef, $mday,$mon,$year) = localtime($time);
455 $mon++; $year += 1900;
456 $mday = "0$mday" unless $mday =~ /\d{2}/o;
457 $mon = "0$mon" unless $mon =~ /\d{2}/o;
458 return ($year, $mon, $mday);
462 # --------------------------------------------------------------------
463 # Moves all files from the pending directory to the archive directory
464 # and removes the pending directory
465 # --------------------------------------------------------------------
466 sub ol_archive_files {
468 my ($y, $m, $d) = ol_date();
470 my $dir = "$basedir/pending/$org/$seskey";
471 my $archdir = "$basedir/archive/$org/$seskey";
472 $logger->debug("offline: archiving files to $archdir");
474 # Tell the db the files are moving
475 $_->logfile($archdir.'/'.$_->workstation.'.log') for ($session->scripts);
477 qx/mkdir -p $archdir/;
478 qx/mv $_ $archdir/ for <$dir/*>;
483 # --------------------------------------------------------------------
484 # Appends results to the results file.
485 # --------------------------------------------------------------------
487 sub ol_append_result {
492 $obj = JSON->perl2JSON($obj);
495 open($rhandle, ">>$basedir/pending/$org/$seskey/results")
496 or ol_handle_event('OFFLINE_FILE_ERROR');
499 print $rhandle "$obj\n";
500 close($rhandle) if $last;
505 # --------------------------------------------------------------------
506 # Runs the commands and returns the list of errors
507 # --------------------------------------------------------------------
508 sub ol_process_commands {
511 my $commands = shift;
514 $session->start_time(CORE::time);
516 for my $d ( @$commands ) {
519 my $last = ($x++ == scalar(@$commands) - 1) ? 1 : 0;
520 my $res = { command => $d };
522 $res->{event} = ol_handle_checkin($d) if $t eq 'checkin';
523 $res->{event} = ol_handle_inhouse($d) if $t eq 'in_house_use';
524 $res->{event} = ol_handle_checkout($d) if $t eq 'checkout';
525 $res->{event} = ol_handle_renew($d) if $t eq 'renew';
526 $res->{event} = ol_handle_register($d) if $t eq 'register';
529 ol_append_result($res, $last);
530 $session->num_complete( $session->num_complete + 1 );
532 $logger->debug("offline: set session [".$session->key."] num_complete to ".$session->num_complete);
535 $session->end_time(CORE::time);
536 $session->in_process(0);
540 # --------------------------------------------------------------------
541 # Runs an in_house_use action
542 # --------------------------------------------------------------------
543 sub ol_handle_inhouse {
546 my $realtime = $command->{_realtime};
547 my $ws = $command->{_workstation};
548 my $barcode = $command->{barcode};
549 my $count = $command->{count} || 1;
550 my $use_time = $command->{use_time} || "";
552 $logger->activity("offline: in_house_use : requestor=". $requestor->id.", realtime=$realtime, ".
553 "workstation=$ws, barcode=$barcode, count=$count, use_time=$use_time");
555 my $ids = $U->simplereq(
557 'open-ils.circ.in_house_use.create', $authtoken,
558 { barcode => $barcode, count => $count, location => $org, use_time => $use_time } );
560 return OpenILS::Event->new('SUCCESS', payload => $ids) if( ref($ids) eq 'ARRAY' );
566 # --------------------------------------------------------------------
567 # Pulls the relevant circ args from the command, fetches data where
569 # --------------------------------------------------------------------
570 sub ol_circ_args_from_command {
573 my $type = $command->{type};
574 my $realtime = $command->{_realtime};
575 my $ws = $command->{_workstation};
576 my $barcode = $command->{barcode} || "";
577 my $cotime = $command->{checkout_time} || "";
578 my $pbc = $command->{patron_barcode};
579 my $due_date = $command->{due_date} || "";
580 my $noncat = ($command->{noncat}) ? "yes" : "no"; # for logging
582 $logger->activity("offline: $type : requestor=". $requestor->id.
583 ", realtime=$realtime, workstation=$ws, checkout_time=$cotime, ".
584 "patron=$pbc, due_date=$due_date, noncat=$noncat");
587 permit_override => 1,
589 checkout_time => $cotime,
590 patron_barcode => $pbc,
591 due_date => $due_date };
593 if( $command->{noncat} ) {
595 $args->{noncat_type} = $command->{noncat_type};
596 $args->{noncat_count} = $command->{noncat_count};
604 # --------------------------------------------------------------------
605 # Performs a checkout action
606 # --------------------------------------------------------------------
607 sub ol_handle_checkout {
609 my $args = ol_circ_args_from_command($command);
610 return $U->simplereq(
611 'open-ils.circ', 'open-ils.circ.checkout', $authtoken, $args );
615 # --------------------------------------------------------------------
616 # Performs the renewal action
617 # --------------------------------------------------------------------
618 sub ol_handle_renew {
620 my $args = ol_circ_args_from_command($command);
622 return $U->simplereq(
623 'open-ils.circ', 'open-ils.circ.renew', $authtoken, $args );
627 # --------------------------------------------------------------------
628 # Runs a checkin action
629 # --------------------------------------------------------------------
630 sub ol_handle_checkin {
633 my $realtime = $command->{_realtime};
634 my $ws = $command->{_workstation};
635 my $barcode = $command->{barcode};
636 my $backdate = $command->{backdate} || "";
638 $logger->activity("offline: checkin : requestor=". $requestor->id.
639 ", realtime=$realtime, ". "workstation=$ws, barcode=$barcode, backdate=$backdate");
641 return $U->simplereq(
643 'open-ils.circ.checkin', $authtoken,
644 { barcode => $barcode, backdate => $backdate } );
649 # --------------------------------------------------------------------
650 # Registers a new patron
651 # --------------------------------------------------------------------
652 sub ol_handle_register {
655 my $barcode = $command->{user}->{card}->{barcode};
656 delete $command->{user}->{card};
658 $logger->info("offline: creating new user with barcode $barcode");
660 # now, create the user
661 my $actor = Fieldmapper::actor::user->new;
662 my $card = Fieldmapper::actor::card->new;
665 # username defaults to the barcode
666 $actor->usrname( ($actor->usrname) ? $actor->usrname : $barcode );
668 # Set up all of the virtual IDs, isnew, etc.
672 $actor->cards([$card]);
677 $card->barcode($barcode);
683 for my $resp (@{$command->{user}->{survey_responses}}) {
684 my $sr = Fieldmapper::action::survey_response->new;
685 $sr->$_( $resp->{$_} ) for keys %$resp;
689 $logger->debug("offline: created new survey response for survey ".$sr->survey);
691 delete $command->{user}->{survey_responses};
692 $actor->survey_responses(\@sresp) if @sresp;
694 # extract the billing address
695 if( my $addr = $command->{user}->{billing_address} ) {
696 $billing_address = Fieldmapper::actor::user_address->new;
697 $billing_address->$_($addr->{$_}) for keys %$addr;
698 $billing_address->isnew(1);
699 $billing_address->id(-1);
700 $billing_address->usr(-1);
701 delete $command->{user}->{billing_address};
702 $logger->debug("offline: read billing address ".$billing_address->street1);
705 # extract the mailing address
706 if( my $addr = $command->{user}->{mailing_address} ) {
707 $mailing_address = Fieldmapper::actor::user_address->new;
708 $mailing_address->$_($addr->{$_}) for keys %$addr;
709 $mailing_address->isnew(1);
710 $mailing_address->id(-2);
711 $mailing_address->usr(-1);
712 delete $command->{user}->{mailing_address};
713 $logger->debug("offline: read mailing address ".$mailing_address->street1);
716 # make sure we have values for both
717 $billing_address ||= $mailing_address;
718 $mailing_address ||= $billing_address;
720 $actor->billing_address($billing_address->id);
721 $actor->mailing_address($mailing_address->id);
722 $actor->addresses([$mailing_address]);
724 push( @{$actor->addresses}, $billing_address )
725 unless $billing_address->id eq $mailing_address->id;
727 # pull all of the rest of the data from the command blob
728 $actor->$_( $command->{user}->{$_} ) for keys %{$command->{user}};
730 $logger->debug("offline: creating user object...");
731 $actor = $U->simplereq(
733 'open-ils.actor.patron.update', $authtoken, $actor);
735 return $actor if(ref($actor) eq 'HASH'); # an event occurred
737 return OpenILS::Event->new('SUCCESS', payload => $actor);