Bug 38630: Make the REST API respect KohaOpacLanguage cookie

The REST API can return translated strings to the opac or staff
interface, so it should respect the KohaOpacLanguage cookie and return
translations in the same language than the interface.

Test plan:
1. Do not apply patch yet
2. Log in to koha staff interface
3. Use the browser's devtools to get the value of CGISESSID cookie
4. Make sure you have at least 2 languages installed ('en' + another
   one - I'll use 'fr-FR' as an example in this test plan)
5. Find an item and note its itemnumber and itemtype
6. Go translate this itemtype's description (admin » item types » edit
   item type » translate). Add a translation for 'en' and another one
   for your language.
7. Close your browser tab (do not logout) and restart Koha to flush the
   memory cache.
8. Execute the following commands (replace variables by the correct
   value):

    curl -sSL "$KOHA_URL/api/v1/items/$ITEMNUMBER" \
        -H 'x-koha-embed: +strings' \
        -H "Cookie: KohaOpacLanguage=en; CGISESSID=$CGISESSID" \
        | jq -r '._strings.item_type_id.str'

   If you do not have jq, it can be found in most distributions
   packages. Or you could inspect the JSON manually at the path
   /_strings/item_type_id/str

   Restart Koha again, and execute the same command but with your
   language set in KohaOpacLanguage cookie:

    curl -sSL "$KOHA_URL/api/v1/items/$ITEMNUMBER" \
        -H 'x-koha-embed: +strings' \
        -H "Cookie: KohaOpacLanguage=fr-FR; CGISESSID=$CGISESSID" \
        | jq -r '._strings.item_type_id.str'

   Depending on how Koha sort the languages (not sure how it's done),
   you should get either the 'en' one or the 'fr-FR' one, but the
   important thing is you should get the same translation, no matter
   what the value of KohaOpacLanguage is, which is the issue this patch
   is trying to fix.
9. Apply the patch and restart Koha
10. Repeat step 8. This time you should get the translation
    corresponding to the language set in KohaOpacLanguage cookie

Side note:
* The need to restart Koha before each request is due to a bug (see bug
  38454). If already pushed when you're testing this one, you don't have
  to restart.

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
This commit is contained in:
Julian Maurice 2024-12-05 11:24:33 +01:00 committed by Katrin Fischer
parent 427e4d582a
commit ca5db9ffb7
No known key found for this signature in database
GPG Key ID: 0EF6E2C03357A834
9 changed files with 216 additions and 32 deletions

View File

@ -28,6 +28,7 @@ use List::MoreUtils qw( any );
use C4::Context;
use Koha::Caches;
use Koha::Cache::Memory::Lite;
use Koha::Language;
our (@ISA, @EXPORT_OK);
BEGIN {
@ -111,12 +112,17 @@ Returns a reference to an array of hashes:
=cut
sub getTranslatedLanguages {
my ($interface, $theme, $current_language, $which) = @_;
my ($interface, $theme, $current_language) = @_;
my @languages;
my @enabled_languages =
( $interface && $interface eq 'intranet' )
? split ",", C4::Context->preference('StaffInterfaceLanguages')
: split ",", C4::Context->preference('OPACLanguages');
my @enabled_languages;
if ($interface) {
if ($interface eq 'intranet') {
@enabled_languages = split ",", C4::Context->preference('StaffInterfaceLanguages');
} elsif ($interface eq 'opac') {
@enabled_languages = split ",", C4::Context->preference('OPACLanguages');
}
}
my $cache = Koha::Caches->get_instance;
my $cache_key = "languages_${interface}_${theme}";
@ -640,8 +646,26 @@ sub accept_language {
=head2 getlanguage
Select a language based on the URL parameter 'language', a cookie,
syspref available languages & browser
Select a language based on:
=over
=item * the URL parameter 'language' (staff and opac interfaces only),
=item * the 'KohaOpacLanguage' cookie (staff and opac interfaces only),
=item * the language set by C<Koha::Language::set_requested_language>
(interfaces other than staff and opac only). The REST API sets this to the
value of 'KohaOpacLanguage' cookie,
=item * syspref StaffInterfaceLanguages (staff only)
=item * syspref OpacLanguages (opac only),
=item * HTTP header Accept-Language (more precisely, environment variable
HTTP_ACCEPT_LANGUAGE)
=back
=cut
@ -655,29 +679,36 @@ sub getlanguage {
return $cached if $cached;
}
$cgi //= CGI->new;
my $interface = C4::Context->interface;
my $theme = C4::Context->preference( ( $interface eq 'opac' ) ? 'opacthemes' : 'template' );
my $theme = '';
my $language;
my $preference_to_check =
$interface eq 'intranet' ? 'StaffInterfaceLanguages' : 'OPACLanguages';
# Get the available/valid languages list
my @languages;
my $preference_value = C4::Context->preference($preference_to_check);
if ($preference_value) {
@languages = split /,/, $preference_value;
}
# Chose language from the URL
my $cgi_param_language = $cgi->param( 'language' );
if ( defined $cgi_param_language && any { $_ eq $cgi_param_language } @languages) {
$language = $cgi_param_language;
}
if ($interface eq 'opac' || $interface eq 'intranet') {
$cgi //= CGI->new;
$theme = C4::Context->preference( ( $interface eq 'opac' ) ? 'opacthemes' : 'template' );
# cookie
if (not $language and my $cgi_cookie_language = $cgi->cookie('KohaOpacLanguage') ) {
( $language = $cgi_cookie_language ) =~ s/[^a-zA-Z_-]*//; # sanitize cookie
my $preference_to_check =
$interface eq 'intranet' ? 'StaffInterfaceLanguages' : 'OPACLanguages';
# Get the available/valid languages list
my $preference_value = C4::Context->preference($preference_to_check);
if ($preference_value) {
@languages = split /,/, $preference_value;
}
# Chose language from the URL
my $cgi_param_language = $cgi->param( 'language' );
if ( defined $cgi_param_language && any { $_ eq $cgi_param_language } @languages) {
$language = $cgi_param_language;
}
# cookie
if (not $language and my $cgi_cookie_language = $cgi->cookie('KohaOpacLanguage') ) {
( $language = $cgi_cookie_language ) =~ s/[^a-zA-Z_-]*//; # sanitize cookie
}
} else {
@languages = map { $_->{rfc4646_subtag} } @{ getTranslatedLanguages($interface, $theme) };
$language = Koha::Language->get_requested_language();
}
# HTTP_ACCEPT_LANGUAGE

View File

@ -39,6 +39,7 @@ sub startup {
$self->plugin('RESTV1');
$self->plugin('CSRF');
$self->plugin('Language');
$self->hook(before_dispatch => \&_before_dispatch);
$self->hook(around_action => \&_around_action);

View File

@ -39,6 +39,7 @@ sub startup {
$self->plugin('RESTV1');
$self->plugin('CSRF');
$self->plugin('Language');
$self->hook(before_dispatch => \&_before_dispatch);
$self->hook(around_action => \&_around_action);

View File

@ -0,0 +1,72 @@
package Koha::App::Plugin::Language;
# 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 <http://www.gnu.org/licenses>.
=head1 NAME
Koha::App::Plugin::Language
=head1 SYNOPSIS
$app->plugin('Language');
=head1 DESCRIPTION
Sets the current language based on request (cookies, headers, ...)
=cut
use Modern::Perl;
use Mojo::Base 'Mojolicious::Plugin';
use Koha::Language;
=head1 METHODS
=head2 register
Called by Mojolicious when the plugin is loaded.
Defines an `around_action` hook that will sets the current language based on
the current request.
=cut
sub register {
my ( $self, $app, $conf ) = @_;
$app->hook(
around_action => sub {
my ( $next, $c, $action, $last ) = @_;
# Make the value of Accept-Language header and KohaOpacLanguage
# cookie accessible to C4::Languages::getlanguage when not in a CGI
# context
if ( my $accept_language = $c->req->headers->accept_language ) {
$ENV{HTTP_ACCEPT_LANGUAGE} = $accept_language;
}
if ( my $KohaOpacLanguage = $c->cookie('KohaOpacLanguage') ) {
Koha::Language->set_requested_language($KohaOpacLanguage);
}
return $next->();
}
);
}
1;

46
Koha/Language.pm Normal file
View File

@ -0,0 +1,46 @@
package Koha::Language;
=head1 NAME
Koha::Language
=head1 SYNOPSIS
use Koha::Language;
Koha::Language->set_requested_language('xx-XX');
my $language = Koha::Language->get_requested_language();
=head1 DESCRIPTION
This module is essentially a communication tool between the REST API and
C4::Languages::getlanguage so that getlanguage can be aware of the value of
KohaOpacLanguage cookie when not in CGI context.
It can also be used in other contexts, like command line scripts for instance.
=cut
use Modern::Perl;
use Koha::Cache::Memory::Lite;
use constant REQUESTED_LANGUAGE_CACHE_KEY => 'requested_language';
sub set_requested_language {
my ($class, $language) = @_;
my $cache = Koha::Cache::Memory::Lite->get_instance;
$cache->set_in_cache(REQUESTED_LANGUAGE_CACHE_KEY, $language);
}
sub get_requested_language {
my ($class) = @_;
my $cache = Koha::Cache::Memory::Lite->get_instance;
return $cache->get_from_cache(REQUESTED_LANGUAGE_CACHE_KEY);
}
1;

View File

@ -153,6 +153,7 @@ sub startup {
$self->app->log->warn( "Warning: Failed to fetch oauth configuration: " . $_ );
};
$self->plugin('Koha::App::Plugin::Language');
$self->plugin('Koha::REST::Plugin::Pagination');
$self->plugin('Koha::REST::Plugin::Query');
$self->plugin('Koha::REST::Plugin::Objects');

View File

@ -155,11 +155,6 @@ sub authenticate_api_request {
$c->stash_embed( { spec => $spec } );
$c->stash_overrides();
# FIXME: Remove once CGI is not used
my $accept_language = $c->req->headers->accept_language;
$ENV{HTTP_ACCEPT_LANGUAGE} = $accept_language
if $accept_language;
my $cookie_auth = 0;
my $authorization = $spec->{'x-koha-authorization'};

View File

@ -18,10 +18,11 @@
# with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Test::More tests => 4;
use Test::More tests => 5;
use Test::MockModule;
use CGI qw ( -utf8 );
use Koha::Cache::Memory::Lite;
use Koha::Language;
BEGIN {
use_ok('C4::Languages', qw( getlanguage ));
@ -32,6 +33,7 @@ my @languages = (); # stores the list of active languages
my $return_undef = 0;
my $module_context = Test::MockModule->new('C4::Context');
my $module_languages = Test::MockModule->new('C4::Languages');
$module_context->mock(
preference => sub {
@ -46,6 +48,12 @@ $module_context->mock(
},
);
$module_languages->mock(
_get_language_dirs => sub {
return @languages
}
);
delete $ENV{HTTP_ACCEPT_LANGUAGE};
my $query = CGI->new();
@ -59,3 +67,24 @@ is(C4::Languages::getlanguage($query), 'en', 'default to English if no language
Koha::Cache::Memory::Lite->get_instance()->clear_from_cache('getlanguage');
$return_undef = 1;
is(C4::Languages::getlanguage($query), 'en', 'default to English if no database');
subtest 'when interface is not intranet or opac' => sub {
plan tests => 3;
my $cache = Koha::Cache::Memory::Lite->get_instance;
C4::Context->interface('api');
@languages = ('fr-FR', 'de-DE', 'en');
$ENV{HTTP_ACCEPT_LANGUAGE} = 'de-DE';
$cache->clear_from_cache('getlanguage');
is(C4::Languages::getlanguage(), 'de-DE', 'Accept-Language HTTP header is respected');
Koha::Language->set_requested_language('fr-FR');
$cache->clear_from_cache('getlanguage');
is(C4::Languages::getlanguage(), 'fr-FR', 'but language requested through Koha::Language is prefered');
@languages = ();
$cache->clear_from_cache('getlanguage');
is(C4::Languages::getlanguage(), 'en', 'fallback to english when no language is available');
};

View File

@ -32,6 +32,7 @@ use Mojo::JSON qw(encode_json);
use C4::Auth;
use Koha::Items;
use Koha::Database;
use Koha::Language;
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new;
@ -256,7 +257,7 @@ subtest 'list_public() tests' => sub {
subtest 'get() tests' => sub {
plan tests => 34;
plan tests => 37;
$schema->storage->txn_begin;
@ -346,6 +347,13 @@ subtest 'get() tests' => sub {
->json_is( '/not_for_loan_status' => 0, 'not_for_loan_status is 0' )
->json_is( '/effective_not_for_loan_status' => 3, 'effective_not_for_loan_status now picks up itemtype level - item-level_itypes:0' );
my $module = Test::MockModule->new('C4::Languages');
$module->mock( _get_language_dirs => sub { return 'en', 'de-DE' } );
$schema->resultset('Localization')->create({ entity => 'itemtypes', code => $itype->itemtype, lang => 'de-DE', translation => 'translated' });
$t->get_ok( "//$userid:$password@/api/v1/items/" . $item->itemnumber, { 'X-Koha-Embed' => '+strings', Cookie => 'KohaOpacLanguage=de-DE', 'Accept-Language' => 'en' } )
->status_is( 200, 'SWAGGER3.2.2' )
->json_is( '/_strings/item_type_id/str' => 'translated', 'translations in _strings use language set in KohaOpacLanguage cookie' );
$schema->storage->txn_rollback;
};