From 0c5e30002573cc205a29825b24c1a63244a48d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 23 May 2023 13:22:12 +0200 Subject: [PATCH 1/5] Fixed the language value passed to `in_array()` Also fixed the language tags in the sample `skosmos:languages` config --- controller/WebController.php | 2 +- dockerfiles/config/config-docker-compose.ttl | 4 ++-- dockerfiles/config/config-docker.ttl | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/controller/WebController.php b/controller/WebController.php index 5afe6dd3d..13cf8dafe 100644 --- a/controller/WebController.php +++ b/controller/WebController.php @@ -103,7 +103,7 @@ public function guessLanguage($vocid = null) // using a random language from the configured UI languages when there is no accept language header set $acceptLanguage = filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ? filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE', FILTER_SANITIZE_FULL_SPECIAL_CHARS) : $langcodes[0]; $bestLang = $this->negotiator->getBest($acceptLanguage, $langcodes); - if (isset($bestLang) && in_array($bestLang, $langcodes)) { + if (isset($bestLang) && in_array($bestLang->getValue(), $langcodes)) { return $bestLang->getValue(); } diff --git a/dockerfiles/config/config-docker-compose.ttl b/dockerfiles/config/config-docker-compose.ttl index 954399926..893d5ad1c 100644 --- a/dockerfiles/config/config-docker-compose.ttl +++ b/dockerfiles/config/config-docker-compose.ttl @@ -37,7 +37,7 @@ [ rdfs:label "da" ; rdf:value "da_DK.utf8" ] [ rdfs:label "de" ; rdf:value "de_DE.utf8" ] [ rdfs:label "en" ; rdf:value "en_GB.utf8" ] - [ rdfs:label "en_US" ; rdf:value "en_US.utf8" ] + [ rdfs:label "en-US" ; rdf:value "en_US.utf8" ] [ rdfs:label "es" ; rdf:value "es_ES.utf8" ] [ rdfs:label "fa" ; rdf:value "fa_IR.utf8" ] [ rdfs:label "fi" ; rdf:value "fi_FI.utf8" ] @@ -48,7 +48,7 @@ [ rdfs:label "nn" ; rdf:value "nn_NO.utf8" ] [ rdfs:label "pl" ; rdf:value "pl_PL.utf8" ] [ rdfs:label "pt" ; rdf:value "pt_PT.utf8" ] - [ rdfs:label "pt_BR" ; rdf:value "pt_BR.utf8" ] + [ rdfs:label "pt-BR" ; rdf:value "pt_BR.utf8" ] [ rdfs:label "ru" ; rdf:value "ru_RU.utf8" ] [ rdfs:label "sv" ; rdf:value "sv_SE.utf8" ] [ rdfs:label "zh" ; rdf:value "zh_CN.utf8" ] diff --git a/dockerfiles/config/config-docker.ttl b/dockerfiles/config/config-docker.ttl index b779328a6..bbd107594 100644 --- a/dockerfiles/config/config-docker.ttl +++ b/dockerfiles/config/config-docker.ttl @@ -37,7 +37,7 @@ [ rdfs:label "da" ; rdf:value "da_DK.utf8" ] [ rdfs:label "de" ; rdf:value "de_DE.utf8" ] [ rdfs:label "en" ; rdf:value "en_GB.utf8" ] - [ rdfs:label "en_US" ; rdf:value "en_US.utf8" ] + [ rdfs:label "en-US" ; rdf:value "en_US.utf8" ] [ rdfs:label "es" ; rdf:value "es_ES.utf8" ] [ rdfs:label "fa" ; rdf:value "fa_IR.utf8" ] [ rdfs:label "fi" ; rdf:value "fi_FI.utf8" ] @@ -48,7 +48,7 @@ [ rdfs:label "nn" ; rdf:value "nn_NO.utf8" ] [ rdfs:label "pl" ; rdf:value "pl_PL.utf8" ] [ rdfs:label "pt" ; rdf:value "pt_PT.utf8" ] - [ rdfs:label "pt_BR" ; rdf:value "pt_BR.utf8" ] + [ rdfs:label "pt-BR" ; rdf:value "pt_BR.utf8" ] [ rdfs:label "ru" ; rdf:value "ru_RU.utf8" ] [ rdfs:label "sv" ; rdf:value "sv_SE.utf8" ] [ rdfs:label "zh" ; rdf:value "zh_CN.utf8" ] From 003ec1fee2a4c864f10ad7214d5b4f1481e899fa Mon Sep 17 00:00:00 2001 From: Osma Suominen Date: Wed, 24 May 2023 16:26:26 +0300 Subject: [PATCH 2/5] upgrade willdurand/negotiation to 3.1.* which supports PHP 8.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 76b958cba..f114ec451 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "twig/extensions": "1.5.*", "twbs/bootstrap": "5.1.*", "twitter/typeahead.js": "v0.11.*", - "willdurand/negotiation": "3.0.*", + "willdurand/negotiation": "3.1.*", "vakata/jstree": "3.3.*", "punic/punic": "3.5.1", "ml/json-ld": "1.*", From ccfe4754779eabcd3844a25f5016700f6ec0b35b Mon Sep 17 00:00:00 2001 From: Osma Suominen Date: Wed, 24 May 2023 16:26:54 +0300 Subject: [PATCH 3/5] allow accessing and mocking cookie values via Request (in addition to GET, POST, SERVER vars) --- model/Request.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/model/Request.php b/model/Request.php index a1bf02c85..b695b2d4e 100644 --- a/model/Request.php +++ b/model/Request.php @@ -17,6 +17,7 @@ class Request private $queryParams; private $queryParamsPOST; private $serverConstants; + private $cookies; /** * Initializes the Request Object @@ -45,6 +46,13 @@ public function __construct($model) foreach (filter_input_array(INPUT_SERVER) ?: [] as $key => $val) { $this->serverConstants[$key] = $val; } + + // Store cookies in a local array, so we can mock them in tests. + // We do not apply any filters at this point. + $this->cookies = []; + foreach (filter_input_array(INPUT_COOKIE) ?: [] as $key => $val) { + $this->cookies[$key] = $val; + } } /** @@ -67,6 +75,16 @@ public function setServerConstant($paramName, $value) $this->serverConstants[$paramName] = $value; } + /** + * Set a cookie to mock it in tests. + * @param string $paramName parameter name + * @param string $value parameter value + */ + public function setCookie($paramName, $value) + { + $this->cookies[$paramName] = $value; + } + /** * Return the requested GET query parameter as a string. Backslashes are stripped for security reasons. * @param string $paramName parameter name @@ -110,6 +128,12 @@ public function getServerConstant($paramName) return filter_var($this->serverConstants[$paramName], FILTER_SANITIZE_FULL_SPECIAL_CHARS); } + public function getCookie($paramName) + { + if (!isset($this->cookies[$paramName])) return null; + return filter_var($this->cookies[$paramName], FILTER_SANITIZE_FULL_SPECIAL_CHARS); + } + public function getLang() { return $this->lang; From 2230cf10d28f2bd937ec3c9a9dea29d916f1e3fa Mon Sep 17 00:00:00 2001 From: Osma Suominen Date: Wed, 24 May 2023 16:28:08 +0300 Subject: [PATCH 4/5] implement unit tests for WebController.guessLanguage --- controller/WebController.php | 14 +++++++++----- index.php | 8 ++++---- tests/GlobalConfigTest.php | 2 +- tests/WebControllerTest.php | 27 +++++++++++++++++++++++++++ tests/testconfig.ttl | 4 +++- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/controller/WebController.php b/controller/WebController.php index 5afe6dd3d..932a16470 100644 --- a/controller/WebController.php +++ b/controller/WebController.php @@ -76,14 +76,16 @@ public function __construct($model) /** * Guess the language of the user. Return a language string that is one * of the supported languages defined in the $LANGUAGES setting, e.g. "fi". + * @param Request $request HTTP request * @param string $vocid identifier for the vocabulary eg. 'yso'. * @return string returns the language choice as a numeric string value */ - public function guessLanguage($vocid = null) + public function guessLanguage($request, $vocid = null) { // 1. select language based on SKOSMOS_LANGUAGE cookie - if (filter_input(INPUT_COOKIE, 'SKOSMOS_LANGUAGE', FILTER_SANITIZE_FULL_SPECIAL_CHARS)) { - return filter_input(INPUT_COOKIE, 'SKOSMOS_LANGUAGE', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $languageCookie = $request->getCookie('SKOSMOS_LANGUAGE'); + if ($languageCookie) { + return $languageCookie; } // 2. if vocabulary given, select based on the default language of the vocabulary @@ -101,9 +103,11 @@ public function guessLanguage($vocid = null) $this->negotiator = new \Negotiation\LanguageNegotiator(); $langcodes = array_keys($this->languages); // using a random language from the configured UI languages when there is no accept language header set - $acceptLanguage = filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ? filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE', FILTER_SANITIZE_FULL_SPECIAL_CHARS) : $langcodes[0]; + $acceptLanguage = $request->getServerConstant('HTTP_ACCEPT_LANGUAGE') ? $request->getServerConstant('HTTP_ACCEPT_LANGUAGE') : $langcodes[0]; + $bestLang = $this->negotiator->getBest($acceptLanguage, $langcodes); - if (isset($bestLang) && in_array($bestLang, $langcodes)) { + + if (isset($bestLang) && in_array($bestLang->getValue(), $langcodes)) { return $bestLang->getValue(); } diff --git a/index.php b/index.php index 2425b72d7..c963c0130 100644 --- a/index.php +++ b/index.php @@ -26,7 +26,7 @@ if (sizeof($parts) <= 2) { // if language code missing, redirect to guessed language // in any case, redirect to / - $lang = sizeof($parts) == 2 && $parts[1] !== '' ? $parts[1] : $controller->guessLanguage(); + $lang = sizeof($parts) == 2 && $parts[1] !== '' ? $parts[1] : $controller->guessLanguage($request); header("Location: " . $lang . "/"); } else { if (array_key_exists($parts[1], $config->getLanguages())) { // global pages @@ -50,12 +50,12 @@ try { $request->setVocab($parts[1]); } catch (Exception | ValueError $e) { - $request->setLang($controller->guessLanguage()); + $request->setLang($controller->guessLanguage($request)); $controller->invokeGenericErrorPage($request); return; } if (sizeof($parts) == 3) { // language code missing - $lang = $controller->guessLanguage(); + $lang = $controller->guessLanguage($request); $newurl = $controller->getBaseHref() . $vocab . "/" . $lang . "/"; header("Location: " . $newurl); } else { @@ -97,7 +97,7 @@ $controller->invokeGenericErrorPage($request); } } else { // language code missing, redirect to some language version - $lang = $controller->guessLanguage($vocab); + $lang = $controller->guessLanguage($request, $vocab); $newurl = $controller->getBaseHref() . $vocab . "/" . $lang . "/" . implode('/', array_slice($parts, 2)); $qs = $request->getServerConstant('QUERY_STRING'); if ($qs) { diff --git a/tests/GlobalConfigTest.php b/tests/GlobalConfigTest.php index 7715a3952..5ed79d727 100644 --- a/tests/GlobalConfigTest.php +++ b/tests/GlobalConfigTest.php @@ -58,7 +58,7 @@ public function testGetBaseHref() public function testGetLanguages() { - $this->assertEquals(array('en' => 'en_GB.utf8'), $this->config->getLanguages()); + $this->assertEquals(array('en' => 'en_GB.utf8', 'fi' => 'fi_FI.utf8', 'fr' => 'fr_FR.utf8'), $this->config->getLanguages()); } public function testGetSearchResultsSize() diff --git a/tests/WebControllerTest.php b/tests/WebControllerTest.php index b4ac6f9e9..d0439161d 100644 --- a/tests/WebControllerTest.php +++ b/tests/WebControllerTest.php @@ -226,4 +226,31 @@ public function testFormatChangeList() { $expected = array ('hurr durr' => array ('uri' => 'http://www.skosmos.skos/changes/d3', 'prefLabel' => 'Hurr Durr', 'date' => DateTime::__set_state(array('date' => '2010-02-12 10:26:39.000000', 'timezone_type' => 3, 'timezone' => 'UTC')), 'datestring' => 'Feb 12, 2010'), 'second date' => array ('uri' => 'http://www.skosmos.skos/changes/d2', 'prefLabel' => 'Second date', 'date' => DateTime::__set_state(array('date' => '2010-02-12 15:26:39.000000', 'timezone_type' => 3, 'timezone' => 'UTC')), 'datestring' => 'Feb 12, 2010')); $this->assertEquals($expected, $months['February 2010']); } + + public function testGuessLanguageFirstInConfig() { + $request = new Request($this->model); + $guessedLanguage = $this->webController->guessLanguage($request); + $this->assertEquals($guessedLanguage, 'en'); + } + + public function testGuessLanguageCookie() { + $request = new Request($this->model); + $request->setCookie('SKOSMOS_LANGUAGE', 'fr'); + $guessedLanguage = $this->webController->guessLanguage($request); + $this->assertEquals($guessedLanguage, 'fr'); + } + + public function testGuessLanguageVocabDefault() { + $request = new Request($this->model); + $guessedLanguage = $this->webController->guessLanguage($request, 'groups'); + $this->assertEquals($guessedLanguage, 'fi'); + } + + public function testGuessLanguageAcceptLanguage() { + $request = new Request($this->model); + $request->setServerConstant('HTTP_ACCEPT_LANGUAGE', 'fr'); + $guessedLanguage = $this->webController->guessLanguage($request); + $this->assertEquals($guessedLanguage, 'fr'); + } + } diff --git a/tests/testconfig.ttl b/tests/testconfig.ttl index 924da08db..0835a6620 100644 --- a/tests/testconfig.ttl +++ b/tests/testconfig.ttl @@ -36,7 +36,9 @@ # customize the base element. Set this if the automatic base url detection doesn't work. For example setups behind a proxy. skosmos:baseHref "http://tests.localhost/Skosmos/" ; # interface languages available, and the corresponding system locales - skosmos:languages ( [ rdfs:label "en" ; rdf:value "en_GB.utf8" ] ) ; + skosmos:languages ( [ rdfs:label "en" ; rdf:value "en_GB.utf8" ] + [ rdfs:label "fi" ; rdf:value "fi_FI.utf8" ] + [ rdfs:label "fr" ; rdf:value "fr_FR.utf8" ] ) ; # how many results (maximum) to load at a time on the search results page skosmos:searchResultsSize 5 ; # how many items (maximum) to retrieve in transitive property queries From 9817e231a150c7b249ea4e6cf0300167d2c00a57 Mon Sep 17 00:00:00 2001 From: Osma Suominen Date: Thu, 25 May 2023 09:25:41 +0300 Subject: [PATCH 5/5] add test for best matching Accept-Language --- tests/WebControllerTest.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/WebControllerTest.php b/tests/WebControllerTest.php index d0439161d..ae38bd087 100644 --- a/tests/WebControllerTest.php +++ b/tests/WebControllerTest.php @@ -246,11 +246,20 @@ public function testGuessLanguageVocabDefault() { $this->assertEquals($guessedLanguage, 'fi'); } - public function testGuessLanguageAcceptLanguage() { + public function testGuessLanguageAcceptLanguageSimple() { $request = new Request($this->model); $request->setServerConstant('HTTP_ACCEPT_LANGUAGE', 'fr'); $guessedLanguage = $this->webController->guessLanguage($request); $this->assertEquals($guessedLanguage, 'fr'); } + public function testGuessLanguageAcceptLanguageBestMatch() { + $request = new Request($this->model); + $request->setServerConstant('HTTP_ACCEPT_LANGUAGE', 'sv, de;q=0.9, fi;q=0.8, fr;q=0.5'); + $guessedLanguage = $this->webController->guessLanguage($request); + // configured/available languages are en, fi, fr + // the best matching language for the given Accept-Language is fi + $this->assertEquals($guessedLanguage, 'fi'); + } + }