From 213add970a92e581ffad3e3d13ee3e8910e8d9e8 Mon Sep 17 00:00:00 2001 From: Thijs Kinkhorst <thijs@kinkhorst.com> Date: Fri, 3 Sep 2021 14:55:05 +0000 Subject: [PATCH] getEntityDisplayName to get entityID's name for display Reimplement getPreferredTranslation outside of IdPDisco so it's reusable and much more complete. The new method will search the current, default and fallback language for a displayable string in various metadata fields. This should be much more complete and robust than previous approches. Make it a Twig filter so it can also be used there. --- lib/SimpleSAML/IdP.php | 2 + lib/SimpleSAML/Locale/Language.php | 26 ++++- lib/SimpleSAML/XHTML/IdPDisco.php | 42 +------ lib/SimpleSAML/XHTML/Template.php | 43 +++++++ tests/lib/SimpleSAML/Locale/LanguageTest.php | 23 ++++ tests/lib/SimpleSAML/XHTML/TemplateTest.php | 114 +++++++++++++++++++ 6 files changed, 206 insertions(+), 44 deletions(-) create mode 100644 tests/lib/SimpleSAML/XHTML/TemplateTest.php diff --git a/lib/SimpleSAML/IdP.php b/lib/SimpleSAML/IdP.php index 7b9ea5be3..9fce99741 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 6d3cd9d5d..3ae24285f 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 e3c0517ad..39d475e87 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 f9df3c72c..5921db059 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 f309bfb75..c82cd1ffd 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 000000000..d5c2732d1 --- /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); + } +} -- GitLab