From 0fb1ec0d9b17756f8379695f848155c1b0bf5a04 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Thu, 5 Dec 2024 11:24:33 +0100 Subject: [PATCH] Bug 38630: Make the REST API respect KohaOpacLanguage cookie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- C4/Languages.pm | 81 ++++++++++++++++++++++++----------- Koha/App/Intranet.pm | 1 + Koha/App/Opac.pm | 1 + Koha/App/Plugin/Language.pm | 72 +++++++++++++++++++++++++++++++ Koha/Language.pm | 46 ++++++++++++++++++++ Koha/REST/V1.pm | 1 + Koha/REST/V1/Auth.pm | 5 --- t/Languages.t | 31 +++++++++++++- t/db_dependent/api/v1/items.t | 10 ++++- 9 files changed, 216 insertions(+), 32 deletions(-) create mode 100644 Koha/App/Plugin/Language.pm create mode 100644 Koha/Language.pm diff --git a/C4/Languages.pm b/C4/Languages.pm index a9f8295cf1..d482e5ed7c 100644 --- a/C4/Languages.pm +++ b/C4/Languages.pm @@ -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}"; @@ -618,8 +624,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 +(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 @@ -633,29 +657,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' ); + + 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 + # 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 diff --git a/Koha/App/Intranet.pm b/Koha/App/Intranet.pm index 558de8da1c..5628174fdc 100644 --- a/Koha/App/Intranet.pm +++ b/Koha/App/Intranet.pm @@ -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); diff --git a/Koha/App/Opac.pm b/Koha/App/Opac.pm index 1f722360e4..d41a704776 100644 --- a/Koha/App/Opac.pm +++ b/Koha/App/Opac.pm @@ -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); diff --git a/Koha/App/Plugin/Language.pm b/Koha/App/Plugin/Language.pm new file mode 100644 index 0000000000..c93e4fa95f --- /dev/null +++ b/Koha/App/Plugin/Language.pm @@ -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 . + +=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; diff --git a/Koha/Language.pm b/Koha/Language.pm new file mode 100644 index 0000000000..1dcb58237e --- /dev/null +++ b/Koha/Language.pm @@ -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; diff --git a/Koha/REST/V1.pm b/Koha/REST/V1.pm index 52bd15697e..1a3559bfdf 100644 --- a/Koha/REST/V1.pm +++ b/Koha/REST/V1.pm @@ -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'); diff --git a/Koha/REST/V1/Auth.pm b/Koha/REST/V1/Auth.pm index c10e81cf8a..dd59def5f1 100644 --- a/Koha/REST/V1/Auth.pm +++ b/Koha/REST/V1/Auth.pm @@ -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'}; diff --git a/t/Languages.t b/t/Languages.t index 14c5d2866d..4b13d02cc2 100755 --- a/t/Languages.t +++ b/t/Languages.t @@ -18,10 +18,11 @@ # with Koha; if not, see . 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'); +}; diff --git a/t/db_dependent/api/v1/items.t b/t/db_dependent/api/v1/items.t index 63ca5f73db..bed68e490b 100755 --- a/t/db_dependent/api/v1/items.t +++ b/t/db_dependent/api/v1/items.t @@ -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; }; -- 2.39.5