]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/offline/offline.pl
committing jason's fixes
[Evergreen.git] / Open-ILS / src / offline / offline.pl
1 #!/usr/bin/perl
2 use strict; use warnings;
3 use CGI;
4 use OpenSRF::System;
5 use OpenSRF::Utils::Logger qw/$logger/;
6 use OpenILS::Application::AppUtils;
7 use OpenILS::Event;
8 use OpenSRF::EX qw/:try/;
9 use JSON;
10 use Data::Dumper;
11 use OpenILS::Utils::Fieldmapper;
12 use Digest::MD5 qw/md5_hex/;
13 use OpenSRF::Utils qw/:daemon/;
14 use OpenILS::Utils::OfflineStore;
15
16 use DBI;
17 $DBI::trace = 1;
18
19 #use Class::DBI;
20 #Class::DBI->autoupdate(1);
21 #local $OpenILS::Utils::OfflineStore::NO_TRIGGERS = 1;
22
23 my $U = "OpenILS::Application::AppUtils";
24 my $DB = "OpenILS::Utils::OfflineStore";
25 my $SES = "${DB}::Session";
26 my $SCRIPT = "OpenILS::Utils::OfflineStore::Script";
27 #$DB->autoupdate(1);
28
29 our %config;
30
31 # --------------------------------------------------------------------
32 # Load the config
33 # --------------------------------------------------------------------
34 #do '##CONFIG##/upload-server.pl';
35 do 'offline-config.pl';
36
37
38 my $cgi                 = new CGI;
39 my $basedir             = $config{base_dir};
40 my $wsname              = $cgi->param('ws');
41 my $org                 = $cgi->param('org');
42 my $authtoken   = $cgi->param('ses') || "";
43 my $seskey              = $cgi->param('seskey');
44 my $action              = $cgi->param('action'); # - create, load, execute, status
45 my $requestor; 
46 my $wsobj;
47 my $orgobj;
48 my $evt;
49
50
51 &ol_init;
52 &ol_runtime_init;
53 &ol_do_action;
54
55
56 # --------------------------------------------------------------------
57 # Set it all up
58 # This function should behave as a child_init might behave in case 
59 # this is moved to mod_perl
60 # --------------------------------------------------------------------
61 sub ol_init {
62         _ol_debug_params();
63         $DB->DBFile($config{db});
64         OpenSRF::System->bootstrap_client(config_file => $config{bootstrap} ); 
65 }
66
67
68 sub _ol_debug_params {
69         my $s = "";
70         my @params = $cgi->param;
71         @params = sort { $a cmp $b } @params;
72         $s .= "$_=" . $cgi->param($_) . "\n" for @params;
73         $s =~ s/\n$//o;
74         warn '-'x60 ."\n$s\n";
75 }
76
77
78 # --------------------------------------------------------------------
79 # Finds the requestor and other info specific to this request
80 # --------------------------------------------------------------------
81 sub ol_runtime_init {
82
83         # fetch the requestor object
84         ($requestor, $evt) = $U->checkses($authtoken);
85         ol_handle_result($evt) if $evt;
86
87         # try the param, the workstation, and finally the user's ws org
88         if(!$org) { 
89                 $wsobj = ol_fetch_workstation($wsname);
90                 $org = $wsobj->owning_lib if $wsobj;
91                 $org = $requestor->ws_ou unless $org;
92                 ol_handle_result(OpenILS::Event->new('OFFLINE_NO_ORG')) unless $org;
93         }
94 }
95
96
97 # --------------------------------------------------------------------
98 # Runs the requested action
99 # --------------------------------------------------------------------
100 sub ol_do_action {
101
102         my $payload;
103
104         if( $action eq 'create' ) {
105                 
106                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
107                 ol_handle_result($evt) if $evt;
108                 $payload = ol_create_session();
109
110         } elsif( $action eq 'load' ) {
111
112                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_UPLOAD');
113                 ol_handle_result($evt) if $evt;
114                 $payload = ol_load();
115
116         } elsif( $action eq 'execute' ) {
117
118                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_EXECUTE');
119                 ol_handle_result($evt) if $evt;
120                 $payload = ol_execute();
121
122         } elsif( $action eq 'status' ) {
123
124                 $evt = $U->check_perms($requestor->id, $org, 'OFFLINE_VIEW');
125                 ol_handle_result($evt) if $evt;
126                 $payload = ol_status();
127         }
128
129         ol_handle_event('SUCCESS', payload => $payload );
130 }
131
132
133 # --------------------------------------------------------------------
134 # Creates a new session
135 # --------------------------------------------------------------------
136 sub ol_create_session {
137
138         my $desc = $cgi->param('desc') || "";
139         $seskey = time . "_${$}_" . int(rand() * 1000);
140
141         $logger->debug("offline: user ".$requestor->id.
142                 " creating new session with key $seskey and description $desc");
143
144         $SES->create(
145                 {       
146                         key                             => $seskey,
147                         org                             => $org,
148                         description             => $desc,
149                         creator                 => $requestor->id,
150                         create_time             => CORE::time(), 
151                         num_complete    => 0,
152                 } 
153         );
154
155         return $seskey;
156 }
157
158
159 # --------------------------------------------------------------------
160 # Holds the meta-info for a script file
161 # --------------------------------------------------------------------
162 sub ol_create_script {
163         my $count = shift;
164
165         my $session = ol_find_session($seskey);
166         my $delta = $cgi->param('delta') || 0;
167
168         my $script = $session->add_to_scripts( 
169                 {
170                         requestor       => $requestor->id,
171                         create_time     => CORE::time,
172                         workstation     => $wsname,
173                         logfile         => "$basedir/pending/$seskey/$wsname.log",
174                         time_delta      => $delta,
175                         count                   => $count,
176                 }
177         );
178 }
179
180 # --------------------------------------------------------------------
181 # Finds the current session in the db
182 # --------------------------------------------------------------------
183 sub ol_find_session {
184         my $ses = $SES->retrieve($seskey);
185         ol_handle_event('OFFLINE_INVALID_SESSION', payload => $seskey) unless $ses;
186         return $ses;
187 }
188
189 # --------------------------------------------------------------------
190 # Finds a script object in the DB based on workstation and seskey
191 # --------------------------------------------------------------------
192 sub ol_find_script {
193         my $ws = shift || $wsname;
194         my $sk = shift || $seskey;
195         my ($script) = $SCRIPT->search( session => $seskey, workstation => $ws );
196         return $script;
197 }
198
199 # --------------------------------------------------------------------
200 # Creates a new script in the database and loads the new script file
201 # --------------------------------------------------------------------
202 sub ol_load {
203
204         my $session = ol_find_session;
205         my $handle      = $cgi->upload('file');
206         my $outdir      = "$basedir/pending/$seskey";
207         my $outfile = "$outdir/$wsname.log";
208
209         ol_handle_event('OFFLINE_SESSION_FILE_EXISTS') if ol_find_script();
210         ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
211         ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
212
213         qx/mkdir -p $outdir/;
214         my $x = 0;
215         open(FILE, ">>$outfile") or ol_handle_event('OFFLINE_FILE_ERROR');
216         while( <$handle> ) { print FILE; $x++;}
217         close(FILE);
218
219         ol_create_script($x);
220
221         return undef;
222 }
223
224
225 # --------------------------------------------------------------------
226 sub ol_handle_result {
227         my $obj = shift;
228         my $json = JSON->perl2JSON($obj);
229
230         if( $cgi->param('html')) {
231                 my $html = "<html><body onload='xulG.handle_event($json)'></body></html>";
232                 print "content-type: text/html\n\n";
233                 print "$html\n";
234
235         } else {
236
237                 print "content-type: text/plain\n\n";
238                 print "$json\n";
239         }
240
241         exit(0);
242 }
243
244 # --------------------------------------------------------------------
245 sub ol_handle_event {
246         my( $evt, @args ) = @_;
247         ol_handle_result(OpenILS::Event->new($evt, @args));
248 }
249
250
251 # --------------------------------------------------------------------
252 sub ol_flesh_session {
253         my $session = shift;
254         my %data;
255
256         map { $data{$_} = $session->$_ } $session->columns;
257         $data{scripts} = [];
258
259         for my $script ($session->scripts) {
260                 my %sdata;
261                 map { $sdata{$_} = $script->$_ } $script->columns;
262
263                 # the client doesn't need this info
264                 delete $sdata{session};
265                 delete $sdata{id};
266                 delete $sdata{logfile};
267
268                 push( @{$data{scripts}}, \%sdata );
269         }
270
271         return \%data;
272 }
273
274
275 # --------------------------------------------------------------------
276 # Returns various information on the sessions and scripts
277 # --------------------------------------------------------------------
278 sub ol_status {
279
280         my $type = $cgi->param('status_type') || "scripts";
281
282         # --------------------------------------------------------------------
283         # Returns info on every script upload attached to the current session
284         # --------------------------------------------------------------------
285         if( $type eq 'scripts' ) {
286                 my $session = ol_find_session();
287                 ol_handle_result(ol_flesh_session($session));
288
289
290         # --------------------------------------------------------------------
291         # Returns all scripts and sessions for the given org
292         # --------------------------------------------------------------------
293         } elsif( $type eq 'sessions' ) {
294                 my @sessions = $SES->search( org => $org );
295
296                 # can I do this in the DB without raw SQL?
297                 @sessions = sort { $a->create_time <=> $b->create_time } @sessions; 
298                 my @data;
299                 push( @data, ol_flesh_session($_) ) for @sessions;
300                 ol_handle_result(\@data);
301
302
303         # --------------------------------------------------------------------
304         # Returns total commands and completed commands counts
305         # --------------------------------------------------------------------
306         } elsif( $type eq 'summary' ) {
307                 my $session = ol_find_session();
308
309                 $logger->debug("offline: retrieving summary info ".
310                         "for session ".$session->key." with completed=".$session->num_complete);
311
312                 my $count = 0;
313                 $count += $_->count for ($session->scripts);
314                 ol_handle_result(
315                         { total => $count, num_complete => $session->num_complete });
316
317
318
319         # --------------------------------------------------------------------
320         # Returns the list of non-SUCCESS events that have occurred so far for 
321         # this set of commands
322         # --------------------------------------------------------------------
323         } elsif( $type eq 'exceptions' ) {
324
325                 my $session = ol_find_session();
326                 my $resfile = "$basedir/pending/$seskey/results";
327                 if( $session->end_time ) {
328                         $resfile = "$basedir/archive/$org/$seskey/results";
329                 }
330                 my $data = ol_file_to_perl($resfile);
331                 $data = [ grep { $_->{event}->{ilsevent} ne '0' } @$data ];
332                 ol_handle_result($data);
333         }
334 }
335
336
337 sub ol_fetch_workstation {
338         my $name = shift;
339         $logger->debug("offline: Fetching workstation $name");
340         my $ws = $U->storagereq(
341                 'open-ils.storage.direct.actor.workstation.search.name', $name);
342         ol_handle_result(OpenILS::Event->new('WORKSTATION_NOT_FOUND')) unless $ws;
343         return $ws;
344 }
345
346
347
348
349 # --------------------------------------------------------------------
350 # Sorts the script commands then forks a child to executes them.
351 # --------------------------------------------------------------------
352 sub ol_execute {
353
354         my $session = ol_find_session();
355         ol_handle_event('OFFLINE_SESSION_ACTIVE') if $session->in_process;
356         ol_handle_event('OFFLINE_SESSION_COMPLETE') if $session->end_time;
357
358         my $commands = ol_collect_commands();
359
360         # --------------------------------------------------------------------
361         # Note that we must disconnect from opensrf before forking or the 
362         # connection will be borked...
363         # --------------------------------------------------------------------
364         OpenSRF::Transport::PeerHandle->retrieve->disconnect;
365         $DB->disconnect;
366
367
368         if( safe_fork() ) {
369
370                 # --------------------------------------------------------------------
371                 # Tell the client all is well
372                 # --------------------------------------------------------------------
373                 ol_handle_event('SUCCESS'); # - this exits
374
375         } else {
376
377
378                 # --------------------------------------------------------------------
379                 # close stdout/stderr or apache will wait on the child to finish
380                 # --------------------------------------------------------------------
381                 close(STDOUT);
382                 close(STDERR);
383
384                 $logger->debug("offline: child $$ processing data...");
385
386                 # --------------------------------------------------------------------
387                 # The child re-connects to the opensrf network and processes
388                 # the script requests 
389                 # --------------------------------------------------------------------
390                 OpenSRF::System->bootstrap_client(config_file => $config{bootstrap});
391         
392                 try {
393
394                         #use Class::DBI
395                         #Class::DBI->autoupdate(1);
396
397                         $DB->autoupdate(1);
398
399                         my $sesion = ol_find_session();
400                         $session->in_process(1);
401                         ol_process_commands($session, $commands);
402                         ol_archive_files($session);
403
404                 } catch Error with {
405                         my $e = shift;
406                         $logger->error("offline: child process error $e");
407                 };
408         }
409 }
410
411 sub ol_file_to_perl {
412         my $fname = shift;
413         open(F, "$fname") or ol_handle_event('OFFLINE_FILE_ERROR');
414         my @d = <F>;
415         my @p;
416         push(@p, JSON->JSON2perl($_)) for @d;
417         close(F);
418         return \@p;
419 }
420
421 # collects the commands and sorts them on timestamp+delta
422 sub ol_collect_commands {
423         my $ses = ol_find_session();
424         my @commands;
425
426         # cycle through every script loaded to this session
427         for my $script ($ses->scripts) {
428                 my $coms = ol_file_to_perl($script->logfile);
429
430                 # cycle through all of the commands for this script
431                 for my $com (@$coms) {
432                         $$com{_workstation} = $script->workstation;
433                         $$com{_realtime} = $script->time_delta + $com->{timestamp};
434                         push( @commands, $com );
435                 }
436         }
437
438         # make sure thera are no blank commands
439         @commands = grep { ($_ and $_->{type}) } @commands;
440
441         # sort on realtime
442         @commands = sort { $a->{_realtime} <=> $b->{_realtime} } @commands;
443
444         # push user registrations to the front
445         my @regs                = grep { $_->{type} eq 'register' } @commands;
446         my @others      = grep { $_->{type} ne 'register' } @commands;
447
448         return [ @regs, @others ];
449 }
450
451 sub ol_date {
452         my $time = shift || CORE::time;
453         my (undef,undef, undef, $mday,$mon,$year) = localtime($time);
454         $mon++; $year   += 1900;
455         $mday   = "0$mday" unless $mday =~ /\d{2}/o;
456         $mon    = "0$mon" unless $mon   =~ /\d{2}/o;
457         return ($year, $mon, $mday);
458 }
459
460
461 # --------------------------------------------------------------------
462 # Moves all files from the pending directory to the archive directory
463 # and removes the pending directory
464 # --------------------------------------------------------------------
465 sub ol_archive_files {
466         my $session = shift;
467         my ($y, $m, $d) = ol_date();
468
469         my $dir = "$basedir/pending/$seskey";
470         my $archdir = "$basedir/archive/$org/$seskey";
471         $logger->debug("offline: archiving files to $archdir");
472
473         # Tell the db the files are moving
474         $_->logfile($archdir.'/'.$_->workstation.'.log') for ($session->scripts);
475
476         qx/mkdir -p $archdir/;
477         qx/mv $_ $archdir/ for <$dir/*>;
478         qx/rmdir $dir/;
479 }
480
481
482 # --------------------------------------------------------------------
483 # Appends results to the results file.
484 # --------------------------------------------------------------------
485 my $rhandle;
486 sub ol_append_result {
487
488         my $obj = shift;
489         my $last = shift;
490
491         $obj = JSON->perl2JSON($obj);
492
493         if(!$rhandle) {
494                 open($rhandle, ">>$basedir/pending/$seskey/results") 
495                         or ol_handle_event('OFFLINE_FILE_ERROR');
496         }
497
498         print $rhandle "$obj\n";
499         close($rhandle) if $last;
500 }
501
502
503
504 # --------------------------------------------------------------------
505 # Runs the commands and returns the list of errors
506 # --------------------------------------------------------------------
507 sub ol_process_commands {
508
509         my $session      = shift;
510         my $commands = shift;
511         my $x        = 0;
512
513         $session->start_time(CORE::time);
514
515         for my $d ( @$commands ) {
516
517                 my $t           = $d->{type};
518                 my $last = ($x++ == scalar(@$commands) - 1) ? 1 : 0;
519                 my $res = { command => $d };
520
521                 $res->{event} = ol_handle_checkin($d)   if $t eq 'checkin';
522                 $res->{event} = ol_handle_inhouse($d)   if $t eq 'in_house_use';
523                 $res->{event} = ol_handle_checkout($d) if $t eq 'checkout';
524                 $res->{event} = ol_handle_renew($d)             if $t eq 'renew';
525                 $res->{event} = ol_handle_register($d) if $t eq 'register';
526
527
528                 ol_append_result($res, $last);
529                 $session->num_complete( $session->num_complete + 1 );
530
531                 $logger->debug("offline: set session [".$session->key."] num_complete to ".$session->num_complete);
532         }
533
534         $session->end_time(CORE::time);
535         $session->in_process(0);
536 }
537
538
539 # --------------------------------------------------------------------
540 # Runs an in_house_use action
541 # --------------------------------------------------------------------
542 sub ol_handle_inhouse {
543
544         my $command             = shift;
545         my $realtime    = $command->{_realtime};
546         my $ws                  = $command->{_workstation};
547         my $barcode             = $command->{barcode};
548         my $count               = $command->{count} || 1;
549         my $use_time    = $command->{use_time} || "";
550
551         $logger->activity("offline: in_house_use : requestor=". $requestor->id.", realtime=$realtime, ".  
552                 "workstation=$ws, barcode=$barcode, count=$count, use_time=$use_time");
553
554         my $ids = $U->simplereq(
555                 'open-ils.circ', 
556                 'open-ils.circ.in_house_use.create', $authtoken, 
557                 { barcode => $barcode, count => $count, location => &offline_org, use_time => $use_time } );
558         
559         return OpenILS::Event->new('SUCCESS', payload => $ids) if( ref($ids) eq 'ARRAY' );
560         return $ids;
561 }
562
563
564
565 # --------------------------------------------------------------------
566 # Pulls the relevant circ args from the command, fetches data where 
567 # necessary
568 # --------------------------------------------------------------------
569 sub ol_circ_args_from_command {
570         my $command = shift;
571
572         my $type                        = $command->{type};
573         my $realtime    = $command->{_realtime};
574         my $ws                  = $command->{_workstation};
575         my $barcode             = $command->{barcode} || "";
576         my $cotime              = $command->{checkout_time} || "";
577         my $pbc                 = $command->{patron_barcode};
578         my $due_date    = $command->{due_date} || "";
579         my $noncat              = ($command->{noncat}) ? "yes" : "no"; # for logging
580
581         $logger->activity("offline: $type : requestor=". $requestor->id.
582                 ", realtime=$realtime, workstation=$ws, checkout_time=$cotime, ".
583                 "patron=$pbc, due_date=$due_date, noncat=$noncat");
584
585         my $args = { 
586                 permit_override => 1, 
587                 barcode                         => $barcode,            
588                 checkout_time           => $cotime, 
589                 patron_barcode          => $pbc,
590                 due_date                                => $due_date };
591
592         if( $command->{noncat} ) {
593                 $args->{noncat} = 1;
594                 $args->{noncat_type} = $command->{noncat_type};
595                 $args->{noncat_count} = $command->{noncat_count};
596         }
597
598         return $args;
599 }
600
601
602
603 # --------------------------------------------------------------------
604 # Performs a checkout action
605 # --------------------------------------------------------------------
606 sub ol_handle_checkout {
607         my $command     = shift;
608         my $args = ol_circ_args_from_command($command);
609         return $U->simplereq(
610                 'open-ils.circ', 'open-ils.circ.checkout', $authtoken, $args );
611 }
612
613
614 # --------------------------------------------------------------------
615 # Performs the renewal action
616 # --------------------------------------------------------------------
617 sub ol_handle_renew {
618         my $command = shift;
619         my $args = ol_circ_args_from_command($command);
620         my $t = time;
621         return $U->simplereq(
622                 'open-ils.circ', 'open-ils.circ.renew', $authtoken, $args );
623 }
624
625
626 # --------------------------------------------------------------------
627 # Runs a checkin action
628 # --------------------------------------------------------------------
629 sub ol_handle_checkin {
630
631         my $command             = shift;
632         my $realtime    = $command->{_realtime};
633         my $ws                  = $command->{_workstation};
634         my $barcode             = $command->{barcode};
635         my $backdate    = $command->{backdate} || "";
636
637         $logger->activity("offline: checkin : requestor=". $requestor->id.
638                 ", realtime=$realtime, ".  "workstation=$ws, barcode=$barcode, backdate=$backdate");
639
640         return $U->simplereq(
641                 'open-ils.circ', 
642                 'open-ils.circ.checkin', $authtoken,
643                 { barcode => $barcode, backdate => $backdate } );
644 }
645
646
647
648 # --------------------------------------------------------------------
649 # Registers a new patron
650 # --------------------------------------------------------------------
651 sub ol_handle_register {
652         my $command = shift;
653
654         my $barcode = $command->{user}->{card}->{barcode};
655         delete $command->{user}->{card}; 
656
657         $logger->info("offline: creating new user with barcode $barcode");
658
659         # now, create the user
660         my $actor       = Fieldmapper::actor::user->new;
661         my $card                = Fieldmapper::actor::card->new;
662
663
664         # username defaults to the barcode
665         $actor->usrname( ($actor->usrname) ? $actor->usrname : $barcode );
666
667         # Set up all of the virtual IDs, isnew, etc.
668         $actor->isnew(1);
669         $actor->id(-1);
670         $actor->card(-1);
671         $actor->cards([$card]);
672
673         $card->isnew(1);
674         $card->id(-1);
675         $card->usr(-1);
676         $card->barcode($barcode);
677
678         my $billing_address;
679         my $mailing_address;
680
681         my @sresp;
682         for my $resp (@{$command->{user}->{survey_responses}}) {
683                 my $sr = Fieldmapper::action::survey_response->new;
684                 $sr->$_( $resp->{$_} ) for keys %$resp;
685                 $sr->isnew(1);
686                 $sr->usr(-1);
687                 push(@sresp, $sr);
688                 $logger->debug("offline: created new survey response for survey ".$sr->survey);
689         }
690         delete $command->{user}->{survey_responses};
691         $actor->survey_responses(\@sresp) if @sresp;
692
693         # extract the billing address
694         if( my $addr = $command->{user}->{billing_address} ) {
695                 $billing_address = Fieldmapper::actor::user_address->new;
696                 $billing_address->$_($addr->{$_}) for keys %$addr;
697                 $billing_address->isnew(1);
698                 $billing_address->id(-1);
699                 $billing_address->usr(-1);
700                 delete $command->{user}->{billing_address};
701                 $logger->debug("offline: read billing address ".$billing_address->street1);
702         }
703
704         # extract the mailing address
705         if( my $addr = $command->{user}->{mailing_address} ) {
706                 $mailing_address = Fieldmapper::actor::user_address->new;
707                 $mailing_address->$_($addr->{$_}) for keys %$addr;
708                 $mailing_address->isnew(1);
709                 $mailing_address->id(-2);
710                 $mailing_address->usr(-1);
711                 delete $command->{user}->{mailing_address};
712                 $logger->debug("offline: read mailing address ".$mailing_address->street1);
713         }
714
715         # make sure we have values for both
716         $billing_address ||= $mailing_address;
717         $mailing_address ||= $billing_address;
718
719         $actor->billing_address($billing_address->id);
720         $actor->mailing_address($mailing_address->id);
721         $actor->addresses([$mailing_address]);
722
723         push( @{$actor->addresses}, $billing_address ) 
724                 unless $billing_address->id eq $mailing_address->id;
725         
726         # pull all of the rest of the data from the command blob
727         $actor->$_( $command->{user}->{$_} ) for keys %{$command->{user}};
728
729         $logger->debug("offline: creating user object...");
730         $actor = $U->simplereq(
731                 'open-ils.actor', 
732                 'open-ils.actor.patron.update', $authtoken, $actor);
733
734         return $actor if(ref($actor) eq 'HASH'); # an event occurred
735
736         return OpenILS::Event->new('SUCCESS', payload => $actor);
737 }
738
739
740
741
742
743
744