1 package Koha::NorwegianPatronDB;
3 # Copyright 2014 Oslo Public Library
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 Koha::NorwegianPatronDB
26 use Koha::NorwegianPatronDB;
28 =head1 CONDITIONAL LOADING
30 This module depends on some Perl modules that have not been marked as required.
31 This is because the module only will be of interest to Norwegian libraries, and
32 it seems polite not to bother the rest of the world with these modules. It is
33 also good practice to check that the module is actually needed before loading
34 it. So in a NorwegianPatronDB page or script it will be OK to just do:
36 use Koha::NorwegianPatronDB qw(...);
38 But in scripts that are also used by others (like e.g. moremember.pl), it will
39 be polite to only load the module at runtime, if it is needed:
42 if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) {
43 load Koha::NorwegianPatronDB, qw( NLGetSyncDataFromBorrowernumber );
46 (Module::Load::Conditional is used for this in other parts of Koha, but it does
47 not seem to allow for a list of subroutines to import, so Module::Load looks
48 like a better candidate.)
56 use C4::Members::Attributes qw( UpdateBorrowerAttribute );
59 use Digest::SHA qw( sha256_hex );
65 our %EXPORT_TAGS = ( all => [qw(
76 NLGetSyncDataFromBorrowernumber
78 Exporter::export_ok_tags('all');
80 my $nl_uri = 'http://lanekortet.no';
82 =head2 SOAP::Transport::HTTP::Client::get_basic_credentials
84 This is included to set the username and password used by SOAP::Lite.
88 sub SOAP::Transport::HTTP::Client::get_basic_credentials {
89 # Library username and password from Base Bibliotek (stored as system preferences)
90 my $library_username = C4::Context->preference("NorwegianPatronDBUsername");
91 my $library_password = C4::Context->preference("NorwegianPatronDBPassword");
92 # Vendor username and password (stored in koha-conf.xml)
93 my $vendor_username = C4::Context->config( 'nlvendoruser' );
94 my $vendor_password = C4::Context->config( 'nlvendorpass' );
95 # Combine usernames and passwords, and encrypt with SHA256
96 my $combined_username = "$vendor_username-$library_username";
97 my $combined_password = sha256_hex( "$library_password-$vendor_password" );
98 return $combined_username => $combined_password;
101 =head2 NLCheckSysprefs
103 Check that sysprefs relevant to NL are set.
107 sub NLCheckSysprefs {
116 # Check that the Norwegian national paron database is enabled
117 if ( C4::Context->preference("NorwegianPatronDBEnable") == 1 ) {
118 $response->{ 'nlenabled' } = 1;
120 $response->{ 'error' } = 1;
123 # Check that an endpoint is specified
124 if ( C4::Context->preference("NorwegianPatronDBEndpoint") ne '' ) {
125 $response->{ 'endpoint' } = 1;
127 $response->{ 'error' } = 1;
130 # Check that the username and password for the patron database is set
131 if ( C4::Context->preference("NorwegianPatronDBUsername") ne '' && C4::Context->preference("NorwegianPatronDBPassword") ne '' ) {
132 $response->{ 'userpass' } = 1;
134 $response->{ 'error' } = 1;
143 Search the NL patron database.
145 SOAP call: "hent" (fetch)
151 my ( $identifier ) = @_;
153 my $client = SOAP::Lite
154 ->on_action( sub { return '""';})
156 ->proxy( C4::Context->preference("NorwegianPatronDBEndpoint") );
158 my $id = SOAP::Data->type('string');
159 $id->name('identifikator');
160 $id->value( $identifier );
161 my $som = $client->hent( $id );
169 Sync a patron that has been changed or created in Koha "upstream" to NL.
171 Input is a hashref with one of two possible elements, either a patron retrieved
174 my $result = NLSync({ 'patron' => $borrower_from_dbic });
176 or a plain old borrowernumber:
178 my $result = NLSync({ 'borrowernumber' => $borrowernumber });
180 In the latter case, this function will retrieve the patron record from the
183 Which part of the API is called depends on the value of the "syncstatus" column:
187 =item * B<new> = The I<nyPost> ("new record") method is called.
189 =item * B<edited> = The I<endre> ("change/update") method is called.
191 =item * B<delete> = The I<slett> ("delete") method is called.
195 Required values for B<new> and B<edited>:
199 =item * sist_endret (last updated)
201 =item * adresse, postnr eller sted (address, zip or city)
203 =item * fdato (birthdate)
205 =item * fnr_hash (social security number, but not hashed...)
207 =item * kjonn (gender, M/F)
218 if ( defined $input->{'borrowernumber'} ) {
219 $patron = Koha::Database->new->schema->resultset('Borrower')->find( $input->{'borrowernumber'} );
220 } elsif ( defined $input->{'patron'} ) {
221 $patron = $input->{'patron'};
224 # There should only be one sync, so we use the first one
225 my @syncs = $patron->borrower_syncs;
227 foreach my $this_sync ( @syncs ) {
228 if ( $this_sync->synctype eq 'norwegianpatrondb' ) {
233 my $client = SOAP::Lite
234 ->on_action( sub { return '""';})
236 ->proxy( C4::Context->preference("NorwegianPatronDBEndpoint") );
238 my $cardnumber = SOAP::Data->name( 'lnr' => $patron->cardnumber );
240 # Call the appropriate method based on syncstatus
242 if ( $sync->syncstatus eq 'edited' || $sync->syncstatus eq 'new' ) {
243 my $soap_patron = _koha_patron_to_soap( $patron );
244 if ( $sync->syncstatus eq 'edited' ) {
245 $response = $client->endre( $cardnumber, $soap_patron );
246 } elsif ( $sync->syncstatus eq 'new' ) {
247 $response = $client->nyPost( $soap_patron );
250 if ( $sync->syncstatus eq 'delete' ) {
251 $response = $client->slett( $cardnumber );
254 # Update the sync data according to the results
255 if ( $response->{'status'} && $response->{'status'} == 1 ) {
256 if ( $sync->syncstatus eq 'delete' ) {
257 # Turn off any further syncing
258 $sync->update( { 'sync' => 0 } );
260 # Update the syncstatus to 'synced'
261 $sync->update( { 'syncstatus' => 'synced' } );
262 # Update the 'synclast' attribute with the "server time" ("server_tid") returned by the method
263 $sync->update( { 'lastsync' => $response->{'server_tid'} } );
271 Fetches patrons from NL that have been changed since a given timestamp. This includes
272 patrons that have been changed by the library that runs the sync, so we have to
273 check which library was the last one to change a patron, before we update patrons
276 This is supposed to be executed once per night.
278 SOAP call: soekEndret
284 my ( $from_arg ) = @_;
286 my $client = SOAP::Lite
287 ->on_action( sub { return '""';})
289 ->proxy( C4::Context->preference("NorwegianPatronDBEndpoint") );
292 if ( $from_arg && $from_arg ne '' ) {
293 $from_string = $from_arg;
295 # Calculate 1 second past midnight of the day before
296 my $dt = DateTime->now( time_zone => 'Europe/Oslo' );
297 $dt->subtract( days => 1 );
298 my $from = DateTime->new(
300 month => $dt->month(),
305 time_zone => 'Europe/Oslo',
307 $from_string = $from->ymd . "T" . $from->hms;
310 my $timestamp = SOAP::Data->name( 'tidspunkt' => $from_string );
311 my $max_results = SOAP::Data->name( 'max_antall' => 0 ); # 0 = no limit
312 my $start_index = SOAP::Data->name( 'start_indeks' => 0 ); # 1 is the first record
314 # Call the appropriate method based on syncstatus
315 my $som = $client->soekEndret( $timestamp, $max_results, $start_index );
317 # Extract and massage patron data
318 my $result = $som->result;
319 foreach my $patron ( @{ $result->{'respons_poster'} } ) {
320 # Only handle patrons that have lnr (barcode) and fnr_hash (social security number)
321 # Patrons that lack these two have been deleted from NL
322 if ( $patron->{'lnr'} && $patron->{'fnr_hash'} ) {
323 push @{ $result->{'kohapatrons'} }, _soap_to_kohapatron( $patron );
330 =head2 NLMarkForDeletion
332 Mark a borrower for deletion, but do not do the actual deletion. Deleting the
333 borrower from NL will be done later by the nl-sync-from-koha.pl script.
337 sub NLMarkForDeletion {
339 my ( $borrowernumber ) = @_;
341 my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
342 'synctype' => 'norwegianpatrondb',
343 'borrowernumber' => $borrowernumber,
345 return $borrowersync->update( { 'syncstatus' => 'delete' } );
351 Takes a string encoded with AES/ECB/PKCS5PADDING and a 128-bits key, and returns
352 the decoded string as plain text.
354 The key needs to be stored in koha-conf.xml, like so:
369 my $key = C4::Context->config( 'nlkey' );
371 # Convert the hash from Base16
372 my $cb = Convert::BaseN->new( base => 16 );
373 my $decoded_hash = $cb->decode( $hash );
376 my $cipher = Crypt::GCrypt->new(
380 padding => 'standard', # "This is also known as PKCS#5"
382 $cipher->start( 'decrypting' );
383 $cipher->setkey( $key ); # Must be called after start()
384 my $plaintext = $cipher->decrypt( $decoded_hash );
385 $plaintext .= $cipher->finish;
393 Takes a plain text PIN as argument, returns the encrypted PIN, according to the
396 my $encrypted_pin = NLEncryptPIN( $plain_text_pin );
403 return _encrypt_pin( $pin );
407 =head2 NLUpdateHashedPIN
413 =item * Borrowernumber
415 =item * Clear text PIN code
419 Hashes the password and saves it in borrower_sync.hashed_pin.
423 sub NLUpdateHashedPIN {
425 my ( $borrowernumber, $pin ) = @_;
426 my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({
427 'synctype' => 'norwegianpatrondb',
428 'borrowernumber' => $borrowernumber,
430 return $borrowersync->update({ 'hashed_pin', _encrypt_pin( $pin ) });
436 Takes a plain text PIN and returns the encrypted version, according to the NL specs.
443 my $key = C4::Context->config( 'nlkey' );
446 my $cipher = Crypt::GCrypt->new(
450 padding => 'standard', # "This is also known as PKCS#5"
452 $cipher->start( 'encrypting' );
453 $cipher->setkey( $key ); # Must be called after start()
454 my $ciphertext = $cipher->encrypt( $pin );
455 $ciphertext .= $cipher->finish;
458 my $cb = Convert::BaseN->new( base => 16 );
459 my $encoded_ciphertext = $cb->encode( $ciphertext );
461 return $encoded_ciphertext;
465 =head2 NLGetSyncDataFromBorrowernumber
467 Takes a borrowernumber as argument, returns a Koha::Schema::Result::BorrowerSync
470 my $syncdata = NLGetSyncDataFromBorrowernumber( $borrowernumber );
474 sub NLGetSyncDataFromBorrowernumber {
476 my ( $borrowernumber ) = @_;
477 my $data = Koha::Database->new->schema->resultset('BorrowerSync')->find({
478 'synctype' => 'norwegianpatrondb',
479 'borrowernumber' => $borrowernumber,
485 =head2 NLGetFirstname
487 Takes a string like "Surname, Firstname" and returns the "Firstname" part.
489 If there is no comma, the string is returned unaltered.
491 my $firstname = NLGetFirstname( $name );
498 my ( $surname, $firstname ) = _split_name( $s );
499 if ( $surname eq $s ) {
509 Takes a string like "Surname, Firstname" and returns the "Surname" part.
511 If there is no comma, the string is returned unaltered.
513 my $surname = NLGetSurname( $name );
520 my ( $surname, $firstname ) = _split_name( $s );
527 Takes a string like "Surname, Firstname" and returns a list of surname and firstname.
529 If there is no comma, the string is returned unaltered.
531 my ( $surname, $firstname ) = _split_name( $name );
539 # Return the string if there is no comma
540 unless ( $s =~ m/,/ ) {
544 my ( $surname, $firstname ) = split /, /, $s;
546 return ( $surname, $firstname );
550 =head2 _format_soap_error
552 Takes a soap result object as input and returns a formatted string containing SOAP error data.
556 sub _format_soap_error {
560 return join ', ', $result->faultcode, $result->faultstring, $result->faultdetail;
567 =head2 _soap_to_koha_patron
569 Convert a SOAP object of type "Laaner" into a hash that can be sent to AddMember or ModMember.
573 sub _soap_to_kohapatron {
578 'cardnumber' => $soap->{ 'lnr' },
579 'surname' => NLGetSurname( $soap->{ 'navn' } ),
580 'firstname' => NLGetFirstname( $soap->{ 'navn' } ),
581 'sex' => $soap->{ 'kjonn' },
582 'dateofbirth' => $soap->{ 'fdato' },
583 'address' => $soap->{ 'p_adresse1' },
584 'address2' => $soap->{ 'p_adresse2' },
585 'zipcode' => $soap->{ 'p_postnr' },
586 'city' => $soap->{ 'p_sted' },
587 'country' => $soap->{ 'p_land' },
588 'b_address' => $soap->{ 'm_adresse1' },
589 'b_address2' => $soap->{ 'm_adresse2' },
590 'b_zipcode' => $soap->{ 'm_postnr' },
591 'b_city' => $soap->{ 'm_sted' },
592 'b_country' => $soap->{ 'm_land' },
593 'password' => $soap->{ 'pin' },
594 'dateexpiry' => $soap->{ 'gyldig_til' },
595 'email' => $soap->{ 'epost' },
596 'mobile' => $soap->{ 'tlf_mobil' },
597 'phone' => $soap->{ 'tlf_hjemme' },
598 'phonepro' => $soap->{ 'tlf_jobb' },
599 '_extra' => { # Data that should not go in the borrowers table
600 'socsec' => $soap->{ 'fnr_hash' },
601 'created' => $soap->{ 'opprettet' },
602 'created_by' => $soap->{ 'opprettet_av' },
603 'last_change' => $soap->{ 'sist_endret' },
604 'last_change_by' => $soap->{ 'sist_endret_av' },
610 =head2 _koha_patron_to_soap
612 Convert a patron (in the form of a Koha::Schema::Result::Borrower) into a SOAP
613 object that can be sent to NL.
617 sub _koha_patron_to_soap {
622 my $patron_attributes = {};
623 foreach my $attribute ( $patron->borrower_attributes ) {
624 $patron_attributes->{ $attribute->code->code } = $attribute->attribute;
627 # There should only be one sync, so we use the first one
628 my @syncs = $patron->borrower_syncs;
629 my $sync = $syncs[0];
631 # Create SOAP::Data object
632 my $soap_patron = SOAP::Data->name(
633 'post' => \SOAP::Data->value(
634 SOAP::Data->name( 'lnr' => $patron->cardnumber ),
635 SOAP::Data->name( 'fnr_hash' => $patron_attributes->{ 'fnr' } )->type( 'string' )->type( 'string' ),
636 SOAP::Data->name( 'navn' => $patron->surname . ', ' . $patron->firstname )->type( 'string' ),
637 SOAP::Data->name( 'sist_endret' => $sync->lastsync )->type( 'string' ),
638 SOAP::Data->name( 'kjonn' => $patron->sex )->type( 'string' ),
639 SOAP::Data->name( 'fdato' => $patron->dateofbirth )->type( 'string' ),
640 SOAP::Data->name( 'p_adresse1' => $patron->address )->type( 'string' ),
641 SOAP::Data->name( 'p_adresse2' => $patron->address2 )->type( 'string' ),
642 SOAP::Data->name( 'p_postnr' => $patron->zipcode )->type( 'string' ),
643 SOAP::Data->name( 'p_sted' => $patron->city )->type( 'string' ),
644 SOAP::Data->name( 'p_land' => $patron->country )->type( 'string' ),
645 SOAP::Data->name( 'm_adresse1' => $patron->b_address )->type( 'string' ),
646 SOAP::Data->name( 'm_adresse2' => $patron->b_address2 )->type( 'string' ),
647 SOAP::Data->name( 'm_postnr' => $patron->b_zipcode )->type( 'string' ),
648 SOAP::Data->name( 'm_sted' => $patron->b_city )->type( 'string' ),
649 SOAP::Data->name( 'm_land' => $patron->b_country )->type( 'string' ),
650 # Do not send the PIN code as it has been hashed by Koha, but use the version hashed according to NL
651 SOAP::Data->name( 'pin' => $sync->hashed_pin )->type( 'string' ),
652 SOAP::Data->name( 'gyldig_til' => $patron->dateexpiry )->type( 'string' ),
653 SOAP::Data->name( 'epost' => $patron->email )->type( 'string' ),
654 SOAP::Data->name( 'tlf_mobil' => $patron->mobile )->type( 'string' ),
655 SOAP::Data->name( 'tlf_hjemme' => $patron->phone )->type( 'string' ),
656 SOAP::Data->name( 'tlf_jobb' => $patron->phonepro )->type( 'string' ),
670 Magnus Enger <digitalutvikling@gmail.com>