diff --git a/lib/SimpleSAML/IdP.php b/lib/SimpleSAML/IdP.php index 7b9ea5be373bd2911e902960adf65ffd706e8fb7..9fce99741424bb5778db0e47c5494416043626b4 100644 --- a/lib/SimpleSAML/IdP.php +++ b/lib/SimpleSAML/IdP.php @@ -169,6 +169,8 @@ class IdP /** * Get SP name. + * Only usefd in IFrameLogout it seems. + * TODO: probably replace with template Template::getEntityDisplayName() * * @param string $assocId The association identifier. * diff --git a/lib/SimpleSAML/Locale/Language.php b/lib/SimpleSAML/Locale/Language.php index 6d3cd9d5dcbe21be671266cb58fc02b9fbb186c0..3ae24285f5ec4c47b0ac774a71a4a3fe07cb5eab 100644 --- a/lib/SimpleSAML/Locale/Language.php +++ b/lib/SimpleSAML/Locale/Language.php @@ -51,6 +51,13 @@ class Language */ private string $defaultLanguage; + /** + * The final fallback language to use when no current or default available + * + * @var string + */ + public const FALLBACKLANGUAGE = 'en'; + /** * An array holding a list of languages that are written from right to left. * @@ -146,7 +153,7 @@ class Language { $this->configuration = $configuration; $this->availableLanguages = $this->getInstalledLanguages(); - $this->defaultLanguage = $this->configuration->getString('language.default', 'en'); + $this->defaultLanguage = $this->configuration->getString('language.default', self::FALLBACKLANGUAGE); $this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language'); $this->customFunction = $this->configuration->getArray('language.get_language_function', null); $this->rtlLanguages = $this->configuration->getArray('language.rtl', []); @@ -166,7 +173,7 @@ class Language */ private function getInstalledLanguages(): array { - $configuredAvailableLanguages = $this->configuration->getArray('language.available', ['en']); + $configuredAvailableLanguages = $this->configuration->getArray('language.available', [self::FALLBACKLANGUAGE]); $availableLanguages = []; foreach ($configuredAvailableLanguages as $code) { if (array_key_exists($code, self::$language_names) && isset(self::$language_names[$code])) { @@ -377,6 +384,16 @@ class Language return in_array($this->getLanguage(), $this->rtlLanguages, true); } + /** + * Returns the list of languages in order of preference. This is useful + * to search e.g. an array of entity names for first the current language, + * if not present the default language, if not present the fallback language. + */ + public function getPreferredLanguages(): array + { + $curLanguage = $this->getLanguage(); + return array_unique([0 => $curLanguage, 1 => $this->defaultLanguage, 2 => self::FALLBACKLANGUAGE]); + } /** * Retrieve the user-selected language from a cookie. @@ -386,7 +403,7 @@ class Language public static function getLanguageCookie(): ?string { $config = Configuration::getInstance(); - $availableLanguages = $config->getArray('language.available', ['en']); + $availableLanguages = $config->getArray('language.available', [self::FALLBACKLANGUAGE]); $name = $config->getString('language.cookie.name', 'language'); if (isset($_COOKIE[$name])) { @@ -399,7 +416,6 @@ class Language return null; } - /** * This method will attempt to set the user-selected language in a cookie. It will do nothing if the language * specified is not in the list of available languages, or the headers have already been sent to the browser. @@ -410,7 +426,7 @@ class Language { $language = strtolower($language); $config = Configuration::getInstance(); - $availableLanguages = $config->getArray('language.available', ['en']); + $availableLanguages = $config->getArray('language.available', [self::FALLBACKLANGUAGE]); if (!in_array($language, $availableLanguages, true) || headers_sent()) { return; diff --git a/lib/SimpleSAML/XHTML/IdPDisco.php b/lib/SimpleSAML/XHTML/IdPDisco.php index e3c0517ad974fa85aa73b66f734aa0f2493b783f..39d475e8750ad9e25ca7f212a99be92bf965a4f6 100644 --- a/lib/SimpleSAML/XHTML/IdPDisco.php +++ b/lib/SimpleSAML/XHTML/IdPDisco.php @@ -589,30 +589,12 @@ class IdPDisco $t = new Template($this->config, $templateFile, 'disco'); - $fallbackLanguage = 'en'; - $defaultLanguage = $this->config->getString('language.default', $fallbackLanguage); - $translator = $t->getTranslator(); - $language = $translator->getLanguage()->getLanguage(); - $tryLanguages = [0 => $language, 1 => $defaultLanguage, 2 => $fallbackLanguage]; - $newlist = []; foreach ($idpList as $entityid => $data) { $newlist[$entityid]['entityid'] = $entityid; - foreach ($tryLanguages as $lang) { - if ($name = $this->getEntityDisplayName($data, $lang)) { - $newlist[$entityid]['name'] = $name; - continue; - } - } - if (empty($newlist[$entityid]['name'])) { - $newlist[$entityid]['name'] = $entityid; - } - foreach ($tryLanguages as $lang) { - if (!empty($data['description'][$lang])) { - $newlist[$entityid]['description'] = $data['description'][$lang]; - continue; - } - } + $newlist[$entityid]['name'] = $t->getEntityDisplayName($data); + + $newlist[$entityid]['description'] = $t->getEntityPropertyTranslation('description', $data); if (!empty($data['icon'])) { $newlist[$entityid]['icon'] = $data['icon']; $newlist[$entityid]['iconurl'] = $httpUtils->resolveURL($data['icon']); @@ -640,22 +622,4 @@ class IdPDisco $t->data['rememberchecked'] = $this->config->getBoolean('idpdisco.rememberchecked', false); $t->send(); } - - - /** - * @param array $idpData - * @param string $language - * @return string|null - */ - private function getEntityDisplayName(array $idpData, string $language): ?string - { - if (isset($idpData['UIInfo']['DisplayName'][$language])) { - return $idpData['UIInfo']['DisplayName'][$language]; - } elseif (isset($idpData['name'][$language])) { - return $idpData['name'][$language]; - } elseif (isset($idpData['OrganizationDisplayName'][$language])) { - return $idpData['OrganizationDisplayName'][$language]; - } - return null; - } } diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php index f9df3c72cda1e154ec698ec6afb8b96507b8fbcf..5921db0597d4e66d367e0fee3d22aae5ffcd48d9 100644 --- a/lib/SimpleSAML/XHTML/Template.php +++ b/lib/SimpleSAML/XHTML/Template.php @@ -583,4 +583,47 @@ class Template extends Response { return $this->translator->getLanguage()->isLanguageRTL(); } + + /** + * Search through entity metadata to find the best display name for this + * entity. It will search in order for the current language, default + * language and fallback language for the DisplayName, name, OrganizationDisplayName + * and OrganizationName; the first one found is considered the best match. + * If nothing found, will return the entityId. + */ + public function getEntityDisplayName(array $data): string + { + $tryLanguages = $this->translator->getLanguage()->getPreferredLanguages(); + + foreach($tryLanguages as $language) { + if (isset($data['UIInfo']['DisplayName'][$language])) { + return $data['UIInfo']['DisplayName'][$language]; + } elseif (isset($data['name'][$language])) { + return $data['name'][$language]; + } elseif (isset($data['OrganizationDisplayName'][$language])) { + return $data['OrganizationDisplayName'][$language]; + } elseif (isset($data['OrganizationName'][$language])) { + return $data['OrganizationName'][$language]; + } + } + return $data['entityid']; + } + + /** + * Search through entity metadata to find the best value for a + * specific property. It will search in order for the current language, default + * language and fallback language; if not found it returns null. + */ + public function getEntityPropertyTranslation(string $property, array $data): ?string + { + $tryLanguages = $this->translator->getLanguage()->getPreferredLanguages(); + + foreach($tryLanguages as $language) { + if (isset($data[$property][$language])) { + return $data[$property][$language]; + } + } + + return null; + } } diff --git a/tests/lib/SimpleSAML/Locale/LanguageTest.php b/tests/lib/SimpleSAML/Locale/LanguageTest.php index f309bfb7501957cddc0efa4416a050ccbc4f8f66..c82cd1ffdd88a215014c908804cf974458055cef 100644 --- a/tests/lib/SimpleSAML/Locale/LanguageTest.php +++ b/tests/lib/SimpleSAML/Locale/LanguageTest.php @@ -173,4 +173,27 @@ class LanguageTest extends TestCase $l = new Language($c); $this->assertEquals('en', $l->getLanguage()); } + + public function testGetPreferredLanguages(): void + { + // test defaults + $c = Configuration::loadFromArray([], '', 'simplesaml'); + $l = new Language($c); + $l->setLanguage('en'); + $this->assertEquals(['en'], $l->getPreferredLanguages()); + + // test order current, default, fallback + $c = Configuration::loadFromArray([ + 'language.available' => ['fr', 'nn', 'es'], + 'language.default' => 'nn', + ], '', 'simplesaml'); + $l = new Language($c); + $l->setLanguage('es'); + $this->assertEquals(['es', 'nn', 'en'], $l->getPreferredLanguages()); + + // test duplicate values (curlang is default lang) removed + $l->setLanguage('nn'); + $this->assertEquals([0 => 'nn', 2 => 'en'], $l->getPreferredLanguages()); + } + } diff --git a/tests/lib/SimpleSAML/XHTML/TemplateTest.php b/tests/lib/SimpleSAML/XHTML/TemplateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d5c2732d1035b15784d8535af790e80641034ef5 --- /dev/null +++ b/tests/lib/SimpleSAML/XHTML/TemplateTest.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Test\XHTML; + +use Exception; +use PHPUnit\Framework\TestCase; +use SimpleSAML\Configuration; +use SimpleSAML\XHTML\Template; + +/** + * @covers \SimpleSAML\XHTML\Template + */ +class TemplateTest extends TestCase +{ + private const TEMPLATE = 'sandbox.twig'; + + function testSetup(): void + { + $c = Configuration::loadFromArray([], '', 'simplesaml'); + $t = new Template($c, self::TEMPLATE); + $this->assertEquals(self::TEMPLATE, $t->getTemplateName()); + } + + function testNormalizeName(): void + { + $c = Configuration::loadFromArray([], '', 'simplesaml'); + $t = new Template($c, 'sandbox'); + $this->assertEquals(self::TEMPLATE, $t->getTemplateName()); + } + + function testTemplateModuleNamespace(): void + { + $c = Configuration::loadFromArray([], '', 'simplesaml'); + $t = new Template($c, 'core:login'); + $this->assertEquals('core:login.twig', $t->getTemplateName()); + } + + function testGetEntityDisplayNameBasic(): void + { + $c = Configuration::loadFromArray([], '', 'simplesaml'); + $t = new Template($c, self::TEMPLATE); + + $data = [ + 'entityid' => 'urn:example.org', + 'name' => ['nl' => 'Something', 'en' => 'Other lang'], + ]; + $name = $t->getEntityDisplayName($data); + $this->assertEquals('Other lang', $name); + + $c = Configuration::loadFromArray(['language.default' => 'nl'], '', 'simplesaml'); + $t = new Template($c, self::TEMPLATE); + $name = $t->getEntityDisplayName($data); + $this->assertEquals('Something', $name); + } + + function testGetEntityDisplayNamePriorities(): void + { + $c = Configuration::loadFromArray([], '', 'simplesaml'); + $t = new Template($c, self::TEMPLATE); + + $data = [ + 'entityid' => 'urn:example.org', + ]; + $name = $t->getEntityDisplayName($data); + $this->assertEquals('urn:example.org', $name); + + $data['OrganizationName'] = ['fr' => 'Example Org', 'nl' => 'Anything Org']; + $data['OrganizationDisplayName'] = ['fr' => 'DisplayExample', 'nl' => 'DisplayAnything']; + + $name = $t->getEntityDisplayName($data); + $this->assertEquals('urn:example.org', $name); + + $data['OrganizationName']['en'] = 'Example Org EN'; + + $name = $t->getEntityDisplayName($data); + $this->assertEquals('Example Org EN', $name); + + $c = Configuration::loadFromArray(['language.default' => 'nl'], '', 'simplesaml'); + $t = new Template($c, self::TEMPLATE); + + $data['UIInfo']['DisplayName'] = ['de' => 'UIname', 'nl' => 'UIname NL']; + $name = $t->getEntityDisplayName($data); + $this->assertEquals('UIname NL', $name); + } + + function testGetEntityPropertyTranslation(): void + { + $c = Configuration::loadFromArray([], '', 'simplesaml'); + $t = new Template($c, self::TEMPLATE); + + $prop = 'description'; + $data = [ + 'entityid' => 'urn:example.org', + $prop => ['nl' => 'Something', 'en' => 'Other lang', 'fr' => 'Another desc'], + ]; + $name = $t->getEntityPropertyTranslation($prop, $data); + $this->assertEquals('Other lang', $name); + + $c = Configuration::loadFromArray(['language.default' => 'nl'], '', 'simplesaml'); + $t = new Template($c, self::TEMPLATE); + $name = $t->getEntityPropertyTranslation($prop, $data); + $this->assertEquals('Something', $name); + + unset($data[$prop]['nl']); + $name = $t->getEntityPropertyTranslation($prop, $data); + $this->assertEquals('Other lang', $name); + + unset($data[$prop]['en']); + $name = $t->getEntityPropertyTranslation($prop, $data); + $this->assertNull($name); + } +}