From 7645c99c0139b4249f839e806580a4e8595bd8c1 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 4 Mar 2026 09:26:21 +0100 Subject: [PATCH] Koha 25.11: ILSDI comment-Parameter fuer HoldTitle/HoldItem - comment als optionaler Parameter in ilsdi.pl - notes => comment in Services.pm (HoldTitle + HoldItem) - Fallback fuer alte VuFind 5.x Parameternamen - Diff-Patches und komplette Dateien enthalten --- README.md | 44 + files/lib/C4/ILSDI/Services.pm | 1023 +++++++++++++++++++++ files/lib/C4/ILSDI/Services.pm.orig | 1015 ++++++++++++++++++++ files/opac/cgi-bin/opac/ilsdi.pl | 277 ++++++ files/opac/cgi-bin/opac/ilsdi.pl.orig | 275 ++++++ patches/koha-25.11-ilsdi-comment.patch | 15 + patches/koha-25.11-services-comment.patch | 44 + 7 files changed, 2693 insertions(+) create mode 100644 README.md create mode 100644 files/lib/C4/ILSDI/Services.pm create mode 100644 files/lib/C4/ILSDI/Services.pm.orig create mode 100755 files/opac/cgi-bin/opac/ilsdi.pl create mode 100755 files/opac/cgi-bin/opac/ilsdi.pl.orig create mode 100644 patches/koha-25.11-ilsdi-comment.patch create mode 100644 patches/koha-25.11-services-comment.patch diff --git a/README.md b/README.md new file mode 100644 index 0000000..c081eef --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Koha-Anpassungen BIBB + +Lokale Anpassungen an Koha fuer die BIBB-Bibliothek. + +## Koha 25.11 - ILSDI Comment-Parameter + +Erweiterung der ILSDI-Schnittstelle um einen comment-Parameter +fuer HoldTitle und HoldItem. Der Kommentar wird als reservenotes +in der Vormerkung gespeichert. + +### Betroffene Dateien + +| Datei | Pfad auf Server | +|-------|----------------| +| ilsdi.pl | /usr/share/koha/opac/cgi-bin/opac/ilsdi.pl | +| Services.pm | /usr/share/koha/lib/C4/ILSDI/Services.pm | + +### Aenderungen + +**ilsdi.pl:** +- comment als optionaler Parameter fuer HoldTitle und HoldItem +- needed_before_date und pickup_expiry_date als Kompatibilitaetsparameter + fuer VuFind 5.x + +**Services.pm (HoldTitle und HoldItem):** +- $comment = $cgi->param('comment') auslesen +- notes => $comment an AddReserve uebergeben +- Fallback fuer alte Parameternamen: + start_date || needed_before_date + expiry_date || pickup_expiry_date + +### Patches anwenden (nach Koha-Update) +cd / +patch -p0 < /home/rkeck/koha-anpassungen/patches/koha-25.11-ilsdi-comment.patch +patch -p0 < /home/rkeck/koha-anpassungen/patches/koha-25.11-services-comment.patch +sudo koha-plack --restart bibb + +### Pruefen ob Patches noch aktiv sind +grep -n "RKE" /usr/share/koha/opac/cgi-bin/opac/ilsdi.pl +grep -n "RKE" /usr/share/koha/lib/C4/ILSDI/Services.pm + +### Hinweis +Diese Anpassungen werden durch apt-get upgrade ueberschrieben. +Wird obsolet nach Migration von VuFind KohaILSDI auf KohaRest-Treiber. diff --git a/files/lib/C4/ILSDI/Services.pm b/files/lib/C4/ILSDI/Services.pm new file mode 100644 index 0000000..e7f9064 --- /dev/null +++ b/files/lib/C4/ILSDI/Services.pm @@ -0,0 +1,1023 @@ +package C4::ILSDI::Services; + +# Copyright 2009 SARL Biblibre +# +# 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 strict; +use warnings; + +use C4::Members; +use C4::Items qw( get_hostitemnumbers_of ); +use C4::Circulation qw( CanBookBeRenewed barcodedecode CanBookBeIssued AddRenewal ); +use C4::Accounts; +use C4::Reserves qw( CanBookBeReserved IsAvailableForItemLevelRequest CalculatePriority AddReserve CanItemBeReserved ); +use C4::Context; +use C4::Auth; +use CGI qw ( -utf8 ); +use DateTime; +use C4::Auth; +use Koha::DateUtils qw( dt_from_string ); +use C4::AuthoritiesMarc qw( GetAuthorityXML ); + +use Koha::Biblios; +use Koha::Checkouts; +use Koha::I18N qw(__); +use Koha::Items; +use Koha::Libraries; +use Koha::Patrons; + +=head1 NAME + +C4::ILS-DI::Services - ILS-DI Services + +=head1 DESCRIPTION + +Each function in this module represents an ILS-DI service. +They all takes a CGI instance as argument and most of them return a +hashref that will be printed by XML::Simple in opac/ilsdi.pl + +=head1 SYNOPSIS + + use C4::ILSDI::Services; + use XML::Simple; + use CGI qw ( -utf8 ); + + my $cgi = new CGI; + + $out = LookupPatron($cgi); + + print CGI::header('text/xml'); + print XMLout($out, + noattr => 1, + noescape => 1, + nosort => 1, + xmldecl => '', + RootName => 'LookupPatron', + SuppressEmpty => 1); + +=cut + +=head1 FUNCTIONS + +=head2 GetAvailability + +Given a set of biblionumbers or itemnumbers, returns a list with +availability of the items associated with the identifiers. + +Parameters: + +=head3 id (Required) + +list of either biblionumbers or itemnumbers + +=head3 id_type (Required) + +defines the type of record identifier being used in the request, +possible values: + + - bib + - item + +=head3 return_type (Optional) + +requests a particular level of detail in reporting availability, +possible values: + + - bib + - item + +=head3 return_fmt (Optional) + +requests a particular format or set of formats in reporting +availability + +=cut + +sub GetAvailability { + my ($cgi) = @_; + + my $out = "\n"; + $out .= "\n"; + + foreach my $id ( split( / /, $cgi->param('id') ) ) { + if ( $cgi->param('id_type') eq "item" ) { + my ( $biblionumber, $status, $msg, $location, $itemcallnumber ) = _availability($id); + + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " " . $id . "\n"; + $out .= " " . $status . "\n"; + if ($msg) { $out .= " " . $msg . "\n"; } + if ($location) { $out .= " " . $location . "\n"; } + + if ($itemcallnumber) { + $out .= " " . $itemcallnumber . "\n"; + } + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + } else { + my $status; + my $msg; + my $items = Koha::Items->search( { biblionumber => $id } ); + if ( $items->count ) { + + # Open XML + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + + # We loop over the items to clean them + while ( my $item = $items->next ) { + my $itemnumber = $item->itemnumber; + my ( $biblionumber, $status, $msg, $location, $itemcallnumber ) = _availability($itemnumber); + $out .= " \n"; + $out .= " \n"; + $out .= " " . $itemnumber . "\n"; + $out .= " " . $status . "\n"; + if ($msg) { $out .= " " . $msg . "\n"; } + if ($location) { $out .= " " . $location . "\n"; } + + if ($itemcallnumber) { + $out .= " " . $itemcallnumber . "\n"; + } + $out .= " \n"; + $out .= " \n"; + } + + # Close XML + $out .= " \n"; + $out .= " \n"; + } else { + $status = "unknown"; + $msg = "Error: could not retrieve availability for this ID"; + } + } + } + $out .= "\n"; + + return $out; +} + +=head2 GetRecords + +Given a list of biblionumbers, returns a list of record objects that +contain bibliographic information, as well as associated holdings and item +information. The caller may request a specific metadata schema for the +record objects to be returned. + +This function behaves similarly to HarvestBibliographicRecords and +HarvestExpandedRecords in Data Aggregation, but allows quick, real time +lookup by bibliographic identifier. + +You can use OAI-PMH ListRecords instead of this service. + +Parameters: + + - id (Required) + list of system record identifiers + - id_type (Optional) + Defines the metadata schema in which the records are returned, + possible values: + - MARCXML + +=cut + +sub GetRecords { + my ($cgi) = @_; + + # Check if the schema is supported. For now, GetRecords only supports MARCXML + if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) { + return { code => 'UnsupportedSchema' }; + } + + my @records; + + # Loop over biblionumbers + foreach my $biblionumber ( split( / /, $cgi->param('id') ) ) { + + # Get the biblioitem from the biblionumber + my $biblio = Koha::Biblios->find($biblionumber); + unless ($biblio) { + push @records, { code => "RecordNotFound" }; + next; + } + + my $biblioitem = $biblio->biblioitem->unblessed; + + my $record = $biblio->metadata_record( { embed_items => 1, interface => 'opac' } ); + if ($record) { + $biblioitem->{marcxml} = $record->as_xml_record( C4::Context->preference('marcflavour') ); + } + + # Get most of the needed data + my $biblioitemnumber = $biblioitem->{'biblioitemnumber'}; + my $checkouts = Koha::Checkouts->search( + { biblionumber => $biblionumber }, + { + join => 'item', + '+select' => ['item.barcode'], + '+as' => ['barcode'], + } + )->unblessed; + foreach my $checkout (@$checkouts) { + delete $checkout->{'borrowernumber'}; + } + my @items = $biblio->items->filter_by_visible_in_opac->as_list; + + $biblioitem->{items}->{item} = []; + + # We loop over the items to clean them + foreach my $item (@items) { + my %item = %{ $item->unblessed }; + + # This hides additional XML subfields, we don't need these info + delete $item{'more_subfields_xml'}; + + # Display branch names instead of branch codes + my $home_library = $item->home_branch; + my $holding_library = $item->holding_branch; + $item{'homebranchname'} = $home_library ? $home_library->branchname : ''; + $item{'holdingbranchname'} = $holding_library ? $holding_library->branchname : ''; + + if ( $item->location ) { + my $authorised_value = Koha::AuthorisedValues->get_description_by_koha_field( + { frameworkcode => '', kohafield => 'items.location', authorised_value => $item->location } ); + if ($authorised_value) { + $item{location_description} = $authorised_value->{opac_description}; + } + } + + if ( $item->itype ) { + my $itemtype = Koha::ItemTypes->find( $item->itype ); + if ($itemtype) { + $item{itype_description} = $itemtype->description; + } + } + + my $transfer = $item->get_transfer; + if ($transfer) { + $item{transfer} = { + datesent => $transfer->datesent, + frombranch => $transfer->frombranch, + tobranch => $transfer->tobranch, + }; + } + + push @{ $biblioitem->{items}->{item} }, \%item; + } + + # Holds + my $holds = $biblio->current_holds->unblessed; + foreach my $hold (@$holds) { + delete $hold->{'borrowernumber'}; + } + + # Hashref building... + $biblioitem->{'reserves'}->{'reserve'} = $holds; + $biblioitem->{'issues'}->{'issue'} = $checkouts; + + push @records, $biblioitem; + } + + return { record => \@records }; +} + +=head2 GetAuthorityRecords + +Given a list of authority record identifiers, returns a list of record +objects that contain the authority records. The function user may request +a specific metadata schema for the record objects. + +Parameters: + + - id (Required) + list of authority record identifiers + - schema (Optional) + specifies the metadata schema of records to be returned, possible values: + - MARCXML + +=cut + +sub GetAuthorityRecords { + my ($cgi) = @_; + + # If the user asks for an unsupported schema, return an error code + if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) { + return { code => 'UnsupportedSchema' }; + } + + my @records; + + # Let's loop over the authority IDs + foreach my $authid ( split( / /, $cgi->param('id') ) ) { + + # Get the record as XML string, or error code + push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' }; + } + + return { record => \@records }; +} + +=head2 LookupPatron + +Looks up a patron in the ILS by an identifier, and returns the borrowernumber. + +Parameters: + + - id (Required) + an identifier used to look up the patron in Koha + - id_type (Optional) + the type of the identifier, possible values: + - cardnumber + - userid + - email + - borrowernumber + - firstname + - surname + +=cut + +sub LookupPatron { + my ($cgi) = @_; + + my $id = $cgi->param('id'); + if ( !$id ) { + return { message => 'PatronNotFound' }; + } + + my $patrons; + my $passed_id_type = $cgi->param('id_type'); + if ($passed_id_type) { + $patrons = Koha::Patrons->search( { $passed_id_type => $id } ); + } else { + foreach my $id_type ( + 'cardnumber', 'userid', 'email', 'borrowernumber', + 'surname', 'firstname' + ) + { + $patrons = Koha::Patrons->search( { $id_type => $id } ); + last if ( $patrons->count ); + } + } + unless ( $patrons->count ) { + return { message => 'PatronNotFound' }; + } + + return { id => $patrons->next->borrowernumber }; +} + +=head2 AuthenticatePatron + +Authenticates a user's login credentials and returns the identifier for +the patron. + +Parameters: + + - username (Required) + user's login identifier (userid or cardnumber) + - password (Required) + user's password + +=cut + +sub AuthenticatePatron { + my ($cgi) = @_; + my $username = $cgi->param('username'); + my $password = $cgi->param('password'); + my ( $status, $cardnumber, $userid, $patron ) = C4::Auth::checkpw( $username, $password ); + if ( $status == 1 ) { + + # Track the login + $patron->update_lastseen('connection'); + return { id => $patron->borrowernumber }; + } elsif ( $status == -2 ) { + return { code => 'PasswordExpired' }; + } else { + return { code => 'PatronNotFound' }; + } +} + +=head2 GetPatronInfo + +Returns specified information about the patron, based on options in the +request. This function can optionally return patron's contact information, +fine information, hold request information, and loan information. + +Parameters: + + - patron_id (Required) + the borrowernumber + - show_contact (Optional, default 1) + whether or not to return patron's contact information in the response + - show_fines (Optional, default 0) + whether or not to return fine information in the response + - show_holds (Optional, default 0) + whether or not to return hold request information in the response + - show_loans (Optional, default 0) + whether or not to return loan information request information in the response + - show_attributes (Optional, default 0) + whether or not to return additional patron attributes, when enabled the attributes + are limited to those marked as opac visible only. + +=cut + +sub GetPatronInfo { + my ($cgi) = @_; + + # Get Member details + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Cleaning the borrower hashref + my $borrower = $patron->unblessed; + $borrower->{charges} = sprintf "%.02f", + $patron->account->non_issues_charges; # FIXME Formatting should not be done here + my $library = Koha::Libraries->find( $borrower->{branchcode} ); + $borrower->{'branchname'} = $library ? $library->branchname : ''; + delete $borrower->{'userid'}; + delete $borrower->{'password'}; + + # Contact fields management + if ( defined $cgi->param('show_contact') && $cgi->param('show_contact') eq "0" ) { + + # Define contact fields + my @contactfields = ( + 'email', 'emailpro', 'fax', 'mobile', 'phone', 'phonepro', + 'streetnumber', 'zipcode', 'city', 'streettype', 'B_address', 'B_city', + 'B_email', 'B_phone', 'B_zipcode', 'address', 'address2', 'altcontactaddress1', + 'altcontactaddress2', 'altcontactaddress3', 'altcontactfirstname', 'altcontactphone', 'altcontactsurname', + 'altcontactzipcode' + ); + + # and delete them + foreach my $field (@contactfields) { + delete $borrower->{$field}; + } + } + + # Fines management + if ( $cgi->param('show_fines') && $cgi->param('show_fines') eq "1" ) { + $borrower->{fines}{fine} = $patron->account->lines->unblessed; + } + + # Reserves management + if ( $cgi->param('show_holds') && $cgi->param('show_holds') eq "1" ) { + + # Get borrower's reserves + my $holds = $patron->holds; + while ( my $hold = $holds->next ) { + + my ( $item, $biblio, $biblioitem ) = ( {}, {}, {} ); + + # Get additional information + if ( $hold->itemnumber ) { # item level holds + $item = Koha::Items->find( $hold->itemnumber ); + $biblio = $item->biblio; + $biblioitem = $biblio->biblioitem; + + # Remove unwanted fields + $item = $item->unblessed; + delete $item->{more_subfields_xml}; + $biblio = $biblio->unblessed; + $biblioitem = $biblioitem->unblessed; + } + + # Add additional fields + my $unblessed_hold = $hold->unblessed; + $unblessed_hold->{item} = { %$item, %$biblio, %$biblioitem }; + my $library = Koha::Libraries->find( $hold->branchcode ); + my $branchname = $library ? $library->branchname : ''; + $unblessed_hold->{branchname} = $branchname; + $biblio = Koha::Biblios->find( $hold->biblionumber ); # Should be $hold->get_biblio + $unblessed_hold->{title} = $biblio ? $biblio->title : ''; # Just in case, but should not be needed + + push @{ $borrower->{holds}{hold} }, $unblessed_hold; + + } + } + + # Issues management + if ( $cgi->param('show_loans') && $cgi->param('show_loans') eq "1" ) { + my $per_page = $cgi->param('loans_per_page'); + my $page = $cgi->param('loans_page'); + + my $pending_checkouts = $patron->pending_checkouts; + + if ( $page || $per_page ) { + $page ||= 1; + $per_page ||= 10; + $borrower->{total_loans} = $pending_checkouts->count(); + $pending_checkouts = $pending_checkouts->search( + undef, + { + rows => $per_page, + page => $page, + } + ); + } + + my @checkouts; + while ( my $c = $pending_checkouts->next ) { + + # FIXME We should only retrieve what is needed in the template + my $issue = $c->unblessed_all_relateds; + delete $issue->{'more_subfields_xml'}; + + # Is the item already on hold by another user? + $issue->{'holds_on_item'} = Koha::Holds->search( { itemnumber => $issue->{'itemnumber'} } )->count; + + # Is the record (next available item) on hold by another user? + $issue->{'holds_on_record'} = Koha::Holds->search( { biblionumber => $issue->{'biblionumber'} } )->count; + + push @checkouts, $issue; + } + $borrower->{'loans'}->{'loan'} = \@checkouts; + } + + my $show_attributes = $cgi->param('show_attributes'); + if ( $show_attributes && $show_attributes eq "1" ) { + + # FIXME Regression expected here, we do not retrieve the same field as previously + # Waiting for answer on bug 14257 comment 15 + $borrower->{'attributes'} = [ + map { + $_->type->opac_display + ? { + %{ $_->unblessed }, + %{ $_->type->unblessed }, + value => $_->attribute, # Backward compatibility + value_description => $_->description, # Awkward retro-compability... + } + : () + } $patron->extended_attributes->search->as_list + ]; + } + + # Add is expired information + $borrower->{'is_expired'} = $patron->is_expired ? 1 : 0; + + return $borrower; +} + +=head2 GetPatronStatus + +Returns a patron's status information. + +Parameters: + + - patron_id (Required) + the borrower ID + +=cut + +sub GetPatronStatus { + my ($cgi) = @_; + + # Get Member details + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Return the results + return { + type => $patron->categorycode, + status => 0, # TODO + expiry => $patron->dateexpiry, + }; +} + +=head2 GetServices + +Returns information about the services available on a particular item for +a particular patron. + +Parameters: + + - patron_id (Required) + a borrowernumber + - item_id (Required) + an itemnumber + +=cut + +sub GetServices { + my ($cgi) = @_; + + # Get the member, or return an error code if not found + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + my $borrower = $patron->unblessed; + + # Get the item, or return an error code if not found + my $itemnumber = $cgi->param('item_id'); + my $item = Koha::Items->find($itemnumber); + return { code => 'RecordNotFound' } unless $item; + + my @availablefor; + + # Reserve level management + my $biblionumber = $item->biblionumber; + my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber ); + if ( $canbookbereserved->{status} eq 'OK' ) { + push @availablefor, 'title level hold'; + my $canitembereserved = IsAvailableForItemLevelRequest( $item, $patron ); + if ($canitembereserved) { + push @availablefor, 'item level hold'; + } + } + + # Reserve cancellation management + my $holds = $patron->holds; + my @reserveditems; + while ( my $hold = $holds->next ) { # FIXME This could be improved + push @reserveditems, $hold->itemnumber; + } + if ( grep { $itemnumber eq $_ } @reserveditems ) { + push @availablefor, 'hold cancellation'; + } + + # Renewal management + my @renewal = CanBookBeRenewed( $patron, $item->checkout ); # TODO: Error if issue not found? + if ( $renewal[0] ) { + push @availablefor, 'loan renewal'; + } + + # Issuing management + my $barcode = $item->barcode || ''; + $barcode = barcodedecode($barcode) if $barcode; + if ($barcode) { + my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $barcode ); + + # TODO push @availablefor, 'loan'; + } + + my $out; + $out->{'AvailableFor'} = \@availablefor; + + return $out; +} + +=head2 RenewLoan + +Extends the due date for a borrower's existing issue. + +Parameters: + + - patron_id (Required) + a borrowernumber + - item_id (Required) + an itemnumber + - desired_due_date (Required) + the date the patron would like the item returned by + +=cut + +sub RenewLoan { + my ($cgi) = @_; + + # Get borrower infos or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Get the item, or return an error code + my $itemnumber = $cgi->param('item_id'); # TODO: Refactor and send issue_id instead? + my $item = Koha::Items->find($itemnumber); + + return { code => 'RecordNotFound' } unless $item; + + my $issue = $item->checkout; + return unless $issue; # FIXME should be handled + + # Add renewal if possible + my @renewal = CanBookBeRenewed( $patron, $issue ); + if ( $renewal[0] ) { + AddRenewal( + { + borrowernumber => $borrowernumber, + itemnumber => $itemnumber, + seen => 0 + } + ); + } + + # Hashref building + my $out; + $out->{'renewals'} = $issue->renewals_count; + + # FIXME Unusual date formatting + $out->{date_due} = dt_from_string( $issue->date_due )->strftime('%Y-%m-%d %H:%M'); + $out->{'success'} = $renewal[0]; + $out->{'error'} = $renewal[1]; + + return $out; +} + +=head2 HoldTitle + +Creates, for a borrower, a biblio-level hold reserve. + +Parameters: + + - patron_id (Required) + a borrowernumber + - bib_id (Required) + a biblionumber + - request_location (Required) + IP address where the end user request is being placed + - pickup_location (Optional) + a branch code indicating the location to which to deliver the item for pickup + - start_date (Optional) + date after which hold request is no longer needed if the document has not been made available + - expiry_date (Optional) + date after which item returned to shelf if item is not picked up + +=cut + +sub HoldTitle { + my ($cgi) = @_; + + # Get the borrower or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # If borrower is restricted return an error code + return { code => 'PatronRestricted' } if $patron->is_debarred; + + # Check for patron expired, category and syspref settings + return { code => 'PatronExpired' } + if ( $patron->category->effective_BlockExpiredPatronOpacActions_contains('hold') && $patron->is_expired ); + + # Get the biblio record, or return an error code + my $biblionumber = $cgi->param('bib_id'); + my $biblio = Koha::Biblios->find($biblionumber); + return { code => 'RecordNotFound' } unless $biblio; + + my @hostitems = get_hostitemnumbers_of($biblionumber); + my @itemnumbers; + if (@hostitems) { + push( @itemnumbers, @hostitems ); + } + + my $items = + Koha::Items->search( { -or => { biblionumber => $biblionumber, itemnumber => { in => \@itemnumbers } } } ); + + unless ( $items->count ) { + return { code => 'NoItems' }; + } + + my $title = $biblio ? $biblio->title : ''; + + # Check if the biblio can be reserved + my $code = CanBookBeReserved( $borrowernumber, $biblionumber )->{status}; + return { code => $code } unless ( $code eq 'OK' ); + + my $branch; + + # Pickup branch management + if ( $cgi->param('pickup_location') ) { + $branch = $cgi->param('pickup_location'); + return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch); + } else { # if the request provide no branch, use the borrower's branch + $branch = $patron->branchcode; + } + + my $destination = Koha::Libraries->find($branch); + return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location; + return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred( { to => $destination } ); + + my $resdate = $cgi->param('start_date') || $cgi->param('needed_before_date'); + my $expdate = $cgi->param('expiry_date') || $cgi->param('pickup_expiry_date'); + #RKE comment for reserves + my $comment = $cgi->param('comment'); + #RKE End + + # Add the reserve + # $branch, $borrowernumber, $biblionumber, + # $constraint, $bibitems, $priority, $resdate, $expdate, $notes, + # $title, $checkitem, $found + my $priority = C4::Reserves::CalculatePriority($biblionumber); + AddReserve( + { + branchcode => $branch, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + priority => $priority, + reservation_date => $resdate, + expiration_date => $expdate, + title => $title, + notes => $comment, + } + ); + + # Hashref building + my $out; + $out->{'title'} = $title; + my $library = Koha::Libraries->find($branch); + $out->{'pickup_location'} = $library ? $library->branchname : ''; + + # TODO $out->{'date_available'} = ''; + + return $out; +} + +=head2 HoldItem + +Creates, for a borrower, an item-level hold request on a specific item of +a bibliographic record in Koha. + +Parameters: + + - patron_id (Required) + a borrowernumber + - bib_id (Required) + a biblionumber + - item_id (Required) + an itemnumber + - pickup_location (Optional) + a branch code indicating the location to which to deliver the item for pickup + - start_date (Optional) + date after which hold request is no longer needed if the item has not been made available + - expiry_date (Optional) + date after which item returned to shelf if item is not picked up + +=cut + +sub HoldItem { + my ($cgi) = @_; + + # Get the borrower or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # If borrower is restricted return an error code + return { code => 'PatronRestricted' } if $patron->is_debarred; + + # Check for patron expired, category and syspref settings + return { code => 'PatronExpired' } + if ( $patron->category->effective_BlockExpiredPatronOpacActions_contains('hold') && $patron->is_expired ); + + # Get the biblio or return an error code + my $biblionumber = $cgi->param('bib_id'); + my $biblio = Koha::Biblios->find($biblionumber); + return { code => 'RecordNotFound' } unless $biblio; + + my $title = $biblio ? $biblio->title : ''; + + # Get the item or return an error code + my $itemnumber = $cgi->param('item_id'); + my $item = Koha::Items->find($itemnumber); + return { code => 'RecordNotFound' } unless $item; + + # If the biblio does not match the item, return an error code + return { code => 'RecordNotFound' } if $item->biblionumber ne $biblio->biblionumber; + + # Pickup branch management + my $branch; + if ( $cgi->param('pickup_location') ) { + $branch = $cgi->param('pickup_location'); + return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch); + } else { # if the request provide no branch, use the borrower's branch + $branch = $patron->branchcode; + } + + # Check for item disponibility + my $canitembereserved = C4::Reserves::CanItemBeReserved( $patron, $item, $branch )->{status}; + return { code => $canitembereserved } unless $canitembereserved eq 'OK'; + + my $resdate = $cgi->param('start_date'); + my $expdate = $cgi->param('expiry_date'); + # RKE comment fpr reserves + my $comment = $cgi->param('comment'); + # RKE End + + # Add the reserve + my $priority = C4::Reserves::CalculatePriority($biblionumber); + AddReserve( + { + branchcode => $branch, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + priority => $priority, + reservation_date => $resdate, + expiration_date => $expdate, + title => $title, + itemnumber => $itemnumber, + notes => $comment, + } + ); + + # Hashref building + my $out; + my $library = Koha::Libraries->find($branch); + $out->{'pickup_location'} = $library ? $library->branchname : ''; + + # TODO $out->{'date_available'} = ''; + + return $out; +} + +=head2 CancelHold + +Cancels an active reserve request for the borrower. + +Parameters: + + - patron_id (Required) + a borrowernumber + - item_id (Required) + a reserve_id + +=cut + +sub CancelHold { + my ($cgi) = @_; + + # Get the borrower or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Get the reserve or return an error code + my $reserve_id = $cgi->param('item_id'); + my $hold = Koha::Holds->find($reserve_id); + return { code => 'RecordNotFound' } unless $hold; + + # Check if reserve belongs to the borrower and if it is in a state which allows cancellation + return { code => 'BorrowerCannotCancelHold' } + if $hold->borrowernumber ne $patron->borrowernumber || !$hold->is_cancelable_from_opac; + + $hold->cancel; + + return { code => 'Canceled' }; +} + +=head2 _availability + +Returns, for an itemnumber, an array containing availability information. + + my ($biblionumber, $status, $msg, $location) = _availability($id); + +=cut + +sub _availability { + my ($itemnumber) = @_; + my $item = Koha::Items->find($itemnumber); + + unless ($item) { + return ( undef, __('unknown'), __('Error: could not retrieve availability for this ID'), undef ); + } + + my $biblionumber = $item->biblioitemnumber; + my $library = Koha::Libraries->find( $item->holdingbranch ); + my $location = $library ? $library->branchname : ''; + my $itemcallnumber = $item->itemcallnumber; + + if ( $item->effective_not_for_loan_status ) { + return ( $biblionumber, __('not available'), __('Not for loan'), $location, $itemcallnumber ); + } elsif ( $item->onloan ) { + return ( $biblionumber, __('not available'), __('Checked out'), $location, $itemcallnumber ); + } elsif ( $item->itemlost ) { + return ( $biblionumber, __('not available'), __('Item lost'), $location, $itemcallnumber ); + } elsif ( $item->withdrawn ) { + return ( $biblionumber, __('not available'), __('Item withdrawn'), $location, $itemcallnumber ); + } elsif ( $item->damaged ) { + return ( $biblionumber, __('not available'), __('Item damaged'), $location, $itemcallnumber ); + } elsif ( $item->get_transfer ) { + return ( $biblionumber, __('not available'), __('In transit'), $location, $itemcallnumber ); + } elsif ( $item->current_holds->next ) { + return ( $biblionumber, __('not available'), __('On hold'), $location, $itemcallnumber ); + } else { + return ( $biblionumber, __('available'), undef, $location, $itemcallnumber ); + } +} + +1; diff --git a/files/lib/C4/ILSDI/Services.pm.orig b/files/lib/C4/ILSDI/Services.pm.orig new file mode 100644 index 0000000..597a9dd --- /dev/null +++ b/files/lib/C4/ILSDI/Services.pm.orig @@ -0,0 +1,1015 @@ +package C4::ILSDI::Services; + +# Copyright 2009 SARL Biblibre +# +# 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 strict; +use warnings; + +use C4::Members; +use C4::Items qw( get_hostitemnumbers_of ); +use C4::Circulation qw( CanBookBeRenewed barcodedecode CanBookBeIssued AddRenewal ); +use C4::Accounts; +use C4::Reserves qw( CanBookBeReserved IsAvailableForItemLevelRequest CalculatePriority AddReserve CanItemBeReserved ); +use C4::Context; +use C4::Auth; +use CGI qw ( -utf8 ); +use DateTime; +use C4::Auth; +use Koha::DateUtils qw( dt_from_string ); +use C4::AuthoritiesMarc qw( GetAuthorityXML ); + +use Koha::Biblios; +use Koha::Checkouts; +use Koha::I18N qw(__); +use Koha::Items; +use Koha::Libraries; +use Koha::Patrons; + +=head1 NAME + +C4::ILS-DI::Services - ILS-DI Services + +=head1 DESCRIPTION + +Each function in this module represents an ILS-DI service. +They all takes a CGI instance as argument and most of them return a +hashref that will be printed by XML::Simple in opac/ilsdi.pl + +=head1 SYNOPSIS + + use C4::ILSDI::Services; + use XML::Simple; + use CGI qw ( -utf8 ); + + my $cgi = new CGI; + + $out = LookupPatron($cgi); + + print CGI::header('text/xml'); + print XMLout($out, + noattr => 1, + noescape => 1, + nosort => 1, + xmldecl => '', + RootName => 'LookupPatron', + SuppressEmpty => 1); + +=cut + +=head1 FUNCTIONS + +=head2 GetAvailability + +Given a set of biblionumbers or itemnumbers, returns a list with +availability of the items associated with the identifiers. + +Parameters: + +=head3 id (Required) + +list of either biblionumbers or itemnumbers + +=head3 id_type (Required) + +defines the type of record identifier being used in the request, +possible values: + + - bib + - item + +=head3 return_type (Optional) + +requests a particular level of detail in reporting availability, +possible values: + + - bib + - item + +=head3 return_fmt (Optional) + +requests a particular format or set of formats in reporting +availability + +=cut + +sub GetAvailability { + my ($cgi) = @_; + + my $out = "\n"; + $out .= "\n"; + + foreach my $id ( split( / /, $cgi->param('id') ) ) { + if ( $cgi->param('id_type') eq "item" ) { + my ( $biblionumber, $status, $msg, $location, $itemcallnumber ) = _availability($id); + + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " " . $id . "\n"; + $out .= " " . $status . "\n"; + if ($msg) { $out .= " " . $msg . "\n"; } + if ($location) { $out .= " " . $location . "\n"; } + + if ($itemcallnumber) { + $out .= " " . $itemcallnumber . "\n"; + } + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + } else { + my $status; + my $msg; + my $items = Koha::Items->search( { biblionumber => $id } ); + if ( $items->count ) { + + # Open XML + $out .= " \n"; + $out .= " \n"; + $out .= " \n"; + + # We loop over the items to clean them + while ( my $item = $items->next ) { + my $itemnumber = $item->itemnumber; + my ( $biblionumber, $status, $msg, $location, $itemcallnumber ) = _availability($itemnumber); + $out .= " \n"; + $out .= " \n"; + $out .= " " . $itemnumber . "\n"; + $out .= " " . $status . "\n"; + if ($msg) { $out .= " " . $msg . "\n"; } + if ($location) { $out .= " " . $location . "\n"; } + + if ($itemcallnumber) { + $out .= " " . $itemcallnumber . "\n"; + } + $out .= " \n"; + $out .= " \n"; + } + + # Close XML + $out .= " \n"; + $out .= " \n"; + } else { + $status = "unknown"; + $msg = "Error: could not retrieve availability for this ID"; + } + } + } + $out .= "\n"; + + return $out; +} + +=head2 GetRecords + +Given a list of biblionumbers, returns a list of record objects that +contain bibliographic information, as well as associated holdings and item +information. The caller may request a specific metadata schema for the +record objects to be returned. + +This function behaves similarly to HarvestBibliographicRecords and +HarvestExpandedRecords in Data Aggregation, but allows quick, real time +lookup by bibliographic identifier. + +You can use OAI-PMH ListRecords instead of this service. + +Parameters: + + - id (Required) + list of system record identifiers + - id_type (Optional) + Defines the metadata schema in which the records are returned, + possible values: + - MARCXML + +=cut + +sub GetRecords { + my ($cgi) = @_; + + # Check if the schema is supported. For now, GetRecords only supports MARCXML + if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) { + return { code => 'UnsupportedSchema' }; + } + + my @records; + + # Loop over biblionumbers + foreach my $biblionumber ( split( / /, $cgi->param('id') ) ) { + + # Get the biblioitem from the biblionumber + my $biblio = Koha::Biblios->find($biblionumber); + unless ($biblio) { + push @records, { code => "RecordNotFound" }; + next; + } + + my $biblioitem = $biblio->biblioitem->unblessed; + + my $record = $biblio->metadata_record( { embed_items => 1, interface => 'opac' } ); + if ($record) { + $biblioitem->{marcxml} = $record->as_xml_record( C4::Context->preference('marcflavour') ); + } + + # Get most of the needed data + my $biblioitemnumber = $biblioitem->{'biblioitemnumber'}; + my $checkouts = Koha::Checkouts->search( + { biblionumber => $biblionumber }, + { + join => 'item', + '+select' => ['item.barcode'], + '+as' => ['barcode'], + } + )->unblessed; + foreach my $checkout (@$checkouts) { + delete $checkout->{'borrowernumber'}; + } + my @items = $biblio->items->filter_by_visible_in_opac->as_list; + + $biblioitem->{items}->{item} = []; + + # We loop over the items to clean them + foreach my $item (@items) { + my %item = %{ $item->unblessed }; + + # This hides additional XML subfields, we don't need these info + delete $item{'more_subfields_xml'}; + + # Display branch names instead of branch codes + my $home_library = $item->home_branch; + my $holding_library = $item->holding_branch; + $item{'homebranchname'} = $home_library ? $home_library->branchname : ''; + $item{'holdingbranchname'} = $holding_library ? $holding_library->branchname : ''; + + if ( $item->location ) { + my $authorised_value = Koha::AuthorisedValues->get_description_by_koha_field( + { frameworkcode => '', kohafield => 'items.location', authorised_value => $item->location } ); + if ($authorised_value) { + $item{location_description} = $authorised_value->{opac_description}; + } + } + + if ( $item->itype ) { + my $itemtype = Koha::ItemTypes->find( $item->itype ); + if ($itemtype) { + $item{itype_description} = $itemtype->description; + } + } + + my $transfer = $item->get_transfer; + if ($transfer) { + $item{transfer} = { + datesent => $transfer->datesent, + frombranch => $transfer->frombranch, + tobranch => $transfer->tobranch, + }; + } + + push @{ $biblioitem->{items}->{item} }, \%item; + } + + # Holds + my $holds = $biblio->current_holds->unblessed; + foreach my $hold (@$holds) { + delete $hold->{'borrowernumber'}; + } + + # Hashref building... + $biblioitem->{'reserves'}->{'reserve'} = $holds; + $biblioitem->{'issues'}->{'issue'} = $checkouts; + + push @records, $biblioitem; + } + + return { record => \@records }; +} + +=head2 GetAuthorityRecords + +Given a list of authority record identifiers, returns a list of record +objects that contain the authority records. The function user may request +a specific metadata schema for the record objects. + +Parameters: + + - id (Required) + list of authority record identifiers + - schema (Optional) + specifies the metadata schema of records to be returned, possible values: + - MARCXML + +=cut + +sub GetAuthorityRecords { + my ($cgi) = @_; + + # If the user asks for an unsupported schema, return an error code + if ( $cgi->param('schema') and $cgi->param('schema') ne "MARCXML" ) { + return { code => 'UnsupportedSchema' }; + } + + my @records; + + # Let's loop over the authority IDs + foreach my $authid ( split( / /, $cgi->param('id') ) ) { + + # Get the record as XML string, or error code + push @records, GetAuthorityXML($authid) || { code => 'RecordNotFound' }; + } + + return { record => \@records }; +} + +=head2 LookupPatron + +Looks up a patron in the ILS by an identifier, and returns the borrowernumber. + +Parameters: + + - id (Required) + an identifier used to look up the patron in Koha + - id_type (Optional) + the type of the identifier, possible values: + - cardnumber + - userid + - email + - borrowernumber + - firstname + - surname + +=cut + +sub LookupPatron { + my ($cgi) = @_; + + my $id = $cgi->param('id'); + if ( !$id ) { + return { message => 'PatronNotFound' }; + } + + my $patrons; + my $passed_id_type = $cgi->param('id_type'); + if ($passed_id_type) { + $patrons = Koha::Patrons->search( { $passed_id_type => $id } ); + } else { + foreach my $id_type ( + 'cardnumber', 'userid', 'email', 'borrowernumber', + 'surname', 'firstname' + ) + { + $patrons = Koha::Patrons->search( { $id_type => $id } ); + last if ( $patrons->count ); + } + } + unless ( $patrons->count ) { + return { message => 'PatronNotFound' }; + } + + return { id => $patrons->next->borrowernumber }; +} + +=head2 AuthenticatePatron + +Authenticates a user's login credentials and returns the identifier for +the patron. + +Parameters: + + - username (Required) + user's login identifier (userid or cardnumber) + - password (Required) + user's password + +=cut + +sub AuthenticatePatron { + my ($cgi) = @_; + my $username = $cgi->param('username'); + my $password = $cgi->param('password'); + my ( $status, $cardnumber, $userid, $patron ) = C4::Auth::checkpw( $username, $password ); + if ( $status == 1 ) { + + # Track the login + $patron->update_lastseen('connection'); + return { id => $patron->borrowernumber }; + } elsif ( $status == -2 ) { + return { code => 'PasswordExpired' }; + } else { + return { code => 'PatronNotFound' }; + } +} + +=head2 GetPatronInfo + +Returns specified information about the patron, based on options in the +request. This function can optionally return patron's contact information, +fine information, hold request information, and loan information. + +Parameters: + + - patron_id (Required) + the borrowernumber + - show_contact (Optional, default 1) + whether or not to return patron's contact information in the response + - show_fines (Optional, default 0) + whether or not to return fine information in the response + - show_holds (Optional, default 0) + whether or not to return hold request information in the response + - show_loans (Optional, default 0) + whether or not to return loan information request information in the response + - show_attributes (Optional, default 0) + whether or not to return additional patron attributes, when enabled the attributes + are limited to those marked as opac visible only. + +=cut + +sub GetPatronInfo { + my ($cgi) = @_; + + # Get Member details + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Cleaning the borrower hashref + my $borrower = $patron->unblessed; + $borrower->{charges} = sprintf "%.02f", + $patron->account->non_issues_charges; # FIXME Formatting should not be done here + my $library = Koha::Libraries->find( $borrower->{branchcode} ); + $borrower->{'branchname'} = $library ? $library->branchname : ''; + delete $borrower->{'userid'}; + delete $borrower->{'password'}; + + # Contact fields management + if ( defined $cgi->param('show_contact') && $cgi->param('show_contact') eq "0" ) { + + # Define contact fields + my @contactfields = ( + 'email', 'emailpro', 'fax', 'mobile', 'phone', 'phonepro', + 'streetnumber', 'zipcode', 'city', 'streettype', 'B_address', 'B_city', + 'B_email', 'B_phone', 'B_zipcode', 'address', 'address2', 'altcontactaddress1', + 'altcontactaddress2', 'altcontactaddress3', 'altcontactfirstname', 'altcontactphone', 'altcontactsurname', + 'altcontactzipcode' + ); + + # and delete them + foreach my $field (@contactfields) { + delete $borrower->{$field}; + } + } + + # Fines management + if ( $cgi->param('show_fines') && $cgi->param('show_fines') eq "1" ) { + $borrower->{fines}{fine} = $patron->account->lines->unblessed; + } + + # Reserves management + if ( $cgi->param('show_holds') && $cgi->param('show_holds') eq "1" ) { + + # Get borrower's reserves + my $holds = $patron->holds; + while ( my $hold = $holds->next ) { + + my ( $item, $biblio, $biblioitem ) = ( {}, {}, {} ); + + # Get additional information + if ( $hold->itemnumber ) { # item level holds + $item = Koha::Items->find( $hold->itemnumber ); + $biblio = $item->biblio; + $biblioitem = $biblio->biblioitem; + + # Remove unwanted fields + $item = $item->unblessed; + delete $item->{more_subfields_xml}; + $biblio = $biblio->unblessed; + $biblioitem = $biblioitem->unblessed; + } + + # Add additional fields + my $unblessed_hold = $hold->unblessed; + $unblessed_hold->{item} = { %$item, %$biblio, %$biblioitem }; + my $library = Koha::Libraries->find( $hold->branchcode ); + my $branchname = $library ? $library->branchname : ''; + $unblessed_hold->{branchname} = $branchname; + $biblio = Koha::Biblios->find( $hold->biblionumber ); # Should be $hold->get_biblio + $unblessed_hold->{title} = $biblio ? $biblio->title : ''; # Just in case, but should not be needed + + push @{ $borrower->{holds}{hold} }, $unblessed_hold; + + } + } + + # Issues management + if ( $cgi->param('show_loans') && $cgi->param('show_loans') eq "1" ) { + my $per_page = $cgi->param('loans_per_page'); + my $page = $cgi->param('loans_page'); + + my $pending_checkouts = $patron->pending_checkouts; + + if ( $page || $per_page ) { + $page ||= 1; + $per_page ||= 10; + $borrower->{total_loans} = $pending_checkouts->count(); + $pending_checkouts = $pending_checkouts->search( + undef, + { + rows => $per_page, + page => $page, + } + ); + } + + my @checkouts; + while ( my $c = $pending_checkouts->next ) { + + # FIXME We should only retrieve what is needed in the template + my $issue = $c->unblessed_all_relateds; + delete $issue->{'more_subfields_xml'}; + + # Is the item already on hold by another user? + $issue->{'holds_on_item'} = Koha::Holds->search( { itemnumber => $issue->{'itemnumber'} } )->count; + + # Is the record (next available item) on hold by another user? + $issue->{'holds_on_record'} = Koha::Holds->search( { biblionumber => $issue->{'biblionumber'} } )->count; + + push @checkouts, $issue; + } + $borrower->{'loans'}->{'loan'} = \@checkouts; + } + + my $show_attributes = $cgi->param('show_attributes'); + if ( $show_attributes && $show_attributes eq "1" ) { + + # FIXME Regression expected here, we do not retrieve the same field as previously + # Waiting for answer on bug 14257 comment 15 + $borrower->{'attributes'} = [ + map { + $_->type->opac_display + ? { + %{ $_->unblessed }, + %{ $_->type->unblessed }, + value => $_->attribute, # Backward compatibility + value_description => $_->description, # Awkward retro-compability... + } + : () + } $patron->extended_attributes->search->as_list + ]; + } + + # Add is expired information + $borrower->{'is_expired'} = $patron->is_expired ? 1 : 0; + + return $borrower; +} + +=head2 GetPatronStatus + +Returns a patron's status information. + +Parameters: + + - patron_id (Required) + the borrower ID + +=cut + +sub GetPatronStatus { + my ($cgi) = @_; + + # Get Member details + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Return the results + return { + type => $patron->categorycode, + status => 0, # TODO + expiry => $patron->dateexpiry, + }; +} + +=head2 GetServices + +Returns information about the services available on a particular item for +a particular patron. + +Parameters: + + - patron_id (Required) + a borrowernumber + - item_id (Required) + an itemnumber + +=cut + +sub GetServices { + my ($cgi) = @_; + + # Get the member, or return an error code if not found + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + my $borrower = $patron->unblessed; + + # Get the item, or return an error code if not found + my $itemnumber = $cgi->param('item_id'); + my $item = Koha::Items->find($itemnumber); + return { code => 'RecordNotFound' } unless $item; + + my @availablefor; + + # Reserve level management + my $biblionumber = $item->biblionumber; + my $canbookbereserved = CanBookBeReserved( $borrower, $biblionumber ); + if ( $canbookbereserved->{status} eq 'OK' ) { + push @availablefor, 'title level hold'; + my $canitembereserved = IsAvailableForItemLevelRequest( $item, $patron ); + if ($canitembereserved) { + push @availablefor, 'item level hold'; + } + } + + # Reserve cancellation management + my $holds = $patron->holds; + my @reserveditems; + while ( my $hold = $holds->next ) { # FIXME This could be improved + push @reserveditems, $hold->itemnumber; + } + if ( grep { $itemnumber eq $_ } @reserveditems ) { + push @availablefor, 'hold cancellation'; + } + + # Renewal management + my @renewal = CanBookBeRenewed( $patron, $item->checkout ); # TODO: Error if issue not found? + if ( $renewal[0] ) { + push @availablefor, 'loan renewal'; + } + + # Issuing management + my $barcode = $item->barcode || ''; + $barcode = barcodedecode($barcode) if $barcode; + if ($barcode) { + my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron, $barcode ); + + # TODO push @availablefor, 'loan'; + } + + my $out; + $out->{'AvailableFor'} = \@availablefor; + + return $out; +} + +=head2 RenewLoan + +Extends the due date for a borrower's existing issue. + +Parameters: + + - patron_id (Required) + a borrowernumber + - item_id (Required) + an itemnumber + - desired_due_date (Required) + the date the patron would like the item returned by + +=cut + +sub RenewLoan { + my ($cgi) = @_; + + # Get borrower infos or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Get the item, or return an error code + my $itemnumber = $cgi->param('item_id'); # TODO: Refactor and send issue_id instead? + my $item = Koha::Items->find($itemnumber); + + return { code => 'RecordNotFound' } unless $item; + + my $issue = $item->checkout; + return unless $issue; # FIXME should be handled + + # Add renewal if possible + my @renewal = CanBookBeRenewed( $patron, $issue ); + if ( $renewal[0] ) { + AddRenewal( + { + borrowernumber => $borrowernumber, + itemnumber => $itemnumber, + seen => 0 + } + ); + } + + # Hashref building + my $out; + $out->{'renewals'} = $issue->renewals_count; + + # FIXME Unusual date formatting + $out->{date_due} = dt_from_string( $issue->date_due )->strftime('%Y-%m-%d %H:%M'); + $out->{'success'} = $renewal[0]; + $out->{'error'} = $renewal[1]; + + return $out; +} + +=head2 HoldTitle + +Creates, for a borrower, a biblio-level hold reserve. + +Parameters: + + - patron_id (Required) + a borrowernumber + - bib_id (Required) + a biblionumber + - request_location (Required) + IP address where the end user request is being placed + - pickup_location (Optional) + a branch code indicating the location to which to deliver the item for pickup + - start_date (Optional) + date after which hold request is no longer needed if the document has not been made available + - expiry_date (Optional) + date after which item returned to shelf if item is not picked up + +=cut + +sub HoldTitle { + my ($cgi) = @_; + + # Get the borrower or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # If borrower is restricted return an error code + return { code => 'PatronRestricted' } if $patron->is_debarred; + + # Check for patron expired, category and syspref settings + return { code => 'PatronExpired' } + if ( $patron->category->effective_BlockExpiredPatronOpacActions_contains('hold') && $patron->is_expired ); + + # Get the biblio record, or return an error code + my $biblionumber = $cgi->param('bib_id'); + my $biblio = Koha::Biblios->find($biblionumber); + return { code => 'RecordNotFound' } unless $biblio; + + my @hostitems = get_hostitemnumbers_of($biblionumber); + my @itemnumbers; + if (@hostitems) { + push( @itemnumbers, @hostitems ); + } + + my $items = + Koha::Items->search( { -or => { biblionumber => $biblionumber, itemnumber => { in => \@itemnumbers } } } ); + + unless ( $items->count ) { + return { code => 'NoItems' }; + } + + my $title = $biblio ? $biblio->title : ''; + + # Check if the biblio can be reserved + my $code = CanBookBeReserved( $borrowernumber, $biblionumber )->{status}; + return { code => $code } unless ( $code eq 'OK' ); + + my $branch; + + # Pickup branch management + if ( $cgi->param('pickup_location') ) { + $branch = $cgi->param('pickup_location'); + return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch); + } else { # if the request provide no branch, use the borrower's branch + $branch = $patron->branchcode; + } + + my $destination = Koha::Libraries->find($branch); + return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location; + return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred( { to => $destination } ); + + my $resdate = $cgi->param('start_date'); + my $expdate = $cgi->param('expiry_date'); + + # Add the reserve + # $branch, $borrowernumber, $biblionumber, + # $constraint, $bibitems, $priority, $resdate, $expdate, $notes, + # $title, $checkitem, $found + my $priority = C4::Reserves::CalculatePriority($biblionumber); + AddReserve( + { + branchcode => $branch, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + priority => $priority, + reservation_date => $resdate, + expiration_date => $expdate, + title => $title, + } + ); + + # Hashref building + my $out; + $out->{'title'} = $title; + my $library = Koha::Libraries->find($branch); + $out->{'pickup_location'} = $library ? $library->branchname : ''; + + # TODO $out->{'date_available'} = ''; + + return $out; +} + +=head2 HoldItem + +Creates, for a borrower, an item-level hold request on a specific item of +a bibliographic record in Koha. + +Parameters: + + - patron_id (Required) + a borrowernumber + - bib_id (Required) + a biblionumber + - item_id (Required) + an itemnumber + - pickup_location (Optional) + a branch code indicating the location to which to deliver the item for pickup + - start_date (Optional) + date after which hold request is no longer needed if the item has not been made available + - expiry_date (Optional) + date after which item returned to shelf if item is not picked up + +=cut + +sub HoldItem { + my ($cgi) = @_; + + # Get the borrower or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # If borrower is restricted return an error code + return { code => 'PatronRestricted' } if $patron->is_debarred; + + # Check for patron expired, category and syspref settings + return { code => 'PatronExpired' } + if ( $patron->category->effective_BlockExpiredPatronOpacActions_contains('hold') && $patron->is_expired ); + + # Get the biblio or return an error code + my $biblionumber = $cgi->param('bib_id'); + my $biblio = Koha::Biblios->find($biblionumber); + return { code => 'RecordNotFound' } unless $biblio; + + my $title = $biblio ? $biblio->title : ''; + + # Get the item or return an error code + my $itemnumber = $cgi->param('item_id'); + my $item = Koha::Items->find($itemnumber); + return { code => 'RecordNotFound' } unless $item; + + # If the biblio does not match the item, return an error code + return { code => 'RecordNotFound' } if $item->biblionumber ne $biblio->biblionumber; + + # Pickup branch management + my $branch; + if ( $cgi->param('pickup_location') ) { + $branch = $cgi->param('pickup_location'); + return { code => 'LocationNotFound' } unless Koha::Libraries->find($branch); + } else { # if the request provide no branch, use the borrower's branch + $branch = $patron->branchcode; + } + + # Check for item disponibility + my $canitembereserved = C4::Reserves::CanItemBeReserved( $patron, $item, $branch )->{status}; + return { code => $canitembereserved } unless $canitembereserved eq 'OK'; + + my $resdate = $cgi->param('start_date'); + my $expdate = $cgi->param('expiry_date'); + + # Add the reserve + my $priority = C4::Reserves::CalculatePriority($biblionumber); + AddReserve( + { + branchcode => $branch, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + priority => $priority, + reservation_date => $resdate, + expiration_date => $expdate, + title => $title, + itemnumber => $itemnumber, + } + ); + + # Hashref building + my $out; + my $library = Koha::Libraries->find($branch); + $out->{'pickup_location'} = $library ? $library->branchname : ''; + + # TODO $out->{'date_available'} = ''; + + return $out; +} + +=head2 CancelHold + +Cancels an active reserve request for the borrower. + +Parameters: + + - patron_id (Required) + a borrowernumber + - item_id (Required) + a reserve_id + +=cut + +sub CancelHold { + my ($cgi) = @_; + + # Get the borrower or return an error code + my $borrowernumber = $cgi->param('patron_id'); + my $patron = Koha::Patrons->find($borrowernumber); + return { code => 'PatronNotFound' } unless $patron; + + # Get the reserve or return an error code + my $reserve_id = $cgi->param('item_id'); + my $hold = Koha::Holds->find($reserve_id); + return { code => 'RecordNotFound' } unless $hold; + + # Check if reserve belongs to the borrower and if it is in a state which allows cancellation + return { code => 'BorrowerCannotCancelHold' } + if $hold->borrowernumber ne $patron->borrowernumber || !$hold->is_cancelable_from_opac; + + $hold->cancel; + + return { code => 'Canceled' }; +} + +=head2 _availability + +Returns, for an itemnumber, an array containing availability information. + + my ($biblionumber, $status, $msg, $location) = _availability($id); + +=cut + +sub _availability { + my ($itemnumber) = @_; + my $item = Koha::Items->find($itemnumber); + + unless ($item) { + return ( undef, __('unknown'), __('Error: could not retrieve availability for this ID'), undef ); + } + + my $biblionumber = $item->biblioitemnumber; + my $library = Koha::Libraries->find( $item->holdingbranch ); + my $location = $library ? $library->branchname : ''; + my $itemcallnumber = $item->itemcallnumber; + + if ( $item->effective_not_for_loan_status ) { + return ( $biblionumber, __('not available'), __('Not for loan'), $location, $itemcallnumber ); + } elsif ( $item->onloan ) { + return ( $biblionumber, __('not available'), __('Checked out'), $location, $itemcallnumber ); + } elsif ( $item->itemlost ) { + return ( $biblionumber, __('not available'), __('Item lost'), $location, $itemcallnumber ); + } elsif ( $item->withdrawn ) { + return ( $biblionumber, __('not available'), __('Item withdrawn'), $location, $itemcallnumber ); + } elsif ( $item->damaged ) { + return ( $biblionumber, __('not available'), __('Item damaged'), $location, $itemcallnumber ); + } elsif ( $item->get_transfer ) { + return ( $biblionumber, __('not available'), __('In transit'), $location, $itemcallnumber ); + } elsif ( $item->current_holds->next ) { + return ( $biblionumber, __('not available'), __('On hold'), $location, $itemcallnumber ); + } else { + return ( $biblionumber, __('available'), undef, $location, $itemcallnumber ); + } +} + +1; diff --git a/files/opac/cgi-bin/opac/ilsdi.pl b/files/opac/cgi-bin/opac/ilsdi.pl new file mode 100755 index 0000000..572426b --- /dev/null +++ b/files/opac/cgi-bin/opac/ilsdi.pl @@ -0,0 +1,277 @@ +#!/usr/bin/perl + +# Copyright 2009 SARL Biblibre +# +# 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 C4::ILSDI::Services; +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); +use C4::Context; + +use List::MoreUtils qw( any ); +use XML::Simple qw( XMLout ); +use CGI qw ( -utf8 ); +use Net::Netmask; + +=head1 DLF ILS-DI for Koha + +This script is a basic implementation of ILS-DI protocol for Koha. +It acts like a dispatcher, that get the CGI request, check required and +optionals arguments, call a function from C4::ILS-DI, and finally +outputs the returned hashref as XML. + +=cut + +# Instantiate the CGI request +my $cgi = CGI->new; + +# List of available services, sorted by level +my @services = ( + 'Describe', # Not part of ILS-DI, online API doc + + # Level 1: Basic Discovery Interfaces + # 'HarvestBibliographicRecords', # OAI-PMH + # 'HarvestExpandedRecords', # OAI-PMH + 'GetAvailability', # FIXME Add bibliographic level + + # 'GoToBibliographicRequestPage' # I don't understant this one + # Level 2: Elementary OPAC supplement + # 'HarvestAuthorityRecords', # OAI-PMH + # 'HarvestHoldingsRecords', # OAI-PMH + 'GetRecords', # Note that we can use OAI-PMH for this too + + # 'Search', # TODO + # 'Scan', # TODO + 'GetAuthorityRecords', + + # 'OutputRewritablePage', # I don't understant this one + # 'OutputIntermediateFormat', # I don't understant this one + # Level 3: Elementary OPAC alternative + 'LookupPatron', + 'AuthenticatePatron', + 'GetPatronInfo', + 'GetPatronStatus', + 'GetServices', # FIXME Loans + 'RenewLoan', + 'HoldTitle', # FIXME Add dates support + 'HoldItem', # FIXME Add dates support + 'CancelHold', + + # 'RecallItem', # Not supported by Koha + # 'CancelRecall', # Not supported by Koha + # Level 4: Robust/domain specific discovery platforms + # 'SearchCourseReserves', # TODO + # 'Explain' # TODO +); + +# List of required arguments +my %required = ( + 'Describe' => ['verb'], + 'GetAvailability' => [ 'id', 'id_type' ], + 'GetRecords' => ['id'], + 'GetAuthorityRecords' => ['id'], + 'LookupPatron' => ['id'], + 'AuthenticatePatron' => [ 'username', 'password' ], + 'GetPatronInfo' => ['patron_id'], + 'GetPatronStatus' => ['patron_id'], + 'GetServices' => [ 'patron_id', 'item_id' ], + 'RenewLoan' => [ 'patron_id', 'item_id' ], + 'HoldTitle' => [ 'patron_id', 'bib_id', 'request_location' ], + 'HoldItem' => [ 'patron_id', 'bib_id', 'item_id' ], + 'CancelHold' => [ 'patron_id', 'item_id' ], +); + +# List of optional arguments +my %optional = ( + 'Describe' => [], + 'GetAvailability' => [ 'return_type', 'return_fmt', 'language' ], + 'GetRecords' => ['schema'], + 'GetAuthorityRecords' => ['schema'], + 'LookupPatron' => ['id_type'], + 'AuthenticatePatron' => [], + 'GetPatronInfo' => + [ 'show_contact', 'show_fines', 'show_holds', 'show_loans', 'loans_per_page', 'loans_page', 'show_attributes' ], + 'GetPatronStatus' => [], + 'GetServices' => [], + 'RenewLoan' => ['desired_due_date'], + # RKE: 'comment' added as additional parameter for 'HoldTitle' and 'HoldItem' + # RKE: 'comment' added, old param names kept for VuFind 5.x compatibility + 'HoldTitle' => [ 'pickup_location', 'start_date', 'expiry_date', 'needed_before_date', 'pickup_expiry_date', 'comment' ], + 'HoldItem' => [ 'pickup_location', 'start_date', 'expiry_date', 'needed_before_date', 'pickup_expiry_date', 'comment' ], + 'CancelHold' => [], +); + +# If no service is requested, display the online documentation +unless ( $cgi->param('service') ) { + my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "ilsdi.tt", + query => $cgi, + type => "opac", + authnotrequired => 1, + } + ); + output_html_with_http_headers $cgi, $cookie, $template->output; + exit 0; +} + +# Set the userenv +C4::Context->set_userenv( + undef, undef, undef, 'ILSDI', 'ILSDI', + undef, undef, undef, undef, undef, +); +C4::Context->interface('opac'); + +# If user requested a service description, then display it +if ( scalar $cgi->param('service') eq "Describe" and any { scalar $cgi->param('verb') eq $_ } @services ) { + my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "ilsdi.tt", + query => $cgi, + type => "opac", + authnotrequired => 1, + } + ); + $template->param( scalar $cgi->param('verb') => 1 ); + output_html_with_http_headers $cgi, $cookie, $template->output; + exit 0; +} + +# any output after this point will be UTF-8 XML +binmode STDOUT, ':encoding(UTF-8)'; +print CGI::header( '-type' => 'text/xml', '-charset' => 'utf-8' ); + +my $out; + +# If ILS-DI module is disabled in System->Preferences, redirect to 404 +unless ( C4::Context->preference('ILS-DI') ) { + $out->{'code'} = "NotAllowed"; + $out->{'message'} = "ILS-DI is disabled."; +} else { + + # If the remote address is not allowed, redirect to 403 + my @AuthorizedIPs = split( /,/, C4::Context->preference('ILS-DI:AuthorizedIPs') ); + if (@AuthorizedIPs) { # If no filter set, allow access to everybody + my $authorized = 0; + foreach my $ip (@AuthorizedIPs) { + my $netmask = Net::Netmask->new2($ip); + if ( $netmask && $netmask->match( $ENV{REMOTE_ADDR} ) ) { + $authorized = 1; + last; + } + } + unless ($authorized) { + $out->{'code'} = "NotAllowed"; + $out->{'message'} = "Unauthorized IP address: $ENV{REMOTE_ADDR}."; + } + } else { + $out->{'code'} = "NotAllowed"; + $out->{'message'} = "Unauthorized IP address: $ENV{REMOTE_ADDR}."; + + print XMLout( + $out, + noattr => 1, + nosort => 1, + xmldecl => '', + RootName => "ilsdi", + SuppressEmpty => 1 + ); + exit 0; + } + +} + +my $service = $cgi->param('service') || "ilsdi"; + +# Check if the requested service is in the list +if ( $service and any { $service eq $_ } @services ) { + + my @parmsrequired = @{ $required{$service} }; + my @parmsoptional = @{ $optional{$service} }; + my @parmsall = ( @parmsrequired, @parmsoptional ); + my @names = $cgi->multi_param; + my %paramhash; + $paramhash{$_} = 1 for @names; + + # check for missing parameters + for (@parmsrequired) { + unless ( exists $paramhash{$_} ) { + $out->{'code'} = "MissingParameter"; + $out->{'message'} = "The required parameter " . $_ . " is missing."; + } + } + + # check for illegal parameters + for my $name (@names) { + my $found = 0; + for my $name2 (@parmsall) { + if ( $name eq $name2 ) { + $found = 1; + } + } + if ( $found == 0 && $name ne 'service' ) { + $out->{'code'} = "IllegalParameter"; + $out->{'message'} = "The parameter " . $name . " is illegal."; + } + } + + # check for multiple parameters + for (@names) { + my @values = $cgi->multi_param($_); + if ( $#values != 0 ) { + $out->{'code'} = "MultipleValuesNotAllowed"; + $out->{'message'} = "Multiple values not allowed for the parameter " . $_ . "."; + } + } + + if ( !$out->{'message'} ) { + + # GetAvailability is a special case, as it cannot use XML::Simple + if ( $service eq "GetAvailability" ) { + print C4::ILSDI::Services::GetAvailability($cgi); + exit 0; + } else { + + # Variable functions + my $sub = do { + + # no strict 'refs'; + my $symbol = 'C4::ILSDI::Services::' . $service; + \&{"$symbol"}; + }; + + # Call the requested service, and get its return value + $out = &$sub($cgi); + } + } +} else { + $out->{'message'} = "NotSupported"; +} + +# Output XML by passing the hashref to XMLOut +print XMLout( + $out, + noattr => 1, + nosort => 1, + xmldecl => '', + RootName => $service, + SuppressEmpty => 1 +); +exit 0; + diff --git a/files/opac/cgi-bin/opac/ilsdi.pl.orig b/files/opac/cgi-bin/opac/ilsdi.pl.orig new file mode 100755 index 0000000..90060d7 --- /dev/null +++ b/files/opac/cgi-bin/opac/ilsdi.pl.orig @@ -0,0 +1,275 @@ +#!/usr/bin/perl + +# Copyright 2009 SARL Biblibre +# +# 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 C4::ILSDI::Services; +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); +use C4::Context; + +use List::MoreUtils qw( any ); +use XML::Simple qw( XMLout ); +use CGI qw ( -utf8 ); +use Net::Netmask; + +=head1 DLF ILS-DI for Koha + +This script is a basic implementation of ILS-DI protocol for Koha. +It acts like a dispatcher, that get the CGI request, check required and +optionals arguments, call a function from C4::ILS-DI, and finally +outputs the returned hashref as XML. + +=cut + +# Instantiate the CGI request +my $cgi = CGI->new; + +# List of available services, sorted by level +my @services = ( + 'Describe', # Not part of ILS-DI, online API doc + + # Level 1: Basic Discovery Interfaces + # 'HarvestBibliographicRecords', # OAI-PMH + # 'HarvestExpandedRecords', # OAI-PMH + 'GetAvailability', # FIXME Add bibliographic level + + # 'GoToBibliographicRequestPage' # I don't understant this one + # Level 2: Elementary OPAC supplement + # 'HarvestAuthorityRecords', # OAI-PMH + # 'HarvestHoldingsRecords', # OAI-PMH + 'GetRecords', # Note that we can use OAI-PMH for this too + + # 'Search', # TODO + # 'Scan', # TODO + 'GetAuthorityRecords', + + # 'OutputRewritablePage', # I don't understant this one + # 'OutputIntermediateFormat', # I don't understant this one + # Level 3: Elementary OPAC alternative + 'LookupPatron', + 'AuthenticatePatron', + 'GetPatronInfo', + 'GetPatronStatus', + 'GetServices', # FIXME Loans + 'RenewLoan', + 'HoldTitle', # FIXME Add dates support + 'HoldItem', # FIXME Add dates support + 'CancelHold', + + # 'RecallItem', # Not supported by Koha + # 'CancelRecall', # Not supported by Koha + # Level 4: Robust/domain specific discovery platforms + # 'SearchCourseReserves', # TODO + # 'Explain' # TODO +); + +# List of required arguments +my %required = ( + 'Describe' => ['verb'], + 'GetAvailability' => [ 'id', 'id_type' ], + 'GetRecords' => ['id'], + 'GetAuthorityRecords' => ['id'], + 'LookupPatron' => ['id'], + 'AuthenticatePatron' => [ 'username', 'password' ], + 'GetPatronInfo' => ['patron_id'], + 'GetPatronStatus' => ['patron_id'], + 'GetServices' => [ 'patron_id', 'item_id' ], + 'RenewLoan' => [ 'patron_id', 'item_id' ], + 'HoldTitle' => [ 'patron_id', 'bib_id', 'request_location' ], + 'HoldItem' => [ 'patron_id', 'bib_id', 'item_id' ], + 'CancelHold' => [ 'patron_id', 'item_id' ], +); + +# List of optional arguments +my %optional = ( + 'Describe' => [], + 'GetAvailability' => [ 'return_type', 'return_fmt', 'language' ], + 'GetRecords' => ['schema'], + 'GetAuthorityRecords' => ['schema'], + 'LookupPatron' => ['id_type'], + 'AuthenticatePatron' => [], + 'GetPatronInfo' => + [ 'show_contact', 'show_fines', 'show_holds', 'show_loans', 'loans_per_page', 'loans_page', 'show_attributes' ], + 'GetPatronStatus' => [], + 'GetServices' => [], + 'RenewLoan' => ['desired_due_date'], + 'HoldTitle' => [ 'pickup_location', 'start_date', 'expiry_date' ], + 'HoldItem' => [ 'pickup_location', 'start_date', 'expiry_date' ], + 'CancelHold' => [], +); + +# If no service is requested, display the online documentation +unless ( $cgi->param('service') ) { + my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "ilsdi.tt", + query => $cgi, + type => "opac", + authnotrequired => 1, + } + ); + output_html_with_http_headers $cgi, $cookie, $template->output; + exit 0; +} + +# Set the userenv +C4::Context->set_userenv( + undef, undef, undef, 'ILSDI', 'ILSDI', + undef, undef, undef, undef, undef, +); +C4::Context->interface('opac'); + +# If user requested a service description, then display it +if ( scalar $cgi->param('service') eq "Describe" and any { scalar $cgi->param('verb') eq $_ } @services ) { + my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "ilsdi.tt", + query => $cgi, + type => "opac", + authnotrequired => 1, + } + ); + $template->param( scalar $cgi->param('verb') => 1 ); + output_html_with_http_headers $cgi, $cookie, $template->output; + exit 0; +} + +# any output after this point will be UTF-8 XML +binmode STDOUT, ':encoding(UTF-8)'; +print CGI::header( '-type' => 'text/xml', '-charset' => 'utf-8' ); + +my $out; + +# If ILS-DI module is disabled in System->Preferences, redirect to 404 +unless ( C4::Context->preference('ILS-DI') ) { + $out->{'code'} = "NotAllowed"; + $out->{'message'} = "ILS-DI is disabled."; +} else { + + # If the remote address is not allowed, redirect to 403 + my @AuthorizedIPs = split( /,/, C4::Context->preference('ILS-DI:AuthorizedIPs') ); + if (@AuthorizedIPs) { # If no filter set, allow access to everybody + my $authorized = 0; + foreach my $ip (@AuthorizedIPs) { + my $netmask = Net::Netmask->new2($ip); + if ( $netmask && $netmask->match( $ENV{REMOTE_ADDR} ) ) { + $authorized = 1; + last; + } + } + unless ($authorized) { + $out->{'code'} = "NotAllowed"; + $out->{'message'} = "Unauthorized IP address: $ENV{REMOTE_ADDR}."; + } + } else { + $out->{'code'} = "NotAllowed"; + $out->{'message'} = "Unauthorized IP address: $ENV{REMOTE_ADDR}."; + + print XMLout( + $out, + noattr => 1, + nosort => 1, + xmldecl => '', + RootName => "ilsdi", + SuppressEmpty => 1 + ); + exit 0; + } + +} + +my $service = $cgi->param('service') || "ilsdi"; + +# Check if the requested service is in the list +if ( $service and any { $service eq $_ } @services ) { + + my @parmsrequired = @{ $required{$service} }; + my @parmsoptional = @{ $optional{$service} }; + my @parmsall = ( @parmsrequired, @parmsoptional ); + my @names = $cgi->multi_param; + my %paramhash; + $paramhash{$_} = 1 for @names; + + # check for missing parameters + for (@parmsrequired) { + unless ( exists $paramhash{$_} ) { + $out->{'code'} = "MissingParameter"; + $out->{'message'} = "The required parameter " . $_ . " is missing."; + } + } + + # check for illegal parameters + for my $name (@names) { + my $found = 0; + for my $name2 (@parmsall) { + if ( $name eq $name2 ) { + $found = 1; + } + } + if ( $found == 0 && $name ne 'service' ) { + $out->{'code'} = "IllegalParameter"; + $out->{'message'} = "The parameter " . $name . " is illegal."; + } + } + + # check for multiple parameters + for (@names) { + my @values = $cgi->multi_param($_); + if ( $#values != 0 ) { + $out->{'code'} = "MultipleValuesNotAllowed"; + $out->{'message'} = "Multiple values not allowed for the parameter " . $_ . "."; + } + } + + if ( !$out->{'message'} ) { + + # GetAvailability is a special case, as it cannot use XML::Simple + if ( $service eq "GetAvailability" ) { + print C4::ILSDI::Services::GetAvailability($cgi); + exit 0; + } else { + + # Variable functions + my $sub = do { + + # no strict 'refs'; + my $symbol = 'C4::ILSDI::Services::' . $service; + \&{"$symbol"}; + }; + + # Call the requested service, and get its return value + $out = &$sub($cgi); + } + } +} else { + $out->{'message'} = "NotSupported"; +} + +# Output XML by passing the hashref to XMLOut +print XMLout( + $out, + noattr => 1, + nosort => 1, + xmldecl => '', + RootName => $service, + SuppressEmpty => 1 +); +exit 0; + diff --git a/patches/koha-25.11-ilsdi-comment.patch b/patches/koha-25.11-ilsdi-comment.patch new file mode 100644 index 0000000..7bcd66c --- /dev/null +++ b/patches/koha-25.11-ilsdi-comment.patch @@ -0,0 +1,15 @@ +--- /usr/share/koha/opac/cgi-bin/opac/ilsdi.pl.bak.20260303 2026-03-03 12:45:20.698269416 +0100 ++++ /usr/share/koha/opac/cgi-bin/opac/ilsdi.pl 2026-03-04 07:33:55.010753628 +0100 +@@ -110,8 +110,10 @@ + 'GetPatronStatus' => [], + 'GetServices' => [], + 'RenewLoan' => ['desired_due_date'], +- 'HoldTitle' => [ 'pickup_location', 'start_date', 'expiry_date' ], +- 'HoldItem' => [ 'pickup_location', 'start_date', 'expiry_date' ], ++ # RKE: 'comment' added as additional parameter for 'HoldTitle' and 'HoldItem' ++ # RKE: 'comment' added, old param names kept for VuFind 5.x compatibility ++ 'HoldTitle' => [ 'pickup_location', 'start_date', 'expiry_date', 'needed_before_date', 'pickup_expiry_date', 'comment' ], ++ 'HoldItem' => [ 'pickup_location', 'start_date', 'expiry_date', 'needed_before_date', 'pickup_expiry_date', 'comment' ], + 'CancelHold' => [], + ); + diff --git a/patches/koha-25.11-services-comment.patch b/patches/koha-25.11-services-comment.patch new file mode 100644 index 0000000..974ca4e --- /dev/null +++ b/patches/koha-25.11-services-comment.patch @@ -0,0 +1,44 @@ +--- /usr/share/koha/lib/C4/ILSDI/Services.pm.bak.20260303 2026-03-03 12:43:18.320523619 +0100 ++++ /usr/share/koha/lib/C4/ILSDI/Services.pm 2026-03-04 07:39:10.327445880 +0100 +@@ -813,8 +813,11 @@ + return { code => 'libraryNotPickupLocation' } unless $destination->pickup_location; + return { code => 'cannotBeTransferred' } unless $biblio->can_be_transferred( { to => $destination } ); + +- my $resdate = $cgi->param('start_date'); +- my $expdate = $cgi->param('expiry_date'); ++ my $resdate = $cgi->param('start_date') || $cgi->param('needed_before_date'); ++ my $expdate = $cgi->param('expiry_date') || $cgi->param('pickup_expiry_date'); ++ #RKE comment for reserves ++ my $comment = $cgi->param('comment'); ++ #RKE End + + # Add the reserve + # $branch, $borrowernumber, $biblionumber, +@@ -830,6 +833,7 @@ + reservation_date => $resdate, + expiration_date => $expdate, + title => $title, ++ notes => $comment, + } + ); + +@@ -911,7 +915,10 @@ + + my $resdate = $cgi->param('start_date'); + my $expdate = $cgi->param('expiry_date'); +- ++ # RKE comment fpr reserves ++ my $comment = $cgi->param('comment'); ++ # RKE End ++ + # Add the reserve + my $priority = C4::Reserves::CalculatePriority($biblionumber); + AddReserve( +@@ -924,6 +931,7 @@ + expiration_date => $expdate, + title => $title, + itemnumber => $itemnumber, ++ notes => $comment, + } + ); +