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;
19 my $U = "OpenILS::Application::AppUtils";
20 my $DB = "OpenILS::Utils::OfflineStore";
21 my $SES = "${DB}::Session";
22 my $SCRIPT = "OpenILS::Utils::OfflineStore::Script";
24 # --------------------------------------------------------------------
26 # --------------------------------------------------------------------
28 do '##CONFIG##/offline-config.pl';
32 my $basedir = $config{base_dir} || die "Offline config error: no base_dir defined\n";
33 my $bootstrap = $config{bootstrap} || die "Offline config error: no bootstrap defined\n";
34 my $wsname = $cgi->param('ws');
35 my $org = $cgi->param('org');
36 my $authtoken = $cgi->param('ses') || "";
37 my $seskey = $cgi->param('seskey');
38 my $action = $cgi->param('action'); # - create, load, execute, status
50 # --------------------------------------------------------------------
52 # This function should behave as a child_init might behave in case
53 # this is moved to mod_perl
54 # --------------------------------------------------------------------
57 $DB->DBFile($config{db});
58 OpenSRF::System->bootstrap_client(config_file => $bootstrap );
62 sub _ol_debug_params {
64 my @params = $cgi->param;
65 @params = sort { $a cmp $b } @params;
66 $s .= "$_=" . $cgi->param($_) . "\n" for @params;
68 warn '-'x60 ."\n$s\n";
72 # --------------------------------------------------------------------
73 # Finds the requestor and other info specific to this request
74 # --------------------------------------------------------------------
77 # fetch the requestor object
78 ($requestor, $evt) = $U->checkses($authtoken);
79 ol_handle_result($evt) if $evt;
81 # try the param, the workstation, and finally the user's ws org
83 $wsobj = ol_fetch_workstation($wsname);
84 $org = $wsobj->owning_lib if $wsobj;
85 $org = $requestor->ws_ou unless $org;
86 ol_handle_result(OpenILS::Event->new('OFFLINE_NO_ORG')) unless $org;
91 # --------------------------------------------------------------------
92 # Runs the requested action
93 # --------------------------------------------------------------------
98 if( $action eq 'create' ) {
100 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
101 ol_handle_result($evt) if $evt;
102 $payload = ol_create_session();
104 } elsif( $action eq 'load' ) {
106 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
107 ol_handle_result($evt) if $evt;
108 $payload = ol_load();
110 } elsif( $action eq 'execute' ) {
112 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_EXECUTE');
113 ol_handle_result($evt) if $evt;
114 $payload = ol_execute();
116 } elsif( $action eq 'status' ) {
118 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_VIEW');
119 ol_handle_result($evt) if $evt;
120 $payload = ol_status();
123 ol_handle_event('SUCCESS', payload => $payload );
127 # --------------------------------------------------------------------
128 # Creates a new session
129 # --------------------------------------------------------------------
130 sub ol_create_session {
132 my $desc = $cgi->param('desc') || "";
133 $seskey = time . "_${$}_" . int(rand() * 1000);
135 $logger->debug("offline: user ".$requestor->id.
136 " creating new session with key $seskey and description $desc");
142 description => $desc,
143 creator => $requestor->id,
144 create_time => CORE::time(),
153 # --------------------------------------------------------------------
154 # Holds the meta-info for a script file
155 # --------------------------------------------------------------------
156 sub ol_create_script {
159 my $session = ol_find_session($seskey);
160 my $delta = $cgi->param('delta') || 0;
162 my $script = $session->add_to_scripts(
164 requestor => $requestor->id,
165 create_time => CORE::time,
166 workstation => $wsname,
167 logfile => "$basedir/pending/$org/$seskey/$wsname.log",
168 time_delta => $delta,
174 # --------------------------------------------------------------------
175 # Finds the current session in the db
176 # --------------------------------------------------------------------
177 sub ol_find_session {
178 my $ses = $SES->retrieve($seskey);
179 ol_handle_event('OFFLINE_INVALID_SESSION', payload => $seskey) unless $ses;
183 # --------------------------------------------------------------------
184 # Finds a script object in the DB based on workstation and seskey
185 # --------------------------------------------------------------------
187 my $ws = shift || $wsname;
188 my $sk = shift || $seskey;
189 my ($script) = $SCRIPT->search( session => $seskey, workstation => $ws );
193 # --------------------------------------------------------------------
194 # Creates a new script in the database and loads the new script file
195 # --------------------------------------------------------------------
198 my $session = ol_find_session;
199 my $handle = $cgi->upload('file');
200 my $outdir = "$basedir/pending/$org/$seskey";
201 my $outfile = "$outdir/$wsname.log";
203 ol_handle_event('OFFLINE_SESSION_FILE_EXISTS') if ol_find_script();
204 ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
205 ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
207 qx/mkdir -p $outdir/;
209 open(FILE, ">>$outfile") or ol_handle_event('OFFLINE_FILE_ERROR');
210 while( <$handle> ) { print FILE; $x++;}
213 ol_create_script($x);
219 # --------------------------------------------------------------------
220 sub ol_handle_result {
222 my $json = JSON->perl2JSON($obj);
224 if( $cgi->param('html')) {
225 my $html = "<html><body onload='xulG.handle_event($json)'></body></html>";
226 print "content-type: text/html\n\n";
231 print "content-type: text/plain\n\n";
238 # --------------------------------------------------------------------
239 sub ol_handle_event {
240 my( $evt, @args ) = @_;
241 ol_handle_result(OpenILS::Event->new($evt, @args));
245 # --------------------------------------------------------------------
246 sub ol_flesh_session {
250 map { $data{$_} = $session->$_ } $session->columns;
253 for my $script ($session->scripts) {
255 map { $sdata{$_} = $script->$_ } $script->columns;
257 # the client doesn't need this info
258 delete $sdata{session};
260 delete $sdata{logfile};
262 push( @{$data{scripts}}, \%sdata );
269 # --------------------------------------------------------------------
270 # Returns various information on the sessions and scripts
271 # --------------------------------------------------------------------
274 my $type = $cgi->param('status_type') || "scripts";
276 # --------------------------------------------------------------------
277 # Returns info on every script upload attached to the current session
278 # --------------------------------------------------------------------
279 if( $type eq 'scripts' ) {
280 my $session = ol_find_session();
281 ol_handle_result(ol_flesh_session($session));
284 # --------------------------------------------------------------------
285 # Returns all scripts and sessions for the given org
286 # --------------------------------------------------------------------
287 } elsif( $type eq 'sessions' ) {
288 my @sessions = $SES->search( org => $org );
290 # can I do this in the DB without raw SQL?
291 @sessions = sort { $a->create_time <=> $b->create_time } @sessions;
293 push( @data, ol_flesh_session($_) ) for @sessions;
294 ol_handle_result(\@data);
297 # --------------------------------------------------------------------
298 # Returns total commands and completed commands counts
299 # --------------------------------------------------------------------
300 } elsif( $type eq 'summary' ) {
301 my $session = ol_find_session();
303 $logger->debug("offline: retrieving summary info ".
304 "for session ".$session->key." with completed=".$session->num_complete);
307 $count += $_->count for ($session->scripts);
309 { total => $count, num_complete => $session->num_complete });
313 # --------------------------------------------------------------------
314 # Returns the list of non-SUCCESS events that have occurred so far for
315 # this set of commands
316 # --------------------------------------------------------------------
317 } elsif( $type eq 'exceptions' ) {
319 my $session = ol_find_session();
320 my $resfile = "$basedir/pending/$org/$seskey/results";
321 if( $session->end_time ) {
322 $resfile = "$basedir/archive/$org/$seskey/results";
324 my $data = ol_file_to_perl($resfile);
325 $data = [ grep { $_->{event}->{ilsevent} ne '0' } @$data ];
326 ol_handle_result($data);
331 sub ol_fetch_workstation {
333 $logger->debug("offline: Fetching workstation $name");
334 my $ws = $U->storagereq(
335 'open-ils.storage.direct.actor.workstation.search.name', $name);
336 ol_handle_result(OpenILS::Event->new('ACTOR_WORKSTATION_NOT_FOUND')) unless $ws;
343 # --------------------------------------------------------------------
344 # Sorts the script commands then forks a child to executes them.
345 # --------------------------------------------------------------------
348 my $session = ol_find_session();
349 ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
350 ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
352 my $commands = ol_collect_commands();
354 # --------------------------------------------------------------------
355 # Note that we must disconnect from opensrf before forking or the
356 # connection will be borked...
357 # --------------------------------------------------------------------
358 OpenSRF::Transport::PeerHandle->retrieve->disconnect;
364 # --------------------------------------------------------------------
365 # Tell the client all is well
366 # --------------------------------------------------------------------
367 ol_handle_event('SUCCESS'); # - this exits
372 # --------------------------------------------------------------------
373 # close stdout/stderr or apache will wait on the child to finish
374 # --------------------------------------------------------------------
378 $logger->debug("offline: child $$ processing data...");
380 # --------------------------------------------------------------------
381 # The child re-connects to the opensrf network and processes
382 # the script requests
383 # --------------------------------------------------------------------
384 OpenSRF::System->bootstrap_client(config_file => $bootstrap);
389 #Class::DBI->autoupdate(1);
393 my $sesion = ol_find_session();
394 $session->in_process(1);
395 ol_process_commands($session, $commands);
396 ol_archive_files($session);
400 $logger->error("offline: child process error $e");
405 sub ol_file_to_perl {
407 open(F, "$fname") or ol_handle_event('OFFLINE_FILE_ERROR');
410 push(@p, JSON->JSON2perl($_)) for @d;
415 # collects the commands and sorts them on timestamp+delta
416 sub ol_collect_commands {
417 my $ses = ol_find_session();
420 # cycle through every script loaded to this session
421 for my $script ($ses->scripts) {
422 my $coms = ol_file_to_perl($script->logfile);
424 # cycle through all of the commands for this script
425 for my $com (@$coms) {
426 $$com{_workstation} = $script->workstation;
427 $$com{_realtime} = $script->time_delta + $com->{timestamp};
428 push( @commands, $com );
432 # make sure thera are no blank commands
433 @commands = grep { ($_ and $_->{type}) } @commands;
436 @commands = sort { $a->{_realtime} <=> $b->{_realtime} } @commands;
438 # push user registrations to the front
439 my @regs = grep { $_->{type} eq 'register' } @commands;
440 my @others = grep { $_->{type} ne 'register' } @commands;
442 return [ @regs, @others ];
446 my $time = shift || CORE::time;
447 my (undef,undef, undef, $mday,$mon,$year) = localtime($time);
448 $mon++; $year += 1900;
449 $mday = "0$mday" unless $mday =~ /\d{2}/o;
450 $mon = "0$mon" unless $mon =~ /\d{2}/o;
451 return ($year, $mon, $mday);
455 # --------------------------------------------------------------------
456 # Moves all files from the pending directory to the archive directory
457 # and removes the pending directory
458 # --------------------------------------------------------------------
459 sub ol_archive_files {
461 my ($y, $m, $d) = ol_date();
463 my $dir = "$basedir/pending/$org/$seskey";
464 my $archdir = "$basedir/archive/$org/$seskey";
465 $logger->debug("offline: archiving files to $archdir");
467 # Tell the db the files are moving
468 $_->logfile($archdir.'/'.$_->workstation.'.log') for ($session->scripts);
470 qx/mkdir -p $archdir/;
471 qx/mv $_ $archdir/ for <$dir/*>;
476 # --------------------------------------------------------------------
477 # Appends results to the results file.
478 # --------------------------------------------------------------------
480 sub ol_append_result {
485 $obj = JSON->perl2JSON($obj);
488 open($rhandle, ">>$basedir/pending/$org/$seskey/results")
489 or ol_handle_event('OFFLINE_FILE_ERROR');
492 print $rhandle "$obj\n";
493 close($rhandle) if $last;
498 # --------------------------------------------------------------------
499 # Runs the commands and returns the list of errors
500 # --------------------------------------------------------------------
501 sub ol_process_commands {
504 my $commands = shift;
507 $session->start_time(CORE::time);
509 for my $d ( @$commands ) {
512 my $last = ($x++ == scalar(@$commands) - 1) ? 1 : 0;
513 my $res = { command => $d };
515 $res->{event} = ol_handle_checkin($d) if $t eq 'checkin';
516 $res->{event} = ol_handle_inhouse($d) if $t eq 'in_house_use';
517 $res->{event} = ol_handle_checkout($d) if $t eq 'checkout';
518 $res->{event} = ol_handle_renew($d) if $t eq 'renew';
519 $res->{event} = ol_handle_register($d) if $t eq 'register';
522 ol_append_result($res, $last);
523 $session->num_complete( $session->num_complete + 1 );
525 $logger->debug("offline: set session [".$session->key."] num_complete to ".$session->num_complete);
528 $session->end_time(CORE::time);
529 $session->in_process(0);
533 # --------------------------------------------------------------------
534 # Runs an in_house_use action
535 # --------------------------------------------------------------------
536 sub ol_handle_inhouse {
539 my $realtime = $command->{_realtime};
540 my $ws = $command->{_workstation};
541 my $barcode = $command->{barcode};
542 my $count = $command->{count} || 1;
543 my $use_time = $command->{use_time} || "";
545 $logger->activity("offline: in_house_use : requestor=". $requestor->id.", realtime=$realtime, ".
546 "workstation=$ws, barcode=$barcode, count=$count, use_time=$use_time");
548 my $ids = $U->simplereq(
550 'open-ils.circ.in_house_use.create', $authtoken,
551 { barcode => $barcode, count => $count, location => $org, use_time => $use_time } );
553 return OpenILS::Event->new('SUCCESS', payload => $ids) if( ref($ids) eq 'ARRAY' );
559 # --------------------------------------------------------------------
560 # Pulls the relevant circ args from the command, fetches data where
562 # --------------------------------------------------------------------
563 sub ol_circ_args_from_command {
566 my $type = $command->{type};
567 my $realtime = $command->{_realtime};
568 my $ws = $command->{_workstation};
569 my $barcode = $command->{barcode} || "";
570 my $cotime = $command->{checkout_time} || "";
571 my $pbc = $command->{patron_barcode};
572 my $due_date = $command->{due_date} || "";
573 my $noncat = ($command->{noncat}) ? "yes" : "no"; # for logging
575 $logger->activity("offline: $type : requestor=". $requestor->id.
576 ", realtime=$realtime, workstation=$ws, checkout_time=$cotime, ".
577 "patron=$pbc, due_date=$due_date, noncat=$noncat");
580 permit_override => 1,
582 checkout_time => $cotime,
583 patron_barcode => $pbc,
584 due_date => $due_date };
586 if( $command->{noncat} ) {
588 $args->{noncat_type} = $command->{noncat_type};
589 $args->{noncat_count} = $command->{noncat_count};
597 # --------------------------------------------------------------------
598 # Performs a checkout action
599 # --------------------------------------------------------------------
600 sub ol_handle_checkout {
602 my $args = ol_circ_args_from_command($command);
603 return $U->simplereq(
604 'open-ils.circ', 'open-ils.circ.checkout', $authtoken, $args );
608 # --------------------------------------------------------------------
609 # Performs the renewal action
610 # --------------------------------------------------------------------
611 sub ol_handle_renew {
613 my $args = ol_circ_args_from_command($command);
615 return $U->simplereq(
616 'open-ils.circ', 'open-ils.circ.renew', $authtoken, $args );
620 # --------------------------------------------------------------------
621 # Runs a checkin action
622 # --------------------------------------------------------------------
623 sub ol_handle_checkin {
626 my $realtime = $command->{_realtime};
627 my $ws = $command->{_workstation};
628 my $barcode = $command->{barcode};
629 my $backdate = $command->{backdate} || "";
631 $logger->activity("offline: checkin : requestor=". $requestor->id.
632 ", realtime=$realtime, ". "workstation=$ws, barcode=$barcode, backdate=$backdate");
634 return $U->simplereq(
636 'open-ils.circ.checkin', $authtoken,
637 { barcode => $barcode, backdate => $backdate } );
642 # --------------------------------------------------------------------
643 # Registers a new patron
644 # --------------------------------------------------------------------
645 sub ol_handle_register {
648 my $barcode = $command->{user}->{card}->{barcode};
649 delete $command->{user}->{card};
651 $logger->info("offline: creating new user with barcode $barcode");
653 # now, create the user
654 my $actor = Fieldmapper::actor::user->new;
655 my $card = Fieldmapper::actor::card->new;
658 # username defaults to the barcode
659 $actor->usrname( ($actor->usrname) ? $actor->usrname : $barcode );
661 # Set up all of the virtual IDs, isnew, etc.
665 $actor->cards([$card]);
670 $card->barcode($barcode);
676 for my $resp (@{$command->{user}->{survey_responses}}) {
677 my $sr = Fieldmapper::action::survey_response->new;
678 $sr->$_( $resp->{$_} ) for keys %$resp;
682 $logger->debug("offline: created new survey response for survey ".$sr->survey);
684 delete $command->{user}->{survey_responses};
685 $actor->survey_responses(\@sresp) if @sresp;
687 # extract the billing address
688 if( my $addr = $command->{user}->{billing_address} ) {
689 $billing_address = Fieldmapper::actor::user_address->new;
690 $billing_address->$_($addr->{$_}) for keys %$addr;
691 $billing_address->isnew(1);
692 $billing_address->id(-1);
693 $billing_address->usr(-1);
694 delete $command->{user}->{billing_address};
695 $logger->debug("offline: read billing address ".$billing_address->street1);
698 # extract the mailing address
699 if( my $addr = $command->{user}->{mailing_address} ) {
700 $mailing_address = Fieldmapper::actor::user_address->new;
701 $mailing_address->$_($addr->{$_}) for keys %$addr;
702 $mailing_address->isnew(1);
703 $mailing_address->id(-2);
704 $mailing_address->usr(-1);
705 delete $command->{user}->{mailing_address};
706 $logger->debug("offline: read mailing address ".$mailing_address->street1);
709 # make sure we have values for both
710 $billing_address ||= $mailing_address;
711 $mailing_address ||= $billing_address;
713 $actor->billing_address($billing_address->id);
714 $actor->mailing_address($mailing_address->id);
715 $actor->addresses([$mailing_address]);
717 push( @{$actor->addresses}, $billing_address )
718 unless $billing_address->id eq $mailing_address->id;
720 # pull all of the rest of the data from the command blob
721 $actor->$_( $command->{user}->{$_} ) for keys %{$command->{user}};
723 $logger->debug("offline: creating user object...");
724 $actor = $U->simplereq(
726 'open-ils.actor.patron.update', $authtoken, $actor);
728 return $actor if(ref($actor) eq 'HASH'); # an event occurred
730 return OpenILS::Event->new('SUCCESS', payload => $actor);