From 506c9304072425ab611b4c35914c051738f19103 Mon Sep 17 00:00:00 2001 From: Magnus Enger Date: Wed, 17 Sep 2014 10:16:45 +0200 Subject: [PATCH] Bug 11401 - Add support for Norwegian national library card This patch makes it possible to sync patron data between Koha and the Norwegian national patron database, in both directions. In order to use this, the following information is necessary: - a username/password from the Norwegian national database of libraries ("Base Bibliotek"), available to all Norwegian libraries - a special key in order to decrypt and encrypt PIN-codes/passwords, which is only available to Norwegian library system vendors - a norwegian library vendor username/password See http://www.lanekortet.no/ for more information (in Norwegian). While this is of course an implementation of a specific synchronization scheme for borrower data, attempts have been made to prepare the ground for other sync schemes that might be implemented later. Especially the structure of the new borrower_sync table might be reviewed with an eye to how it might fit other schemes. To test: Since the password and cryptographic key needed to use this functionality is only available to Norwegian library system vendors, only regression testing can be done on the submitted code. Suggested things to check: - Apply the patch and make sure the database update is done. This should add the new "borrower_sync" table and five new systmpreferences under the "Patrons" > "Norwegian patron database" category: - NorwegianPatronDBEnable - NorwegianPatronDBEndpoint - NorwegianPatronDBUsername - NorwegianPatronDBPassword - NorwegianPatronDBSearchNLAfterLocalHit - Check that patrons can be created, edited and deleted as usual, when NorwegianPatronDBEnable is set to "Disable" - Check that the new tests in t/NorwegianPatronDB.pm run ok, e.g. on a gitified setup: $ sudo koha-shell -c "PERL5LIB=/path/to/kohaclone prove -v t/NorwegianPatronDB.t" instancename - Check that all the other tests still run ok - Check that the POD in the new files itroduced by this patch looks ok: - Koha/NorwegianPatronDB.pm - members/nl-search.pl - misc/cronjobs/nl-sync-from-koha.pl - misc/cronjobs/nl-sync-to-koha.pl - t/NorwegianPatronDB.t Sponsored-by: Oslo Public Library Update 2014-09-18: - Rebase on master - Split out changes to Koha::Schema - Incorporate new way of authenticating with NL Update 2014-10-21: - Rebase on master - Use Module::Load to load Koha::NorwegianPatronDB in non-NL-specific scripts and modules - Fix the version number of Digest::SHA - Fix a missing semicolon in kohastructure.sql --- C4/Installer/PerlDependencies.pm | 20 + C4/Members.pm | 44 +- Koha/NorwegianPatronDB.pm | 679 ++++++++++++++++++++ installer/data/mysql/kohastructure.sql | 18 + installer/data/mysql/sysprefs.sql | 5 + installer/data/mysql/updatedatabase.pl | 25 + .../prog/en/includes/members-toolbar.inc | 48 +- .../prog/en/includes/nl-search-form.tt | 9 + .../prog/en/modules/admin/preferences/patrons.pref | 22 + .../prog/en/modules/members/deletemem.tt | 42 +- .../prog/en/modules/members/member.tt | 6 + .../prog/en/modules/members/memberentrygen.tt | 13 + .../prog/en/modules/members/moremember.tt | 10 + .../prog/en/modules/members/nl-search.tt | 176 +++++ members/deletemem.pl | 28 +- members/memberentry.pl | 19 +- members/moremember.pl | 14 + members/nl-search.pl | 161 +++++ misc/cronjobs/nl-sync-from-koha.pl | 184 ++++++ misc/cronjobs/nl-sync-to-koha.pl | 192 ++++++ t/00-load.t | 1 + t/NorwegianPatronDB.t | 595 +++++++++++++++++ 22 files changed, 2288 insertions(+), 23 deletions(-) create mode 100644 Koha/NorwegianPatronDB.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/nl-search-form.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/members/nl-search.tt create mode 100755 members/nl-search.pl create mode 100644 misc/cronjobs/nl-sync-from-koha.pl create mode 100644 misc/cronjobs/nl-sync-to-koha.pl create mode 100644 t/NorwegianPatronDB.t diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index 376985e..4ee4f66 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -707,6 +707,26 @@ our $PERL_DEPS = { 'required' => '0', 'min_ver' => '0.42', }, + 'SOAP::Lite' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '0.712', + }, + 'Crypt::GCrypt' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '1.24', + }, + 'Convert::BaseN' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '0.01', + }, + 'Digest::SHA' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '5.61', + }, }; 1; diff --git a/C4/Members.pm b/C4/Members.pm index e3a5538..5b3457f 100644 --- a/C4/Members.pm +++ b/C4/Members.pm @@ -33,15 +33,20 @@ use C4::Accounts; use C4::Biblio; use C4::Letters; use C4::SQLHelper qw(InsertInTable UpdateInTable SearchInTable); -use C4::Members::Attributes qw(SearchIdMatchingAttribute); +use C4::Members::Attributes qw(SearchIdMatchingAttribute UpdateBorrowerAttribute); use C4::NewsChannels; #get slip news use DateTime; use DateTime::Format::DateParse; +use Koha::Database; use Koha::DateUtils; use Koha::Borrower::Debarments qw(IsDebarred); use Text::Unaccent qw( unac_string ); use Koha::AuthUtils qw(hash_password); use Koha::Database; +use Module::Load; +if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + load Koha::NorwegianPatronDB, qw( NLUpdateHashedPIN NLEncryptPIN NLSync ); +} our ($VERSION,@ISA,@EXPORT,@EXPORT_OK,$debug); @@ -783,6 +788,10 @@ sub ModMember { if ($data{password} eq '****' or $data{password} eq '') { delete $data{password}; } else { + if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it + NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} ); + } $data{password} = hash_password($data{password}); } } @@ -803,6 +812,25 @@ sub ModMember { AddEnrolmentFeeIfNeeded( $data{categorycode}, $data{borrowernumber} ); } + # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a + # cronjob will use for syncing with NL + if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $data{'borrowernumber'} + }); + # Do not set to "edited" if syncstatus is "new". We need to sync as new before + # we can sync as changed. And the "new sync" will pick up all changes since + # the patron was created anyway. + if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) { + $borrowersync->update( { 'syncstatus' => 'edited' } ); + } + # Set the value of 'sync' + $borrowersync->update( { 'sync' => $data{'sync'} } ); + # Try to do the live sync + NLSync({ 'borrowernumber' => $data{'borrowernumber'} }); + } + logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog"); } return $execute_success; @@ -845,11 +873,25 @@ sub AddMember { : $patron_category->default_privacy() eq 'never' ? 2 : $patron_category->default_privacy() eq 'forever' ? 0 : undef; + # Make a copy of the plain text password for later use + my $plain_text_password = $data{'password'}; # create a disabled account if no password provided $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!'; $data{'borrowernumber'}=InsertInTable("borrowers",\%data); + # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a + # cronjob will use for syncing with NL + if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + Koha::Database->new->schema->resultset('BorrowerSync')->create({ + 'borrowernumber' => $data{'borrowernumber'}, + 'synctype' => 'norwegianpatrondb', + 'sync' => 1, + 'syncstatus' => 'new', + 'hashed_pin' => NLEncryptPIN( $plain_text_password ), + }); + } + # mysql_insertid is probably bad. not necessarily accurate and mysql-specific at best. logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog"); diff --git a/Koha/NorwegianPatronDB.pm b/Koha/NorwegianPatronDB.pm new file mode 100644 index 0000000..51955eb --- /dev/null +++ b/Koha/NorwegianPatronDB.pm @@ -0,0 +1,679 @@ +package Koha::NorwegianPatronDB; + +# Copyright 2014 Oslo Public Library +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=head1 NAME + +Koha::NorwegianPatronDB + +=head1 SYNOPSIS + + use Koha::NorwegianPatronDB; + +=head1 CONDITIONAL LOADING + +This module depends on some Perl modules that have not been marked as required. +This is because the module only will be of interest to Norwegian libraries, and +it seems polite not to bother the rest of the world with these modules. It is +also good practice to check that the module is actually needed before loading +it. So in a NorwegianPatronDB page or script it will be OK to just do: + + use Koha::NorwegianPatronDB qw(...); + +But in scripts that are also used by others (like e.g. moremember.pl), it will +be polite to only load the module at runtime, if it is needed: + + use Module::Load; + if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + load Koha::NorwegianPatronDB, qw( NLGetSyncDataFromBorrowernumber ); + } + +(Module::Load::Conditional is used for this in other parts of Koha, but it does +not seem to allow for a list of subroutines to import, so Module::Load looks +like a better candidate.) + +=head1 FUNCTIONS + +=cut + +use Modern::Perl; +use C4::Context; +use C4::Members::Attributes qw( UpdateBorrowerAttribute ); +use SOAP::Lite; +use Crypt::GCrypt; +use Digest::SHA qw( sha256_hex ); +use Convert::BaseN; +use DateTime; + +use base 'Exporter'; +use version; our $VERSION = qv('1.0.0'); + +our %EXPORT_TAGS = ( all => [qw( + NLCheckSysprefs + NLSearch + NLSync + NLGetChanged + NLMarkForDeletion + NLDecodePin + NLEncryptPIN + NLUpdateHashedPIN + NLGetFirstname + NLGetSurname + NLGetSyncDataFromBorrowernumber +)] ); +Exporter::export_ok_tags('all'); + +my $nl_uri = 'http://lanekortet.no'; +my $nl_proxy = C4::Context->preference("NorwegianPatronDBEndpoint"); + +=head2 SOAP::Transport::HTTP::Client::get_basic_credentials + +This is included to set the username and password used by SOAP::Lite. + +=cut + +sub SOAP::Transport::HTTP::Client::get_basic_credentials { + # Library username and password from Base Bibliotek (stored as system preferences) + my $library_username = C4::Context->preference("NorwegianPatronDBUsername"); + my $library_password = C4::Context->preference("NorwegianPatronDBPassword"); + # Vendor username and password (stored in koha-conf.xml) + my $vendor_username = C4::Context->config( 'nlvendoruser' ); + my $vendor_password = C4::Context->config( 'nlvendorpass' ); + # Combine usernames and passwords, and encrypt with SHA256 + my $combined_username = "$vendor_username-$library_username"; + my $combined_password = sha256_hex( "$library_password-$vendor_password" ); + warn "$combined_username => $combined_password"; + return $combined_username => $combined_password; +} + +=head2 NLCheckSysprefs + +Check that sysprefs relevant to NL are set. + +=cut + +sub NLCheckSysprefs { + + my $response = { + 'error' => 0, + 'nlenabled' => 0, + 'endpoint' => 0, + 'userpass' => 0, + }; + + # Check that the Norwegian national paron database is enabled + if ( C4::Context->preference("NorwegianPatronDBEnable") == 1 ) { + $response->{ 'nlenabled' } = 1; + } else { + $response->{ 'error' } = 1; + } + + # Check that an endpoint is specified + if ( C4::Context->preference("NorwegianPatronDBEndpoint") ne '' ) { + $response->{ 'endpoint' } = 1; + } else { + $response->{ 'error' } = 1; + } + + # Check that the username and password for the patron database is set + if ( C4::Context->preference("NorwegianPatronDBUsername") ne '' && C4::Context->preference("NorwegianPatronDBPassword") ne '' ) { + $response->{ 'userpass' } = 1; + } else { + $response->{ 'error' } = 1; + } + + return $response; + +} + +=head2 NLSearch + +Search the NL patron database. + +SOAP call: "hent" (fetch) + +=cut + +sub NLSearch { + + my ( $identifier ) = @_; + + my $client = SOAP::Lite + ->on_action( sub { return '""';}) + ->uri( $nl_uri ) + ->proxy( $nl_proxy ); + + my $id = SOAP::Data->type('string'); + $id->name('identifikator'); + $id->value( $identifier ); + my $som = $client->hent( $id ); + + return $som; + +} + +=head2 NLSync + +Sync a patron that has been changed or created in Koha "upstream" to NL. + +Input is a hashref with one of two possible elements, either a patron retrieved +from the database: + + my $result = NLSync({ 'patron' => $borrower_from_dbic }); + +or a plain old borrowernumber: + + my $result = NLSync({ 'borrowernumber' => $borrowernumber }); + +In the latter case, this function will retrieve the patron record from the +database using DBIC. + +Which part of the API is called depends on the value of the "syncstatus" column: + +=over 4 + +=item * B = The I ("new record") method is called. + +=item * B = The I ("change/update") method is called. + +=item * B = The I ("delete") method is called. + +=back + +Required values for B and B: + +=over 4 + +=item * sist_endret (last updated) + +=item * adresse, postnr eller sted (address, zip or city) + +=item * fdato (birthdate) + +=item * fnr_hash (social security number, but not hashed...) + +=item * kjonn (gender, M/F) + +=back + +=cut + +sub NLSync { + + my ( $input ) = @_; + + my $patron; + if ( defined $input->{'borrowernumber'} ) { + $patron = Koha::Database->new->schema->resultset('Borrower')->find( $input->{'borrowernumber'} ); + } elsif ( defined $input->{'patron'} ) { + $patron = $input->{'patron'}; + } + + # There should only be one sync, so we use the first one + my @syncs = $patron->borrower_syncs; + my $sync; + foreach my $this_sync ( @syncs ) { + if ( $this_sync->synctype eq 'norwegianpatrondb' ) { + $sync = $this_sync; + } + } + + my $client = SOAP::Lite + ->on_action( sub { return '""';}) + ->uri( $nl_uri ) + ->proxy( $nl_proxy ); + + my $cardnumber = SOAP::Data->name( 'lnr' => $patron->cardnumber ); + + # Call the appropriate method based on syncstatus + my $response; + if ( $sync->syncstatus eq 'edited' || $sync->syncstatus eq 'new' ) { + my $soap_patron = _koha_patron_to_soap( $patron ); + if ( $sync->syncstatus eq 'edited' ) { + $response = $client->endre( $cardnumber, $soap_patron ); + } elsif ( $sync->syncstatus eq 'new' ) { + $response = $client->nyPost( $soap_patron ); + } + } + if ( $sync->syncstatus eq 'delete' ) { + $response = $client->slett( $cardnumber ); + } + + # Update the sync data according to the results + if ( $response->{'status'} && $response->{'status'} == 1 ) { + if ( $sync->syncstatus eq 'delete' ) { + # Turn off any further syncing + $sync->update( { 'sync' => 0 } ); + } + # Update the syncstatus to 'synced' + $sync->update( { 'syncstatus' => 'synced' } ); + # Update the 'synclast' attribute with the "server time" ("server_tid") returned by the method + $sync->update( { 'lastsync' => $response->{'server_tid'} } ); + } + return $response; + +} + +=head2 NLGetChanged + +Fetches patrons from NL that have been changed since a given timestamp. This includes +patrons that have been changed by the library that runs the sync, so we have to +check which library was the last one to change a patron, before we update patrons +locally. + +This is supposed to be executed once per night. + +SOAP call: soekEndret + +=cut + +sub NLGetChanged { + + my ( $from_arg ) = @_; + + my $client = SOAP::Lite + ->on_action( sub { return '""';}) + ->uri( $nl_uri ) + ->proxy( $nl_proxy ); + + my $from_string; + if ( $from_arg && $from_arg ne '' ) { + $from_string = $from_arg; + } else { + # Calculate 1 second past midnight of the day before + my $dt = DateTime->now( time_zone => 'Europe/Oslo' ); + $dt->subtract( days => 1 ); + my $from = DateTime->new( + year => $dt->year(), + month => $dt->month(), + day => $dt->day(), + hour => 0, + minute => 0, + second => 1, + time_zone => 'Europe/Oslo', + ); + $from_string = $from->ymd . "T" . $from->hms; + } + + my $timestamp = SOAP::Data->name( 'tidspunkt' => $from_string ); + my $max_results = SOAP::Data->name( 'max_antall' => 0 ); # 0 = no limit + my $start_index = SOAP::Data->name( 'start_indeks' => 0 ); # 1 is the first record + + # Call the appropriate method based on syncstatus + my $som = $client->soekEndret( $timestamp, $max_results, $start_index ); + + # Extract and massage patron data + my $result = $som->result; + foreach my $patron ( @{ $result->{'respons_poster'} } ) { + # Only handle patrons that have lnr (barcode) and fnr_hash (social security number) + # Patrons that lack these two have been deleted from NL + if ( $patron->{'lnr'} && $patron->{'fnr_hash'} ) { + push @{ $result->{'kohapatrons'} }, _soap_to_kohapatron( $patron ); + } + } + return $result; + +} + +=head2 NLMarkForDeletion + +Mark a borrower for deletion, but do not do the actual deletion. Deleting the +borrower from NL will be done later by the nl-sync-from-koha.pl script. + +=cut + +sub NLMarkForDeletion { + + my ( $borrowernumber ) = @_; + + my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + return $borrowersync->update( { 'syncstatus' => 'delete' } ); + +} + +=head2 NLDecodePin + +Takes a string encoded with AES/ECB/PKCS5PADDING and a 128-bits key, and returns +the decoded string as plain text. + +The key needs to be stored in koha-conf.xml, like so: + + + ... + + ... + xyz + + + +=cut + +sub NLDecodePin { + + my ( $hash ) = @_; + my $key = C4::Context->config( 'nlkey' ); + + # Convert the hash from Base16 + my $cb = Convert::BaseN->new( base => 16 ); + my $decoded_hash = $cb->decode( $hash ); + + # Do the decryption + my $cipher = Crypt::GCrypt->new( + type => 'cipher', + algorithm => 'aes', + mode => 'ecb', + padding => 'standard', # "This is also known as PKCS#5" + ); + $cipher->start( 'decrypting' ); + $cipher->setkey( $key ); # Must be called after start() + my $plaintext = $cipher->decrypt( $decoded_hash ); + $plaintext .= $cipher->finish; + + return $plaintext; + +} + +=head2 NLEncryptPIN + +Takes a plain text PIN as argument, returns the encrypted PIN, according to the +NL specs. + + my $encrypted_pin = NLEncryptPIN( $plain_text_pin ); + +=cut + +sub NLEncryptPIN { + + my ( $pin ) = @_; + return _encrypt_pin( $pin ); + +} + +=head2 NLUpdateHashedPIN + +Takes two arguments: + +=over 4 + +=item * Borrowernumber + +=item * Clear text PIN code + +=back + +Hashes the password and saves it in borrower_sync.hashed_pin. + +=cut + +sub NLUpdateHashedPIN { + + my ( $borrowernumber, $pin ) = @_; + my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + return $borrowersync->update({ 'hashed_pin', _encrypt_pin( $pin ) }); + +} + +=head2 _encrypt_pin + +Takes a plain text PIN and returns the encrypted version, according to the NL specs. + +=cut + +sub _encrypt_pin { + + my ( $pin ) = @_; + my $key = C4::Context->config( 'nlkey' ); + + # Do the encryption + my $cipher = Crypt::GCrypt->new( + type => 'cipher', + algorithm => 'aes', + mode => 'ecb', + padding => 'standard', # "This is also known as PKCS#5" + ); + $cipher->start( 'encrypting' ); + $cipher->setkey( $key ); # Must be called after start() + my $ciphertext = $cipher->encrypt( $pin ); + $ciphertext .= $cipher->finish; + + # Encode as Bas16 + my $cb = Convert::BaseN->new( base => 16 ); + my $encoded_ciphertext = $cb->encode( $ciphertext ); + + return $encoded_ciphertext; + +} + +=head2 NLGetSyncDataFromBorrowernumber + +Takes a borrowernumber as argument, returns a Koha::Schema::Result::BorrowerSync +object. + + my $syncdata = NLGetSyncDataFromBorrowernumber( $borrowernumber ); + +=cut + +sub NLGetSyncDataFromBorrowernumber { + + my ( $borrowernumber ) = @_; + my $data = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + return $data; + +} + +=head2 NLGetFirstname + +Takes a string like "Surname, Firstname" and returns the "Firstname" part. + +If there is no comma, the string is returned unaltered. + + my $firstname = NLGetFirstname( $name ); + +=cut + +sub NLGetFirstname { + + my ( $s ) = @_; + my ( $surname, $firstname ) = _split_name( $s ); + if ( $surname eq $s ) { + return $s; + } else { + return $firstname; + } + +} + +=head2 NLGetSurname + +Takes a string like "Surname, Firstname" and returns the "Surname" part. + +If there is no comma, the string is returned unaltered. + + my $surname = NLGetSurname( $name ); + +=cut + +sub NLGetSurname { + + my ( $s ) = @_; + my ( $surname, $firstname ) = _split_name( $s ); + return $surname; + +} + +=head2 _split_name + +Takes a string like "Surname, Firstname" and returns a list of surname and firstname. + +If there is no comma, the string is returned unaltered. + + my ( $surname, $firstname ) = _split_name( $name ); + +=cut + +sub _split_name { + + my ( $s ) = @_; + + # Return the string if there is no comma + unless ( $s =~ m/,/ ) { + return $s; + } + + my ( $surname, $firstname ) = split /, /, $s; + + return ( $surname, $firstname ); + +} + +=head2 _format_soap_error + +Takes a soap result object as input and returns a formatted string containing SOAP error data. + +=cut + +sub _format_soap_error { + + my ( $result ) = @_; + if ( $result ) { + return join ', ', $result->faultcode, $result->faultstring, $result->faultdetail; + } else { + return 'No result'; + } + +} + +=head2 _soap_to_koha_patron + +Convert a SOAP object of type "Laaner" into a hash that can be sent to AddMember or ModMember. + +=cut + +sub _soap_to_kohapatron { + + my ( $soap ) = @_; + + return { + 'cardnumber' => $soap->{ 'lnr' }, + 'surname' => NLGetSurname( $soap->{ 'navn' } ), + 'firstname' => NLGetFirstname( $soap->{ 'navn' } ), + 'sex' => $soap->{ 'kjonn' }, + 'dateofbirth' => $soap->{ 'fdato' }, + 'address' => $soap->{ 'p_adresse1' }, + 'address2' => $soap->{ 'p_adresse2' }, + 'zipcode' => $soap->{ 'p_postnr' }, + 'city' => $soap->{ 'p_sted' }, + 'country' => $soap->{ 'p_land' }, + 'b_address' => $soap->{ 'm_adresse1' }, + 'b_address2' => $soap->{ 'm_adresse2' }, + 'b_zipcode' => $soap->{ 'm_postnr' }, + 'b_city' => $soap->{ 'm_sted' }, + 'b_country' => $soap->{ 'm_land' }, + 'password' => $soap->{ 'pin' }, + 'dateexpiry' => $soap->{ 'gyldig_til' }, + 'email' => $soap->{ 'epost' }, + 'mobile' => $soap->{ 'tlf_mobil' }, + 'phone' => $soap->{ 'tlf_hjemme' }, + 'phonepro' => $soap->{ 'tlf_jobb' }, + '_extra' => { # Data that should not go in the borrowers table + 'socsec' => $soap->{ 'fnr_hash' }, + 'created' => $soap->{ 'opprettet' }, + 'created_by' => $soap->{ 'opprettet_av' }, + 'last_change' => $soap->{ 'sist_endret' }, + 'last_change_by' => $soap->{ 'sist_endret_av' }, + }, + }; + +} + +=head2 _koha_patron_to_soap + +Convert a patron (in the form of a Koha::Schema::Result::Borrower) into a SOAP +object that can be sent to NL. + +=cut + +sub _koha_patron_to_soap { + + my ( $patron ) = @_; + + # Extract attributes + my $patron_attributes = {}; + foreach my $attribute ( $patron->borrower_attributes ) { + $patron_attributes->{ $attribute->code->code } = $attribute->attribute; + } + + # There should only be one sync, so we use the first one + my @syncs = $patron->borrower_syncs; + my $sync = $syncs[0]; + + # Create SOAP::Data object + my $soap_patron = SOAP::Data->name( + 'post' => \SOAP::Data->value( + SOAP::Data->name( 'lnr' => $patron->cardnumber ), + SOAP::Data->name( 'fnr_hash' => $patron_attributes->{ 'fnr' } )->type( 'string' )->type( 'string' ), + SOAP::Data->name( 'navn' => $patron->surname . ', ' . $patron->firstname )->type( 'string' ), + SOAP::Data->name( 'sist_endret' => $sync->lastsync )->type( 'string' ), + SOAP::Data->name( 'kjonn' => $patron->sex )->type( 'string' ), + SOAP::Data->name( 'fdato' => $patron->dateofbirth )->type( 'string' ), + SOAP::Data->name( 'p_adresse1' => $patron->address )->type( 'string' ), + SOAP::Data->name( 'p_adresse2' => $patron->address2 )->type( 'string' ), + SOAP::Data->name( 'p_postnr' => $patron->zipcode )->type( 'string' ), + SOAP::Data->name( 'p_sted' => $patron->city )->type( 'string' ), + SOAP::Data->name( 'p_land' => $patron->country )->type( 'string' ), + SOAP::Data->name( 'm_adresse1' => $patron->b_address )->type( 'string' ), + SOAP::Data->name( 'm_adresse2' => $patron->b_address2 )->type( 'string' ), + SOAP::Data->name( 'm_postnr' => $patron->b_zipcode )->type( 'string' ), + SOAP::Data->name( 'm_sted' => $patron->b_city )->type( 'string' ), + SOAP::Data->name( 'm_land' => $patron->b_country )->type( 'string' ), + # Do not send the PIN code as it has been hashed by Koha, but use the version hashed according to NL + SOAP::Data->name( 'pin' => $sync->hashed_pin )->type( 'string' ), + SOAP::Data->name( 'gyldig_til' => $patron->dateexpiry )->type( 'string' ), + SOAP::Data->name( 'epost' => $patron->email )->type( 'string' ), + SOAP::Data->name( 'tlf_mobil' => $patron->mobile )->type( 'string' ), + SOAP::Data->name( 'tlf_hjemme' => $patron->phone )->type( 'string' ), + SOAP::Data->name( 'tlf_jobb' => $patron->phonepro )->type( 'string' ), + ), + )->type("Laaner"); + + return $soap_patron; + +} + +=head1 EXPORT + +None by default. + +=head1 AUTHOR + +Magnus Enger + +=cut + +1; + +__END__ diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index aeeba17..be3e72c 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -357,6 +357,24 @@ CREATE TABLE `branch_item_rules` ( -- information entered in the circulation and ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- +-- Table structure for table borrower_sync +-- + +CREATE TABLE borrower_sync ( + borrowersyncid int(11) NOT NULL AUTO_INCREMENT, -- Primary key, unique identifier + borrowernumber int(11) NOT NULL, -- Connects data about synchronisations to a borrower + synctype varchar(32) NOT NULL, -- There could potentially be more than one kind of syncing going on, a text string here can be used to tell them apart. E.g.: The Norwegian national patron database uses 'norwegianpatrondb' in this column + sync tinyint(1) NOT NULL DEFAULT '0', -- A boolean (1/0) for turning syncing off and on for individual borrowers + syncstatus varchar(10) DEFAULT NULL, -- The sync status for any given borrower. Could be text strings like 'new', 'edited', 'synced' etc. The values used here will depend on the actual syncing being done. + lastsync varchar(50) DEFAULT NULL, -- Date of the last successfull sync. The date format might be different depending on the service that is being used, so no special date format is being enforced here. + hashed_pin varchar(64) DEFAULT NULL, -- Perhaps specific to The Norwegian national patron database, this column holds a hashed PIN code + PRIMARY KEY (borrowersyncid), + KEY borrowernumber (borrowernumber), + CONSTRAINT borrower_sync_ibfk_1 FOREIGN KEY (borrowernumber) REFERENCES borrowers (borrowernumber) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +-- -- Table structure for table `branchcategories` -- diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index b845d53..436da06 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -197,6 +197,11 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('noissuescharge','5','','Define maximum amount withstanding before check outs are blocked','Integer'), ('noItemTypeImages','0',NULL,'If ON, disables item-type images','YesNo'), ('NoLoginInstructions', '', '60|10', 'Instructions to display on the OPAC login form when a patron is not logged in', 'Textarea'), +('NorwegianPatronDBEnable','0',NULL,'Enable communication with the Norwegian national patron database.', 'YesNo'), +('NorwegianPatronDBEndpoint','',NULL,'Which NL endpoint to use.', 'Free'), +('NorwegianPatronDBUsername','',NULL,'Username for communication with the Norwegian national patron database.','Free'), +('NorwegianPatronDBPassword','',NULL,'Password for communication with the Norwegian national patron database.','Free'), +('NorwegianPatronDBSearchNLAfterLocalHit','0',NULL,'Search NL if a search has already given one or more local hits?.','YesNo'), ('NotesBlacklist','',NULL,'List of notes fields that should not appear in the title notes/description separator of details','free'), ('NotHighlightedWords','and|or|not',NULL,'List of words to NOT highlight when OpacHitHighlight is enabled','free'), ('NoticeCSS','',NULL,'Notices CSS url.','free'), diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index eb12838..dbc362f 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -8800,6 +8800,31 @@ if ( CheckVersion($DBversion) ) { SetVersion($DBversion); } +$DBversion = "3.17.00.XXX"; +if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBEnable', '0', NULL, 'Enable communication with the Norwegian national patron database.', 'YesNo')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBEndpoint', '', NULL, 'Which NL endpoint to use.', 'Free')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBUsername', '', NULL, 'Username for communication with the Norwegian national patron database.', 'Free')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBPassword', '', NULL, 'Password for communication with the Norwegian national patron database.', 'Free')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBSearchNLAfterLocalHit','0',NULL,'Search NL if a search has already given one or more local hits?.','YesNo')"); + $dbh->do(" +CREATE TABLE borrower_sync ( + borrowersyncid int(11) NOT NULL AUTO_INCREMENT, + borrowernumber int(11) NOT NULL, + synctype varchar(32) NOT NULL, + sync tinyint(1) NOT NULL DEFAULT '0', + syncstatus varchar(10) DEFAULT NULL, + lastsync varchar(50) DEFAULT NULL, + hashed_pin varchar(64) DEFAULT NULL, + PRIMARY KEY (borrowersyncid), + KEY borrowernumber (borrowernumber), + CONSTRAINT borrower_sync_ibfk_1 FOREIGN KEY (borrowernumber) REFERENCES borrowers (borrowernumber) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8" +); + print "Upgrade to $DBversion done (Bug 11401 - Add support for Norwegian national library card)\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc index caa2fbf..870f254 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -1,12 +1,32 @@ +[% USE Koha %] +[% SET NorwegianPatronDBEnable = Koha.Preference( 'NorwegianPatronDBEnable' ) %]