+
+
+
+
+ [% IF ( op ) %]
+ [% IF ( op == "new" ) || ( op == "edit" ) %]
+ [%# If step is undefined, force it to be step one %]
+ [% IF ( ! step ); step = "one"; END; %]
+
+
+
+ [%# HEADING %]
+ [% IF ( op == "new" ) %]
+
New saved task
+ [% ELSIF ( op == "edit" ) %]
+
Modify saved task
+ [% END %]
+ [%# /HEADING %]
+
+ [%# TODO: Get this working properly...
Validation failed.
#]
+
+ [%# FORM %]
+
+ [%# /FORM %]
+ [% END #/edit or new %]
+
+
+ [% IF ( op == "list" ) %]
+
+
Saved tasks
+ [% IF ( saved_response ) %]
+ [% IF ( saved_response == 'delete_success' ) %]
+
Deletion successful.
+ [% ELSIF ( saved_response == 'delete_failure' ) %]
+
Deletion failed.
+ [% END %]
+ [% END %]
+
+
+
+ Start time |
+ Repeat interval |
+ Task type |
+ Params |
+ |
+ |
+ |
+
+
+
+ [% FOREACH saved_task IN saved_tasks %]
+
+ [% IF ( saved_task.start_time ) != "0000-00-00 00:00:00"; saved_task.start_time; END; %] |
+ [% saved_task.repeat_interval %] |
+ [% saved_task.task_type %] |
+ [% saved_task.params %] |
+ Edit |
+ Send to Icarus |
+ Delete |
+
+ [% END %]
+
+
+
+
Icarus dashboard
+
+
+ Status |
+ |
+
+
+
+
+ [% IF ( daemon_status == 'Permission denied' ) #Apache doesn't have permission to write to socket
+ || ( daemon_status == 'Connection refused' ) #Socket exists, but server is down
+ || ( daemon_status == 'No such file or directory' ) #Socket doesn't exist at all
+ %]
+ Unable to contact
+ [% ELSIF ( daemon_status == 'online' ) %]
+ Online
+ [% ELSIF ( daemon_status == 'shutting down' ) %]
+ Shutting down
+ [% ELSE %]
+ [% daemon_status %]
+ [% END %]
+ |
+ [%# TODO: Also provide controls for starting/restarting Icarus? %]
+ Shutdown Icarus |
+
+
+
+
+
Active Icarus tasks
+ [% IF ( task_response ) %]
+ [% IF ( task_response.action == 'error' ) %]
+ [% IF ( task_response.error_message ) %]
+ [% IF ( task_response.error_message == 'No such process' ) %]
+
Task [% task_response.task_id %] does not exist.
+ [% END %]
+ [% END %]
+ [% ELSIF ( task_response.action == 'pending' ) %]
+
Initialising task [% task_response.task_id %].
+ [% ELSIF ( task_response.action == 'already pending' ) %]
+
Already initialised task [% task_response.task_id %].
+ [% ELSIF ( task_response.action == 'already started' ) %]
+
Already started task [% task_response.task_id %].
+ [% ELSIF ( task_response.action == 'removed' ) %]
+
Removing task [% task_response.task_id %].
+ [% END %]
+ [% END %]
+ [% IF ( tasks ) %]
+
+
+
+ Task id |
+ Status |
+ Next start time (local server time) |
+ Type |
+ Repeat interval (seconds) |
+ |
+ |
+
+
+
+ [% FOREACH task IN tasks %]
+
+ [% task.task_id %] |
+
+ [% SWITCH task.task.status %]
+ [% CASE 'new' %]
+ New
+ [% CASE 'pending' %]
+ Pending
+ [% CASE 'started' %]
+ Started
+ [% CASE 'stopping' %]
+ Stopping
+ [% CASE %]
+ [% task.task.status %]
+ [% END %]
+ |
+ [% task.task.start %] |
+ [% task.task.type %] |
+ [% task.task.repeat_interval %] |
+ Start |
+ Remove |
+
+ [% END %]
+
+
+ [% END %]
+
+ [% END #/list %]
+ [% END #/op %]
+
+
+
+ [% INCLUDE 'admin-menu.inc' %]
+
+
+[% INCLUDE 'intranet-bottom.inc' %]
--- a/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slimFromOAI.xsl
+++ a/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slimFromOAI.xsl
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 8
+
+ 037
+
+
+ a
+
+
+
+
+ b
+ OAI-PMH
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
--- a/koha-tmpl/intranet-tmpl/prog/en/xslt/OAI2MARC21slim.xsl
+++ a/koha-tmpl/intranet-tmpl/prog/en/xslt/OAI2MARC21slim.xsl
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 7
+
+ 024
+
+
+ a
+
+
+
+
+ 2
+ uri
+
+
+
+
+
+
+
+
+
+
+
+
+
+
--- a/koha-tmpl/intranet-tmpl/prog/en/xslt/StripOAIWrapper.xsl
+++ a/koha-tmpl/intranet-tmpl/prog/en/xslt/StripOAIWrapper.xsl
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
--- a/misc/bin/icarusd.pl
+++ a/misc/bin/icarusd.pl
@@ -0,0 +1,257 @@
+#!/usr/bin/perl
+
+#######################################################################
+
+use Modern::Perl;
+use POSIX; #For daemonizing
+use Fcntl qw(:flock); #For pidfile
+use Getopt::Long;
+
+#Make the STDOUT filehandle hot, so that you can use shell re-direction. Otherwise, you'll suffer from buffering.
+STDOUT->autoflush(1);
+#Note that STDERR, by default, is already hot.
+
+#######################################################################
+#FIXME: Debugging signals
+#BEGIN {
+# package POE::Kernel;
+# use constant TRACE_SIGNALS => 1;
+#}
+use Data::Dumper; #FIXME: Remove this line
+
+use POE;
+use JSON; #For Listener messages
+use XML::LibXML; #For configuration files
+
+use Koha::Icarus::Listener;
+
+#######################################################################
+
+my ($filename,$daemon,$log);
+GetOptions (
+ "f|file|filename=s" => \$filename, #/kohawebs/dev/dcook/koha-dev/etc/koha-conf.xml
+ "l|log=s" => \$log,
+ "d|daemon" => \$daemon,
+) or die("Error in command line arguments\n");
+
+#Declare the variable with file scope so the flock stays for the duration of the process's life
+my $pid_filehandle;
+
+#Read configuration file
+my $config = read_config_file($filename);
+
+my $SOCK_PATH = $config->{socket};
+my $pid_file = $config->{pidfile};
+my $max_tasks = $config->{max_tasks};
+
+#Overwrite configuration file with command line options
+if ($log){
+ $config->{log} = $log;
+}
+
+#Go into daemon mode, if user has included flag
+if ($daemon){
+ daemonize();
+}
+
+if ($pid_file){
+ #NOTE: The filehandle needs to have file scope, so that the flock is preserved.
+ $pid_filehandle = make_pid_file($pid_file);
+}
+
+#FIXME: Do we want to log to file only in daemon mode? $config->{log} should be populated by either the config file or the l|log GetOpt...
+if ($daemon && $config->{log}){
+ log_to_file($config->{log});
+}
+
+
+#FIXME: 1) In daemon mode, SIGUSR1 or SIGHUP for reloading/restarting?
+#######################################################################
+
+#Creates Icarus Listener
+Koha::Icarus::Listener->spawn({
+ Alias => "listener",
+ Socket => $SOCK_PATH,
+ ClientInput => \&on_client_input,
+ MaxTasks => $max_tasks,
+ Verbosity => 1,
+});
+
+POE::Kernel->run();
+
+exit;
+
+sub on_client_input {
+ my ($input, $wheel_id, $session, $kernel, $heap) = @_[ARG0, ARG1, SESSION, KERNEL, HEAP];
+
+ #Store server id more explicitly
+ my $server_id = $session->ID;
+
+ #Server listener has received input from client
+ my $client = $heap->{client}->{$wheel_id};
+
+ #FIXME: you probably don't want to log this as it can have auth info...
+ #Log the raw client input
+ say "Input = $input";
+
+ #Parse input from client
+ my $message = from_json($input);
+
+ if ( ref $message eq 'HASH' ){
+ #Read "command" from client
+ if (my $command = $message->{command}){
+ if ($command eq 'add task'){
+ my $output = {};
+
+ #Create a task session
+ eval {
+ #NOTE: The server automatically keeps track of its child tasks
+ my $task_id = $kernel->call($server_id,"got_add_task",$message);
+
+ $output->{action} = "added";
+ $output->{task_id} = $task_id;
+ };
+ if ($@){
+ warn $@;
+ chomp($@);
+ $output->{action} = "error";
+ $output->{error_message} = $@;
+ }
+ my $server_output = to_json($output);
+ $client->put($server_output);
+ return;
+
+ } elsif ( ($command eq 'remove task') || ($command eq 'start task' ) ){
+
+ my $task_id = $message->{task_id};
+
+ my $output = {
+ task_id => $task_id,
+ };
+
+ if ($command eq 'remove task'){
+ $kernel->call($task_id,"got_task_stop");
+ $output->{action} = "removed";
+ } elsif ($command eq 'start task'){
+ my $response = $kernel->call($task_id, "on_task_init");
+ $output->{action} = $response;
+ }
+
+ if ($!){
+ $output->{action} = "error";
+ $output->{error_message} = $!;
+ }
+
+ #FIXME: What do we actually want to send back to the client?
+ my $server_output = to_json($output);
+ $client->put($server_output);
+ return;
+
+ } elsif ($command eq 'list tasks'){
+
+ #Get tasks from listener (ie self)
+ my $tasks = $kernel->call($server_id, "got_list_tasks");
+
+ #Prepare output for client
+ my $server_output = to_json({tasks => $tasks}, {pretty => 1});
+
+ #Send output to client
+ $client->put($server_output);
+ return;
+
+ } elsif ($command eq 'shutdown'){
+ $kernel->post($server_id, "graceful_shutdown");
+ my $server_output = to_json({action => 'shutting down'});
+ $client->put($server_output);
+ return;
+ } else {
+ say "The message contained an invalid command!";
+ $client->put("Sorry! That is an invalid command!");
+ return;
+ }
+ } else {
+ say "The message was missing a command!";
+ }
+ } else {
+ say "The message was malformed!";
+ }
+ $client->put("Sorry! That is an invalid message!");
+ return;
+}
+
+sub read_config_file {
+ my $filename = shift;
+ my $config = {};
+ if ( -e $filename ){
+ eval {
+ my $doc = XML::LibXML->load_xml(location => $filename);
+ if ($doc){
+ my $root = $doc->documentElement;
+ my $icarus = $root->find('icarus')->shift;
+ if ($icarus){
+ #Get all child nodes for the 'icarus' element
+ my @childnodes = $icarus->childNodes();
+ foreach my $node (@childnodes){
+ #Only consider nodes that are elements
+ if ($node->nodeType == XML_ELEMENT_NODE){
+ my $config_key = $node->nodeName;
+ my $first_child = $node->firstChild;
+ #Only consider nodes that have a text node as their first child
+ if ($first_child && $first_child->nodeType == XML_TEXT_NODE){
+ $config->{$config_key} = $first_child->nodeValue;
+ }
+ }
+ }
+ }
+ }
+ };
+ }
+ return $config;
+}
+
+#######################################################################
+#NOTE: On Debian, you can use the daemon binary to make a process into a daemon,
+# the following subs are for systems that don't have the daemon binary.
+
+sub daemonize {
+ my $pid = fork;
+ die "Couldn't fork: $!" unless defined($pid);
+ if ($pid){
+ exit; #Parent exit
+ }
+ POSIX::setsid() or die "Can't start a new session: $!";
+}
+
+sub log_to_file {
+ my $logfile = shift;
+ #Open a filehandle to append to a log file
+ open(LOG, '>>', $logfile) or die "Unable to open a filehandle for $logfile: $!\n"; # --output
+ LOG->autoflush(1); #Make filehandle hot (ie don't buffer)
+ *STDOUT = *LOG; #Re-assign STDOUT to LOG | --stdout
+ *STDERR = *STDOUT; #Re-assign STDERR to STDOUT | --stderr
+}
+
+sub make_pid_file {
+ my $pidfile = shift;
+ if ( ! -e $pidfile ){
+ open(my $fh, '>', $pidfile) or die "Unable to write to $pidfile: $!\n";
+ $fh->close;
+ }
+
+ open(my $pidfilehandle, '+<', $pidfile) or die "Unable to open a filehandle for $pidfile: $!\n";
+ if (flock($pidfilehandle, LOCK_EX|LOCK_NB)){
+ #Write pid to pidfile
+ print "Acquiring lock on $pidfile\n";
+ #Now that we've acquired a lock, let's truncate the file
+ truncate($pidfilehandle, 0);
+ print $pidfilehandle $$."\n" or die $!;
+ #Flush the filehandle so you're not suffering from buffering
+ $pidfilehandle->flush();
+ return $pidfilehandle;
+ } else {
+ my $number = <$pidfilehandle>;
+ chomp($number);
+ warn "$0 is already running with pid $number. Exiting.\n";
+ exit(1);
+ }
+}
--- a/svc/import_oai
+++ a/svc/import_oai
@@ -0,0 +1,197 @@
+#!/usr/bin/perl
+
+# Copyright 2012 CatalystIT Ltd
+# Copyright 2016 Prosentient Systems
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see
.
+#
+
+use Modern::Perl;
+use XML::LibXML;
+use URI;
+use File::Basename;
+
+use CGI qw ( -utf8 );
+use C4::Auth qw/check_api_auth/;
+use C4::Context;
+use C4::ImportBatch;
+use C4::Matcher;
+use XML::Simple;
+
+my $query = new CGI;
+binmode STDOUT, ':encoding(UTF-8)';
+
+my ($status, $cookie, $sessionID) = check_api_auth($query, { editcatalogue => 'edit_catalogue'} );
+unless ($status eq "ok") {
+ print $query->header(-type => 'text/xml', -status => '403 Forbidden');
+ print XMLout({ auth_status => $status }, NoAttr => 1, RootName => 'response', XMLDecl => 1);
+ exit 0;
+}
+
+my $xml;
+if ($query->request_method eq "POST") {
+ $xml = $query->param('xml');
+}
+if ($xml) {
+ #TODO: You could probably use $query->Vars here instead...
+ my %params = map { $_ => $query->param($_) } $query->param;
+ my $result = import_oai($xml, \%params );
+ print $query->header(-type => 'text/xml');
+ print XMLout($result, NoAttr => 1, RootName => 'response', XMLDecl => 1);
+} else {
+ print $query->header(-type => 'text/xml', -status => '400 Bad Request');
+}
+
+exit 0;
+
+sub import_oai {
+ my ($inxml, $params) = @_;
+
+ my $result = {};
+
+ my $filter = delete $params->{filter} || '';
+ my $import_mode = delete $params->{import_mode} || '';
+ my $framework = delete $params->{framework} || '';
+
+ if (my $matcher_code = delete $params->{match}) {
+ $params->{matcher_id} = C4::Matcher::GetMatcherId($matcher_code);
+ }
+
+ my $batch_id = GetWebserviceBatchId($params);
+ unless ($batch_id) {
+ $result->{'status'} = "failed";
+ $result->{'error'} = "Batch create error";
+ return $result;
+ }
+
+ #Log it in the import_oai table here...
+
+ #Parse the XML string into a XML::LibXML object
+ my $doc = XML::LibXML->load_xml(string => $inxml);
+
+ #Get the root element
+ my $root = $doc->documentElement;
+
+ #Register namespaces for searching purposes
+ my $xpc = XML::LibXML::XPathContext->new();
+ $xpc->registerNs('oai','http://www.openarchives.org/OAI/2.0/');
+
+ my $xpath_identifier = XML::LibXML::XPathExpression->new("oai:header/oai:identifier");
+ my $identifier = $xpc->findnodes($xpath_identifier,$root)->shift;
+ my $identifier_string = $identifier->textContent;
+
+ my $xpath_datestamp = XML::LibXML::XPathExpression->new("oai:header/oai:datestamp");
+ my $datestamp = $xpc->findnodes($xpath_datestamp,$root)->shift;
+ my $datestamp_string = $datestamp->textContent;
+
+ my $status_string = "";
+
+ #OAI-PMH Header = identifier, datestamp, status, setSpec?
+ #OAI-PMH Metadata
+
+ my $log_dbh = C4::Context->dbh;
+ my $log_sql = "INSERT INTO import_oai (header_identifier, header_datestamp, header_status, metadata) VALUES (?, ?, ?, ?)";
+ my $log_sth = $log_dbh->prepare($log_sql);
+ $log_sth->execute($identifier_string,$datestamp_string,$status_string,$inxml);
+
+
+
+ #Filter the OAI-PMH record into a MARCXML record
+ my $metadata_xml;
+
+ #Source a default XSLT
+ my $htdocs = C4::Context->config('intrahtdocs');
+ my $theme = C4::Context->preference("template");
+ #FIXME: This doesn't work for UNIMARC!
+ my $xslfilename = "$htdocs/$theme/en/xslt/OAI2MARC21slim.xsl";
+
+ #FIXME: There's a better way to do these filters...
+ if ($filter){
+ my $filter_uri = URI->new($filter);
+ if ($filter_uri){
+ my $scheme = $filter_uri->scheme;
+ if ($scheme && $scheme eq "file"){
+ my $path = $filter_uri->path;
+ #Filters may theoretically be .xsl or .pm files
+ my($filename, $dirs, $suffix) = fileparse($path,(".xsl",".pm"));
+ if ($suffix && $suffix eq ".xsl"){
+ #If this new path exists, change the filter XSLT to it
+ if ( -f $path ){
+ $xslfilename = $path;
+ }
+ }
+ }
+ }
+ }
+
+ if ( -f $xslfilename ){
+ #FIXME: Ideally, it would be good to use Koha::XSLT_Handler here... (especially for persistent environments...)
+ my $xslt = XML::LibXSLT->new();
+ my $style_doc = XML::LibXML->load_xml(location => $xslfilename);
+ my $stylesheet = $xslt->parse_stylesheet($style_doc);
+ if ($stylesheet){
+ my $results = $stylesheet->transform($doc);
+ $metadata_xml = $stylesheet->output_as_bytes($results);
+ }
+ } else {
+ $result->{'status'} = "failed";
+ $result->{'error'} = "Metadata filter unavailable";
+ return $result;
+ }
+
+
+
+
+
+
+
+
+
+ #Import the MARCXML record into Koha
+ my $marcflavour = C4::Context->preference('marcflavour') || 'MARC21';
+ my $marc_record = eval {MARC::Record::new_from_xml( $metadata_xml, "utf8", $marcflavour)};
+ if ($@) {
+ $result->{'status'} = "failed";
+ $result->{'error'} = $@;
+ return $result;
+ }
+
+ my $import_record_id = AddBiblioToBatch($batch_id, 0, $marc_record, "utf8", int(rand(99999)));
+ my @import_items_ids = AddItemsToImportBiblio($batch_id, $import_record_id, $marc_record, 'UPDATE COUNTS');
+
+ my $matcher = C4::Matcher->new($params->{record_type} || 'biblio');
+ $matcher = C4::Matcher->fetch($params->{matcher_id});
+ my $number_of_matches = BatchFindDuplicates($batch_id, $matcher);
+
+ # XXX we are ignoring the result of this;
+ BatchCommitRecords($batch_id, $framework) if lc($import_mode) eq 'direct';
+
+ my $dbh = C4::Context->dbh();
+ my $sth = $dbh->prepare("SELECT matched_biblionumber FROM import_biblios WHERE import_record_id =?");
+ $sth->execute($import_record_id);
+ my $biblionumber=$sth->fetchrow_arrayref->[0] || '';
+ $sth = $dbh->prepare("SELECT overlay_status FROM import_records WHERE import_record_id =?");
+ $sth->execute($import_record_id);
+ my $match_status = $sth->fetchrow_arrayref->[0] || 'no_match';
+ my $url = 'http://'. C4::Context->preference('staffClientBaseURL') .'/cgi-bin/koha/catalogue/detail.pl?biblionumber='. $biblionumber;
+
+ $result->{'status'} = "ok";
+ $result->{'import_batch_id'} = $batch_id;
+ $result->{'match_status'} = $match_status;
+ $result->{'biblionumber'} = $biblionumber;
+ $result->{'url'} = $url;
+ return $result;
+}
--