From b493a0759bdfcdf1595793efc5fea0da068035e6 Mon Sep 17 00:00:00 2001
From: BaranekD <0Baranek.dominik0@gmail.com>
Date: Tue, 16 Aug 2022 11:47:22 +0200
Subject: [PATCH] feat: multivalue idphint

---
 README.md                                     |  16 +-
 config-templates/module_campusmultiauth.php   |   1 +
 lib/Auth/Source/Campusidp.php                 | 154 +++++++++++++++++
 templates/includes/individual-identities.twig |  26 +--
 templates/selectsource.twig                   |  51 +++---
 www/idpSearch.php                             | 155 ++++++++----------
 www/resources/campus-idp.js                   |   2 +
 www/selectsource.php                          |  55 +++++++
 8 files changed, 340 insertions(+), 120 deletions(-)

diff --git a/README.md b/README.md
index 0c52f7d..c92d710 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # simplesamlphp-module-campusmultiauth
 
-Thanks to this module, you can use a saml:SP authentication source together with another authentication source providing basic auth (discovery service and login form are displayed on a single page). This module also supports [aarc_idp_hint](https://zenodo.org/record/4596667/files/AARC-G061-A_specification_for_IdP_hinting.pdf), so you can even skip the login page and be redirected to the targeted identity provider.
+Thanks to this module, you can use a saml:SP authentication source together with another authentication source providing basic auth (discovery service and login form are displayed on a single page).
 
 ## Theme configuration
 
@@ -83,6 +83,8 @@ This component represents a form with username and password. It can be used only
 
 `password_placeholder` - this is displayed as a placeholder in the input for the password. If you want to add localization, you can write the value as a map with language codes as keys and localized strings as values. If current language is not found in keys, the **_first one_** is used instead. If not set at all, it displays a default value.
 
+`entityid` - entityid of the identity provider. Needed for idp hinting.
+
 `priority` - can be set to `primary`, default value is `secondary`. It should be primary if you want users to use this component if they are able to.
 
 `end_col` - on a desktop, components are divided to two columns. If you want this component to be the last one in the first column, set this option to `true`.
@@ -131,6 +133,18 @@ Each identity is a map with the following possible options:
 
 `background_color` - background around the logo. Defined as a [CSS color value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
 
+## Hinting
+
+To help the user choose the right institution to log in, this module supports following standards:
+
+### [aarc_idp_hint](https://zenodo.org/record/4596667/files/AARC-G061-A_specification_for_IdP_hinting.pdf)
+
+A service provider can choose which identity provider should user use, he/she then skips the login page and is redirected to the targeted identity provider.
+
+### [idphint](https://aarc-project.eu/wp-content/uploads/2019/04/AARC-G049-A_specification_for_IdP_hinting-v6.pdf)
+
+A service provider can choose which identity provider(s) should user use. If there is only one option, the user is redirected directly to the identity provider. Otherwise, user chooses from identity providers sent in idphint parameter.
+
 ## Deployment
 
 The easiest way is to use [docker-campusidp](https://github.com/cesnet/docker-campusidp), which includes this module together with SimpleSAMLphp and PHP-FPM.
diff --git a/config-templates/module_campusmultiauth.php b/config-templates/module_campusmultiauth.php
index e9f64c2..0d5daf3 100644
--- a/config-templates/module_campusmultiauth.php
+++ b/config-templates/module_campusmultiauth.php
@@ -28,6 +28,7 @@ $config = [
                 'cs' => 'Heslo',
                 'en' => 'Password',
             ],
+            'entityid' => 'https://idp2.ics.muni.cz/idp/shibboleth',
         ],
         [
             'name' => 'searchbox',
diff --git a/lib/Auth/Source/Campusidp.php b/lib/Auth/Source/Campusidp.php
index 4d249a0..8a5e5d1 100644
--- a/lib/Auth/Source/Campusidp.php
+++ b/lib/Auth/Source/Campusidp.php
@@ -15,6 +15,7 @@ use SimpleSAML\Module;
 use SimpleSAML\Module\core\Auth\UserPassBase;
 use SimpleSAML\Session;
 use SimpleSAML\Utils;
+use Transliterator;
 
 class Campusidp extends Source
 {
@@ -38,6 +39,8 @@ class Campusidp extends Source
 
     public const COOKIE_PASSWORD = 'password';
 
+    public const IDP_HINT_BUTTONS_LIMIT = 5;
+
     private $sources;
 
     private $userPassSourceName;
@@ -231,6 +234,157 @@ class Campusidp extends Source
         return false;
     }
 
+    public static function findSearchboxesToDisplay($hintedIdps, $config)
+    {
+        $result = [];
+
+        for ($i = 0; $i < count($config['components']); $i++) {
+            if ($config['components'][$i]['name'] === 'searchbox') {
+                $ch = curl_init();
+
+                curl_setopt(
+                    $ch,
+                    CURLOPT_URL,
+                    Module::getModuleURL(
+                        'campusmultiauth/idpSearch.php?idphint=' . json_encode(
+                            $hintedIdps
+                        ) . '&skipMatching=true' . '&index=' . $i
+                    )
+                );
+                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+                $idps = json_decode(curl_exec($ch));
+
+                curl_close($ch);
+
+                if (!empty($idps->items)) {
+                    $result[] = $i;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    public static function findIndividualIdentitiesToDisplay($hintedIdps, $config)
+    {
+        $result = [];
+
+        for ($i = 0; $i < count($config['components']); $i++) {
+            if ($config['components'][$i]['name'] === 'individual_identities') {
+                $componentToDisplay = false;
+
+                foreach ($config['components'][$i]['identities'] as $identity) {
+                    if (in_array($identity['upstream_idp'], $hintedIdps, true)) {
+                        $componentToDisplay = true;
+                        break;
+                    }
+                }
+
+                if ($componentToDisplay) {
+                    $result[] = $i;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    public static function getOrPositions($searchboxesToDisplay, $individualIdentitiesToDisplay, $idphint, $config)
+    {
+        $result = [];
+
+        $componentsToDisplay = [];
+        $endColComponent = -1;
+
+        for ($i = 0; $i < count($config['components']); $i++) {
+            if ($config['components'][$i]['name'] === 'local_login' && in_array(
+                $config['components'][$i]['entityid'],
+                $idphint,
+                true
+            )) {
+                $componentsToDisplay[] = $i;
+            }
+
+            if (!empty($config['components'][$i]['end_col']) && $config['components'][$i]['end_col'] === true) {
+                $endColComponent = $i;
+            }
+        }
+
+        $componentsToDisplay = array_merge($componentsToDisplay, $searchboxesToDisplay, $individualIdentitiesToDisplay);
+
+        foreach ($componentsToDisplay as $index1) {
+            if ($index1 <= $endColComponent) {
+                foreach ($componentsToDisplay as $index2) {
+                    if ($index1 < $index2 && $index2 <= $endColComponent) {
+                        $result[] = $index1;
+                        break;
+                    }
+                }
+            } else {
+                foreach ($componentsToDisplay as $index2) {
+                    if ($index1 < $index2) {
+                        $result[] = $index1;
+                        break;
+                    }
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    public static function getIdpsMatchedBySearchTerm($metadata, $searchTerm)
+    {
+        $filteredMetadata = [];
+
+        $transliterator = Transliterator::createFromRules(
+            ':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: Lower(); :: NFC;',
+            Transliterator::FORWARD
+        );
+
+        foreach ($metadata as $entityid => $idpentry) {
+            if (!empty($idpentry['name']) && is_array($idpentry['name'])) {
+                foreach ($idpentry['name'] as $key => $value) {
+                    if (str_contains(
+                        $transliterator->transliterate($value),
+                        $transliterator->transliterate($searchTerm)
+                    )) {
+                        $filteredMetadata[$entityid] = $idpentry;
+                        break;
+                    }
+                }
+            }
+
+            if (!in_array($idpentry, $filteredMetadata, true) && !empty($idpentry['description']) && is_array(
+                $idpentry['description']
+            )) {
+                foreach ($idpentry['description'] as $key => $value) {
+                    if (str_contains(
+                        $transliterator->transliterate($value),
+                        $transliterator->transliterate($searchTerm)
+                    )) {
+                        $filteredMetadata[$entityid] = $idpentry;
+                        break;
+                    }
+                }
+            }
+
+            if (!in_array($idpentry, $filteredMetadata, true) && !empty($idpentry['url']) && is_array(
+                $idpentry['url']
+            )) {
+                foreach ($idpentry['url'] as $key => $value) {
+                    if (str_contains(strtolower($value), strtolower($searchTerm))) {
+                        $filteredMetadata[$entityid] = $idpentry;
+                        break;
+                    }
+                }
+            }
+        }
+
+        return $filteredMetadata;
+    }
+
     public function logout(&$state)
     {
         assert(is_array($state));
diff --git a/templates/includes/individual-identities.twig b/templates/includes/individual-identities.twig
index 1ae23a1..3f1fc6e 100644
--- a/templates/includes/individual-identities.twig
+++ b/templates/includes/individual-identities.twig
@@ -16,18 +16,22 @@
         {% endif %}
     </h4>
 
+    {% set index = 0 %}
     {% for idp in configuration.identities %}
-        <div class="{% if muni_jvs %}margin-bottom-12{% endif %}{% if loop.index0 >= configuration.number_shown %} idp-hidden d-none vhide{% endif %}">
-            <button class="btn-individual-identity btn {% if muni_jvs %}btn-primary btn-border color-{{ configuration.priority }} hover-none-{{ configuration.priority }}{% else %}btn-light shadow-sm {% if configuration.priority == 'primary' %}border-dark text-dark{% else %}border-muted text-muted{% endif %} border-2{% endif %}" type="submit" name="idpentityid" value="{{ idp.upstream_idp }}">
-                {% if muni_jvs %}<span class="no-uppercase color-{{ configuration.priority }} individual-identity-span-wrap">{% endif %}
-                    <img class="individual-identity-logo{% if not muni_jvs %} border-end border-2 border-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}{% endif %}" {% if idp.background_color is defined %}style="background-color: {{ idp.background_color }}"{% endif %} src="{{ idp.logo }}" alt=""/>
-                    <span class="idp-text">{{ '{campusmultiauth:sign_in_with}'|trans }}{{ " " }}{% if attribute(idp.name, currentLanguage) is defined %}{{ attribute(idp.name, currentLanguage) }}
-                        {% elseif idp.name is defined and idp.name is iterable and idp.name is not empty %}{{ idp.name | first }}
-                        {% else %}{{ idp.name }}
-                        {% endif %}</span>
-                {% if muni_jvs %}</span>{% endif %}
-            </button>
-        </div>
+        {% if idphint is not defined or idp.upstream_idp in idphint %}
+            <div class="{% if muni_jvs %}margin-bottom-12{% endif %}{% if index >= configuration.number_shown %} idp-hidden d-none vhide{% endif %}">
+                <button class="btn-individual-identity btn {% if muni_jvs %}btn-primary btn-border color-{{ configuration.priority }} hover-none-{{ configuration.priority }}{% else %}btn-light shadow-sm {% if configuration.priority == 'primary' %}border-dark text-dark{% else %}border-muted text-muted{% endif %} border-2{% endif %}" type="submit" name="idpentityid" value="{{ idp.upstream_idp }}">
+                    {% if muni_jvs %}<span class="no-uppercase color-{{ configuration.priority }} individual-identity-span-wrap">{% endif %}
+                        <img class="individual-identity-logo img-searchbox{% if not muni_jvs %} border-end border-2 border-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}{% endif %}" {% if idp.background_color is defined %}style="background-color: {{ idp.background_color }}"{% endif %} src="{{ idp.logo }}" alt=""/>
+                        <span class="idp-text">{{ '{campusmultiauth:sign_in_with}'|trans }}{{ " " }}{% if attribute(idp.name, currentLanguage) is defined %}{{ attribute(idp.name, currentLanguage) }}
+                            {% elseif idp.name is defined and idp.name is iterable and idp.name is not empty %}{{ idp.name | first }}
+                            {% else %}{{ idp.name }}
+                            {% endif %}</span>
+                    {% if muni_jvs %}</span>{% endif %}
+                </button>
+            </div>
+            {% set index = index + 1 %}
+        {% endif %}
     {% endfor %}
 
     <input type="hidden" name="authstate" value="{{ authstate }}" />
diff --git a/templates/selectsource.twig b/templates/selectsource.twig
index 4dea312..a8a5c35 100644
--- a/templates/selectsource.twig
+++ b/templates/selectsource.twig
@@ -18,6 +18,9 @@
     {{ parent() }}
     <script src="/{{baseurlpath}}module.php/campusmultiauth/resources/jquery-3.6.0.min.js"></script>
     <script src="/{{baseurlpath}}module.php/campusmultiauth/resources/selectize/js/standalone/selectize.min.js"></script>
+
+    <meta name="idphint" value="{% if idphint is defined %}{{ idphint | json_encode | raw }}{% endif %}">
+
     <script src="/{{baseurlpath}}module.php/campusmultiauth/resources/campus-idp.js"></script>
 {% endblock %}
 
@@ -31,32 +34,34 @@
                 {% block content %}
                     <div class="{% if muni_jvs %}grid{% else %}row{% endif %}">
                         <div class="{% if muni_jvs %}grid__cell size--m--2-4 first-col{% else %}col-md-6{% endif %} wrap-col">
+                            {% if hint_component_config is defined and hint_component_config.name == 'individual_identities' %}
+                                {% include '@campusmultiauth/includes/individual-identities.twig' with {'configuration': hint_component_config, 'component_index': 0} %}
+                            {% else %}
+                                {% for component_configuration in wayf_config.components %}
+                                    {% if component_configuration.name == 'local_login' and (idphint is not defined or component_configuration.entityid in idphint) %}
+                                        {% include '@campusmultiauth/includes/local-login.twig' with {'configuration': component_configuration} %}
+                                    {% elseif component_configuration.name == 'individual_identities' and (individual_identities_to_display is not defined or loop.index0 in individual_identities_to_display) %}
+                                        {% include '@campusmultiauth/includes/individual-identities.twig' with {'configuration': component_configuration, 'component_index': loop.index0} %}
+                                    {% elseif component_configuration.name == 'searchbox' and (searchboxes_to_display is not defined or loop.index0 in searchboxes_to_display) %}
+                                        {% include '@campusmultiauth/includes/searchbox.twig' with {'configuration': component_configuration, 'component_index': loop.index0} %}
+                                    {% endif %}
 
-                        {% for component_configuration in wayf_config.components %}
-                            {% if component_configuration.name == 'local_login' %}
-                                {% include '@campusmultiauth/includes/local-login.twig' with {'configuration': component_configuration} %}
-                            {% elseif component_configuration.name == 'individual_identities' %}
-                                {% include '@campusmultiauth/includes/individual-identities.twig' with {'configuration': component_configuration, 'component_index': loop.index0} %}
-                            {% elseif component_configuration.name == 'searchbox' %}
-                                {% include '@campusmultiauth/includes/searchbox.twig' with {'configuration': component_configuration, 'component_index': loop.index0} %}
-                            {% endif %}
-
-                            {% if component_configuration.end_col is defined and component_configuration.end_col %}
-                                </div>
-                                <div class="{% if muni_jvs %}grid__cell size--m--2-4{% else %}col-md-6{% endif %} wrap-col">
-                            {% endif %}
+                                    {% if component_configuration.end_col is defined and component_configuration.end_col %}
+                                        </div>
+                                        <div class="{% if muni_jvs %}grid__cell size--m--2-4{% else %}col-md-6{% endif %} wrap-col">
+                                    {% endif %}
 
-                            {% if not loop.last and (component_configuration.end_col is not defined or component_configuration.end_col != true) %}
-                                <div class="hrline color-secondary">
-                                    <span{% if not muni_jvs %} class="bg-light"{% endif %}>{{ '{campusmultiauth:or}'|trans }}</span>
-                                </div>
-                            {% elseif not loop.last and (component_configuration.end_col is defined and component_configuration.end_col == true) %}
-                                <div class="hrline last-col-component color-secondary">
-                                    <span{% if not muni_jvs %} class="bg-light"{% endif %}>{{ '{campusmultiauth:or}'|trans }}</span>
-                                </div>
+                                    {% if not loop.last and (component_configuration.end_col is not defined or component_configuration.end_col != true) and (or_positions is not defined or loop.index0 in or_positions) %}
+                                        <div class="hrline color-secondary">
+                                            <span{% if not muni_jvs %} class="bg-light"{% endif %}>{{ '{campusmultiauth:or}'|trans }}</span>
+                                        </div>
+                                    {% elseif not loop.last and (component_configuration.end_col is defined and component_configuration.end_col == true) %}
+                                        <div class="hrline last-col-component color-secondary">
+                                            <span{% if not muni_jvs %} class="bg-light"{% endif %}>{{ '{campusmultiauth:or}'|trans }}</span>
+                                        </div>
+                                    {% endif %}
+                                {% endfor %}
                             {% endif %}
-                        {% endfor %}
-
                         </div>
                     </div>
                 {% endblock %}
diff --git a/www/idpSearch.php b/www/idpSearch.php
index 45ac19f..f3e8732 100644
--- a/www/idpSearch.php
+++ b/www/idpSearch.php
@@ -8,118 +8,103 @@ use SimpleSAML\Module\campusmultiauth\Auth\Source\Campusidp;
 
 header('Content-type: application/json');
 
-$index = $_GET['index'];
-
-$config = Configuration::getConfig('module_campusmultiauth.php')->toArray();
-$searchBox = $config['components'][$index];
+$language = $_GET['language'] ?? 'en';
 
 $metadataStorageHandler = MetaDataStorageHandler::getMetadataHandler();
 $metadata = $metadataStorageHandler->getList();
 
-$searchTerm = $_GET['q'];
-$transliterator = Transliterator::createFromRules(
-    ':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: Lower(); :: NFC;',
-    Transliterator::FORWARD
-);
+if (!empty($_GET['idphint']) && !isset($_GET['index'])) {
+    $filteredData = array_intersect_key($metadata, array_flip(json_decode($_GET['idphint'])));
+} else {
+    $index = $_GET['index'];
+    $searchTerm = $_GET['q'] ?? '';
+    $skipMatching = $_GET['skipMatching'] ?? false;
 
-if (
-    !empty($searchBox['include']['upstream_idps']) ||
-    !empty($searchBox['include']['tags']) ||
-    !empty($searchBox['include']['registration_authorities'])
-) {
-    $filteredMetadata = [];
 
-    foreach ($metadata as $entityid => $idpentry) {
-        if (!empty($searchBox['include']['tags'])) {
-            foreach ($searchBox['include']['tags'] as $tag) {
-                if ($tag === $idpentry['tag']) {
-                    $filteredMetadata[$entityid] = $idpentry;
-                    break;
-                }
-            }
+    $config = Configuration::getConfig('module_campusmultiauth.php')->toArray();
+    $searchBox = $config['components'][$index];
+
+    if (!empty($_GET['idphint'])) {
+        $idphint = $_GET['idphint'];
+        if (!is_array($idphint)) {
+            $idphint = json_decode($idphint);
         }
 
-        if (!empty($searchBox['include']['registration_authorities']) && empty($filteredMetadata[$entityid])) {
-            foreach ($searchBox['include']['registration_authorities'] as $registrationAuthority) {
-                if (!empty($idpentry['RegistrationInfo']['registrationAuthority']) && $idpentry['RegistrationInfo']['registrationAuthority'] === $registrationAuthority) {
-                    $filteredMetadata[$entityid] = $idpentry;
-                    break;
+        $metadata = array_intersect_key($metadata, array_flip($idphint));
+    }
+
+    if (
+        !empty($searchBox['include']['upstream_idps']) ||
+        !empty($searchBox['include']['tags']) ||
+        !empty($searchBox['include']['registration_authorities'])
+    ) {
+        $filteredMetadata = [];
+
+        foreach ($metadata as $entityid => $idpentry) {
+            if (!empty($searchBox['include']['tags'])) {
+                foreach ($searchBox['include']['tags'] as $tag) {
+                    if ($tag === $idpentry['tag']) {
+                        $filteredMetadata[$entityid] = $idpentry;
+                        break;
+                    }
                 }
             }
-        }
 
-        if (!empty($searchBox['include']['upstream_idps']) && empty($filteredMetadata[$entityid])) {
-            foreach ($searchBox['include']['upstream_idps'] as $upstreamIdp) {
-                if ($upstreamIdp === $entityid) {
-                    $filteredMetadata[$entityid] = $idpentry;
-                    break;
+            if (!empty($searchBox['include']['registration_authorities']) && empty($filteredMetadata[$entityid])) {
+                foreach ($searchBox['include']['registration_authorities'] as $registrationAuthority) {
+                    if (!empty($idpentry['RegistrationInfo']['registrationAuthority']) && $idpentry['RegistrationInfo']['registrationAuthority'] === $registrationAuthority) {
+                        $filteredMetadata[$entityid] = $idpentry;
+                        break;
+                    }
                 }
             }
-        }
-    }
 
-    $metadata = $filteredMetadata;
-}
-
-foreach ($metadata as $entityid => $idpentry) {
-    if (!empty($searchBox['exclude']['tags'])) {
-        foreach ($searchBox['exclude']['tags'] as $tag) {
-            if ($tag === $idpentry['tag']) {
-                unset($metadata[$entityid]);
-                break;
+            if (!empty($searchBox['include']['upstream_idps']) && empty($filteredMetadata[$entityid])) {
+                foreach ($searchBox['include']['upstream_idps'] as $upstreamIdp) {
+                    if ($upstreamIdp === $entityid) {
+                        $filteredMetadata[$entityid] = $idpentry;
+                        break;
+                    }
+                }
             }
         }
-    }
 
-    if (!empty($searchBox['exclude']['registration_authorities']) && !empty($metadata[$entityid])) {
-        foreach ($searchBox['exclude']['registration_authorities'] as $registrationAuthority) {
-            if ($registrationAuthority === $idpentry['RegistrationInfo']['registrationAuthority']) {
-                unset($metadata[$entityid]);
-                break;
-            }
-        }
+        $metadata = $filteredMetadata;
     }
 
-    if (!empty($searchBox['exclude']['upstream_idps']) && !empty($metadata[$entityid])) {
-        foreach ($searchBox['exclude']['upstream_idps'] as $upstreamIdp) {
-            if ($upstreamIdp === $entityid) {
-                unset($metadata[$entityid]);
-                break;
+    foreach ($metadata as $entityid => $idpentry) {
+        if (!empty($searchBox['exclude']['tags'])) {
+            foreach ($searchBox['exclude']['tags'] as $tag) {
+                if ($tag === $idpentry['tag']) {
+                    unset($metadata[$entityid]);
+                    break;
+                }
             }
         }
-    }
-}
-
-$filteredData = [];
 
-foreach ($metadata as $entityid => $idpentry) {
-    if (!empty($idpentry['name']) && is_array($idpentry['name'])) {
-        foreach ($idpentry['name'] as $key => $value) {
-            if (str_contains($transliterator->transliterate($value), $transliterator->transliterate($searchTerm))) {
-                $filteredData[$entityid] = $idpentry;
-                break;
+        if (!empty($searchBox['exclude']['registration_authorities']) && !empty($metadata[$entityid])) {
+            foreach ($searchBox['exclude']['registration_authorities'] as $registrationAuthority) {
+                if ($registrationAuthority === $idpentry['RegistrationInfo']['registrationAuthority']) {
+                    unset($metadata[$entityid]);
+                    break;
+                }
             }
         }
-    }
 
-    if (!in_array($idpentry, $filteredData, true) && !empty($idpentry['description']) && is_array(
-        $idpentry['description']
-    )) {
-        foreach ($idpentry['description'] as $key => $value) {
-            if (str_contains($transliterator->transliterate($value), $transliterator->transliterate($searchTerm))) {
-                $filteredData[$entityid] = $idpentry;
-                break;
+        if (!empty($searchBox['exclude']['upstream_idps']) && !empty($metadata[$entityid])) {
+            foreach ($searchBox['exclude']['upstream_idps'] as $upstreamIdp) {
+                if ($upstreamIdp === $entityid) {
+                    unset($metadata[$entityid]);
+                    break;
+                }
             }
         }
     }
 
-    if (!in_array($idpentry, $filteredData, true) && !empty($idpentry['url']) && is_array($idpentry['url'])) {
-        foreach ($idpentry['url'] as $key => $value) {
-            if (str_contains(strtolower($value), strtolower($searchTerm))) {
-                $filteredData[$entityid] = $idpentry;
-                break;
-            }
-        }
+    if ($skipMatching) {
+        $filteredData = $metadata;
+    } else {
+        $filteredData = Campusidp::getIdpsMatchedBySearchTerm($metadata, $searchTerm);
     }
 }
 
@@ -129,8 +114,8 @@ foreach ($filteredData as $entityid => $idpentry) {
     $item['idpentityid'] = $entityid;
     $item['image'] = $searchBox['logos'][$entityid] ?? Campusidp::getMostSquareLikeImg($idpentry);
 
-    if (!empty($idpentry['name'][$_GET['language']])) {
-        $item['text'] = $idpentry['name'][$_GET['language']];
+    if (!empty($idpentry['name'][$language])) {
+        $item['text'] = $idpentry['name'][$language];
     } elseif (!empty($idpentry['name']['en'])) {
         $item['text'] = $idpentry['name']['en'];
     } elseif (reset($idpentry['name'])) {
diff --git a/www/resources/campus-idp.js b/www/resources/campus-idp.js
index 365b454..ff10794 100644
--- a/www/resources/campus-idp.js
+++ b/www/resources/campus-idp.js
@@ -77,6 +77,7 @@ function selectizeLoad(query, callback) {
 		data: {
 			q: query,
 			index: this.settings.myIndex,
+			idphint: this.settings.idphint,
 			language: document.documentElement.getAttribute("lang"),
 			page_limit: 10,
 		},
@@ -167,6 +168,7 @@ document.addEventListener("DOMContentLoaded", function () {
 			create: false,
 			maxItems: 1,
 			myIndex: index,
+			idphint: JSON.parse(document.querySelector('meta[name="idphint"]').content),
 			loadThrottle: 250,
 			placeholder: placeholderTexts[index] ?? defaultPlaceholder,
 			render: {
diff --git a/www/selectsource.php b/www/selectsource.php
index 008cecc..7dd0939 100644
--- a/www/selectsource.php
+++ b/www/selectsource.php
@@ -34,9 +34,43 @@ if (array_key_exists('aarc_idp_hint', $state)) {
 
 if (array_key_exists('idphint', $state)) {
     $parts = explode(',', $state['idphint']);
+
     if (count($parts) === 1) {
         $state['saml:idp'] = urldecode($parts[0]);
         Campusidp::delegateAuthentication($state[Campusidp::SP_SOURCE_NAME], $state);
+    } else {
+        $idphint = [];
+        foreach ($parts as $part) {
+            $idphint[] = urldecode($part);
+        }
+
+        if (count($idphint) <= Campusidp::IDP_HINT_BUTTONS_LIMIT) {
+            $ch = curl_init();
+
+            curl_setopt(
+                $ch,
+                CURLOPT_URL,
+                Module::getModuleURL('campusmultiauth/idpSearch.php?idphint=' . json_encode($idphint))
+            );
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+            $idpsAsConfigItems = json_decode(curl_exec($ch));
+            curl_close($ch);
+
+            $hintComponentConfig = [];
+            $hintComponentConfig['name'] = 'individual_identities';
+            $hintComponentConfig['priority'] = 'primary';
+            $hintComponentConfig['title'] = '';
+            $hintComponentConfig['number_shown'] = Campusidp::IDP_HINT_BUTTONS_LIMIT;
+
+            for ($i = 0; $i < count($idpsAsConfigItems->items); $i++) {
+                $hintComponentConfig['identities'][$i] = [
+                    'name' => $idpsAsConfigItems->items[$i]->text,
+                    'logo' => $idpsAsConfigItems->items[$i]->image,
+                    'upstream_idp' => $idpsAsConfigItems->items[$i]->idpentityid,
+                ];
+            }
+        }
     }
 }
 
@@ -141,6 +175,27 @@ $t->data['user_pass_source_name'] = $state[Campusidp::USER_PASS_SOURCE_NAME];
 $t->data['sp_source_name'] = $state[Campusidp::SP_SOURCE_NAME];
 $t->data['cookie_username'] = Campusidp::getCookie(Campusidp::COOKIE_USERNAME);
 $t->data['cookie_password'] = Campusidp::getCookie(Campusidp::COOKIE_PASSWORD);
+
+if (!empty($idphint)) {
+    $t->data['idphint'] = $idphint;
+
+    if (empty($hintComponentConfig)) {
+        $searchboxesToDisplay = Campusidp::findSearchboxesToDisplay($idphint, $wayfConfig);
+        $individualIdentitiesToDisplay = Campusidp::findIndividualIdentitiesToDisplay($idphint, $wayfConfig);
+
+        $t->data['searchboxes_to_display'] = $searchboxesToDisplay;
+        $t->data['individual_identities_to_display'] = $individualIdentitiesToDisplay;
+        $t->data['or_positions'] = Campusidp::getOrPositions(
+            $searchboxesToDisplay,
+            $individualIdentitiesToDisplay,
+            $idphint,
+            $wayfConfig
+        );
+    } else {
+        $t->data['hint_component_config'] = $hintComponentConfig;
+    }
+}
+
 $t->data['searchbox_indexes'] = json_encode(array_values(array_filter(array_map(function ($config, $index) {
     return $config['name'] === 'searchbox' ? $index : null;
 }, $wayfConfig['components'], array_keys($wayfConfig['components'])), function ($a) {
-- 
GitLab