@@ -, +, @@ - 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 - 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 - Rebase on master - Split out changes to Koha::Schema - Incorporate new way of authenticating with NL - 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 | 9 + .../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, 2287 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 --- a/C4/Installer/PerlDependencies.pm +++ a/C4/Installer/PerlDependencies.pm @@ -712,6 +712,26 @@ our $PERL_DEPS = { 'required' => '0', 'min_ver' => '0.03', }, + '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; --- a/C4/Members.pm +++ a/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); @@ -784,6 +789,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}); } } @@ -804,6 +813,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; @@ -846,11 +874,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"); --- a/Koha/NorwegianPatronDB.pm +++ a/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__ --- a/installer/data/mysql/kohastructure.sql +++ a/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` -- --- a/installer/data/mysql/sysprefs.sql +++ a/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'), --- a/installer/data/mysql/updatedatabase.pl +++ a/installer/data/mysql/updatedatabase.pl @@ -8835,6 +8835,31 @@ if ( CheckVersion($DBversion) ) { ('UsageStats', 0, NULL, 'Share anonymous usage data on the Hea Koha community website.', 'YesNo') }); print "Upgrade to $DBversion done (Bug 11926: Add UsageStats systempreferences (HEA))\n"; + 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); } --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -1,12 +1,32 @@ +[% USE Koha %] +[% SET NorwegianPatronDBEnable = Koha.Preference( 'NorwegianPatronDBEnable' ) %]