3 # Copyright 2012 CatalystIT
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
25 my ($help, $config, $daemon);
28 'config|c=s' => \
$config,
29 'daemon|d' => \
$daemon,
33 if($help || !$config){
37 --daemon | -d - go to background; prints pid to stdout
38 --config | -c - config file
39 --help | -? - this message
45 # comments are supported
49 host - ip address or hostname to bind to, defaults all available
50 port - port to bind to, mandatory
51 log - log file path, stderr if omitted
52 debug - dumps requests to the log file, passwords inclusive
53 koha - koha intranet base url, eg http://librarian.koha
54 user - koha user, authentication
55 password - koha user password, authentication
56 match - marc_matchers.code: ISBN or ISSN
57 overlay_action - import_batches.overlay_action: replace, create_new or ignore
58 nomatch_action - import_batches.nomatch_action: create_new or ignore
59 item_action - import_batches.item_action: always_add,
60 add_only_for_matches, add_only_for_new or ignore
61 import_mode - stage or direct
62 framework - to be used if import_mode is direct
64 All process related parameters (all but ip and port) have default values as
65 per Koha import process.
71 my $server = ImportProxyServer
->new($config);
74 print $server->background;
82 package ImportProxyServer
;
89 use HTTP
::Status
qw(:constants);
98 use constant CLIENT_READ_TIMEOUT
=> 5;
99 use constant CLIENT_READ_BUFFER_SIZE
=> 100000;
100 use constant AUTH_URI
=> "/cgi-bin/koha/mainpage.pl";
101 use constant IMPORT_SVC_URI
=> "/cgi-bin/koha/svc/import_bib";
105 my $config_file = shift or croak
"No config file";
107 my $self = {time_to_die
=> 0, config_file
=> $config_file };
117 my $config_file = $self->{config_file
};
119 open (my $conf_fh, '<', $config_file) or die "Cannot open config file $config: $!";
126 s/\s*#.*//o; # remove comments
127 s/^\s+//o; # trim leading spaces
128 s/\s+$//o; # trim trailing spaces
131 my ($p, $v) = m/(\S+?):\s*(.*)/o;
132 die "Invalid config line $line: $_" unless defined $v;
137 $self->{koha
} = delete( $param{koha
} )
138 or die "No koha base url in config file";
139 $self->{user
} = delete( $param{user
} )
140 or die "No koha user in config file";
141 $self->{password
} = delete( $param{password
} )
142 or die "No koha user password in config file";
144 $self->{host
} = delete( $param{host
} );
145 $self->{port
} = delete( $param{port
} )
146 or die "Port not specified";
148 $self->{debug
} = delete( $param{debug
} );
151 close $self->{log_fh
} if $self->{log_fh
};
152 if (my $logfile = delete $param{log}) {
153 open ($log_fh, '>>', $logfile) or die "Cannot open $logfile for write: $!";
157 $self->{log_fh
} = $log_fh;
159 $self->{params
} = \
%param;
164 my $log_fh = $self->{log_fh
}
168 print $log_fh map "$t: $_\n", @_;
175 return ($pid) if $pid; # parent
177 die "Couldn't fork: $!" unless defined($pid);
179 POSIX
::setsid
() or die "Can't start a new session: $!";
181 $SIG{INT
} = $SIG{TERM
} = $SIG{HUP
} = sub { $self->{time_to_die
} = 1 };
182 # trap or ignore $SIG{PIPE}
183 $SIG{USR1
} = sub { $self->parse_config };
191 my $server_port = $self->{port
};
192 my $server_host = $self->{host
};
194 my $server = IO
::Socket
::INET
->new(
195 LocalHost
=> $server_host,
196 LocalPort
=> $server_port,
202 ) or die "Couldn't be a tcp server on port $server_port: $! $@";
204 $self->log("Started tcp listener on $server_host:$server_port");
209 my $client = $server->accept()
210 or die "Cannot accept: $!";
211 my $oldfh = select($client);
212 $self->handle_request($client);
214 last if $self->{time_to_die
};
221 my $ua = LWP
::UserAgent
->new;
228 my ( $self, $io ) = @_;
230 my ($in, @in_arr, $timeout, $bad_marc);
231 my $select = IO
::Select
->new($io) ;
232 while ( "FOREVER" ) {
233 if ( $select->can_read(CLIENT_READ_TIMEOUT
) ){
234 $io->recv($in, CLIENT_READ_BUFFER_SIZE
);
237 # XXX ignore after NULL
238 if ( $in =~ m/^(.*)\000/so ) { # null received, EOT
249 $in = join '', @in_arr;
253 my ($xml, $user, $password, $local_user);
254 my $data = $in; # copy for diagmostic purposes
256 my $first = substr( $data, 0, 1 );
257 if (!defined $first) {
260 $first eq 'U' && do {
261 ($user, $data) = _trim_identifier
($data);
264 $first eq 'A' && do {
265 ($local_user, $data) = _trim_identifier
($data);
268 $first eq 'P' && do {
269 ($password, $data) = _trim_identifier
($data);
272 $first eq ' ' && do {
273 $data = substr( $data, 1 ); # trim
276 $data =~ m/^[0-9]/ && do {
277 # What we have here might be a MARC record...
279 eval { $marc_record = MARC
::Record
->new_from_usmarc($data); };
284 $xml = $marc_record->as_xml();
288 last; # unexpected input
292 push @details, "Timeout" if $timeout;
293 push @details, "Bad MARC" if $bad_marc;
294 push @details, "User: $user" if $user;
295 push @details, "Password: " . ( $self->{debug
} ?
$password : ("x" x
length($password)) ) if $password;
296 push @details, "Local user: $local_user" if $local_user;
297 push @details, "XML: $xml" if $xml;
298 push @details, "Remaining data: $data" if ($data && !$xml);
300 $self->log("Invalid request", $in, @details);
304 $self->log("Request", @details);
305 $self->log($in) if $self->{debug
};
306 return ($xml, $user, $password);
309 sub _trim_identifier
{
310 #my ($a, $len) = unpack "cc", substr( $_[0], 0, 2 );
311 my $len=ord(substr ($_[0], 1, 1)) - 64;
312 if ($len <0) { #length is numeric, and thus comes from the web client, not the desktop client.
315 return ( substr( $_[0], length($len)+1 , $len ), substr( $_[0], length($len) + 1 + $len ) );
317 return ( substr( $_[0], 2 , $len ), substr( $_[0], 2 + $len ) );
321 my ( $self, $io ) = @_;
323 my ($data, $user, $password) = $self->read_request($io)
324 or return $self->error_response("Bad request");
328 $user = $self->{user
};
329 $password = $self->{password
};
333 $ua = _ua
(); # fresh one, needs to authenticate
336 my $base_url = $self->{koha
};
337 my $resp = $ua->post( $base_url.IMPORT_SVC_URI
,
338 {'nomatch_action' => $self->{params
}->{nomatch_action
},
339 'overlay_action' => $self->{params
}->{overlay_action
},
340 'match' => $self->{params
}->{match
},
341 'import_mode' => $self->{params
}->{import_mode
},
342 'framework' => $self->{params
}->{framework
},
343 'item_action' => $self->{params
}->{item_action
},
346 my $status = $resp->code;
347 if ($status == HTTP_UNAUTHORIZED
|| $status == HTTP_FORBIDDEN
) {
348 my $user = $self->{user
};
349 my $password = $self->{password
};
350 $resp = $ua->post( $base_url.AUTH_URI
, { userid
=> $user, password
=> $password } );
351 $resp = $ua->post( $base_url.IMPORT_SVC_URI
,
352 {'nomatch_action' => $self->{params
}->{nomatch_action
},
353 'overlay_action' => $self->{params
}->{overlay_action
},
354 'match' => $self->{params
}->{match
},
355 'import_mode' => $self->{params
}->{import_mode
},
356 'framework' => $self->{params
}->{framework
},
357 'item_action' => $self->{params
}->{item_action
},
359 if $resp->is_success;
361 unless ($resp->is_success) {
362 $self->log("Unsuccessful request", $resp->request->as_string, $resp->as_string);
363 return $self->error_response("Unsuccessful request");
366 my ($koha_status, $bib, $overlay, $batch_id, $error, $url);
367 if ( my $r = eval { XMLin
($resp->content) } ) {
368 $koha_status = $r->{status
};
369 $batch_id = $r->{import_batch_id
};
370 $error = $r->{error
};
371 $bib = $r->{biblionumber
};
372 $overlay = $r->{match_status
};
376 $koha_status = "error";
377 $self->log("Response format error:\n$resp->content");
378 return $self->error_response("Invalid response");
381 if ($koha_status eq "ok") {
382 my $response_string = sprintf( "Success. Batch number %s - biblio record number %s",
384 $response_string .= $overlay eq 'no_match' ?
' added to Koha.' : ' overlaid by import.';
385 $response_string .= "\n\n$url";
387 return $self->response( $response_string );
390 return $self->error_response( sprintf( "%s. Please contact administrator.", $error ) );
400 $self->log("Response: $_[0]");