From 3e6545f9d741360d8eedad0d5fc7b1349a3a9e64 Mon Sep 17 00:00:00 2001
From: "Hahn Axel (hahn)" <axel.hahn@unibe.ch>
Date: Mon, 16 Sep 2024 12:18:45 +0200
Subject: [PATCH] use a class; add tld filter in mode boxes

---
 Readme.md                         | 10 ++++
 classes/shibd_discofeed.class.php | 76 +++++++++++++++++++----------
 config.php.dist                   |  7 +++
 functions.js                      | 44 +++++++++++++----
 inc_functions.php                 | 80 +++++--------------------------
 inc_mode_boxes.php                | 21 ++++----
 index.php                         |  7 +--
 login_aai.css                     | 79 +++++++++++++++++++++---------
 8 files changed, 186 insertions(+), 138 deletions(-)

diff --git a/Readme.md b/Readme.md
index acebbfd..cd2e6f5 100644
--- a/Readme.md
+++ b/Readme.md
@@ -39,6 +39,9 @@ return [
 
     'title' => 'AAI Login',
 
+    // -- language to fetch texts in discofeed
+    'lang' => 'de',
+
     // -- enable one of it:
     'mode' => 'boxes',
     // 'mode' => 'wayf',
@@ -65,18 +68,25 @@ return [
     // -- return URL
     'return-url' => '/shib_login.php'
 
+    // -- cache for discofeed
+    'cachefile' => 'discofeed.json',
+    'cachettl' => 60*60,
+
 ];
 ```
 
 | Key | Type         | Description
 |--   |--            |--
 | title              | string  | Title of the login page; used for title tag and h1 header
+| lang               | string  | Language to detect texts in discofeed as 2 letter code; If the language is not found it takes the 1st text item
 | mode               | string  | Selection mode; one of <br>-`"wayf"` Selection with WAYF script from Switch or<br>- `"boxes"` Boxes with images incl. filter field
 | text-info          | string  | When not empty: show a warning banner with its text on top eg. for maintenance messages
 | text-before-wayf   | string  | Text to show before wayf select box (for mode = "wayf" only)
 | text-after-logins  | string  | Fisnishing text after
 | idps               | array   | List of enabled idps to whitelist; it will filtered by enabled organisatzions by shibboleth
 | return-url         | string  | Return url to your application afer logging in on then organization url<br>- '/shib_login.php' is for Ilias LMS
+| cachefile          | string  | Releative path for cache file; default: discofeed.json
+| cachettl           | integer | Caching time for cache file (discofeed.json ); default: 60 min
 
 ## Devlopment hints
 
diff --git a/classes/shibd_discofeed.class.php b/classes/shibd_discofeed.class.php
index 6323d81..8458462 100644
--- a/classes/shibd_discofeed.class.php
+++ b/classes/shibd_discofeed.class.php
@@ -2,18 +2,29 @@
 
 class shibd_discofeed
 {
-    protected string $SELFURL = '';
-    protected string $listcache = 'discofeed.json';
     protected string $url_list = '/Shibboleth.sso/DiscoFeed';
     protected string $url_login = '/Shibboleth.sso/Login';
-    protected int $ttlcache = 60*60;
 
-    protected string $configfile = 'config.php';
+    protected string $lang = 'en';
+
+    // caching of discofeed
+    protected string $_sCachefile = 'discofeed.json';
+    protected int $_iCacheTtl;
+
     protected array $aConfig = [];
 
     // ----------------------------------------------------------------------
 
-    public function __construct(string $SELFURL = '')
+    /**
+     * Constructor
+     * @param array $aConfig  config array; the following keys are required:
+     *                        idps        array   IDP poositive list
+     *                        return-url  string  return URL
+     *                        cache-ttl   int     caching time in seconds
+     *                        cachefile   string  caching filename
+     * @param string $SELFURL
+     */
+    public function __construct(array $aConfig, string $SELFURL = '')
     {
         $SELFURL = $SELFURL ?: (
             isset($_SERVER['SERVER_NAME']) ? "https://" . $_SERVER['SERVER_NAME'] : ''
@@ -23,19 +34,17 @@ class shibd_discofeed
             die("ERROR: SELFURL is not set. \$_SERVER['SERVER_NAME'] is not available.");
         }
 
-        $this->SELFURL = $SELFURL;
         $this->url_list = "$SELFURL$this->url_list";
         $this->url_login = "$SELFURL$this->url_login";
 
-        $this->listcache = dirname(__DIR__).'/'.$this->listcache;
-        $this->configfile = dirname(__DIR__).'/'.$this->configfile;
+        $this->_sCachefile = dirname(__DIR__).'/'
+            .($aConfig['cachefile']??'discofeed.json')
+            ;
+        $this->_iCacheTtl = $aConfig['cachettl']??60*60;
 
-        // get the user config
-        if (!file_exists($this->configfile)) {
-            die("ERROR: config.php file does not exist yet.");
-        }
+        $this->lang = $aConfig['lang']??'en';
 
-        $this->aConfig = require($this->configfile);
+        $this->aConfig = $aConfig;
     }
 
     /**
@@ -45,17 +54,17 @@ class shibd_discofeed
      */
     function getAllIdps(): array
     {
-        if (!file_exists($this->listcache) || filemtime($this->listcache) < time() - $this->ttlcache) {
+        if (!file_exists($this->_sCachefile) || filemtime($this->_sCachefile) < time() - $this->_iCacheTtl) {
             // echo "DEBUG: IDP - reading from Shibboleth<br>";
             $aReturn = json_decode(file_get_contents($this->url_list), 1);
     
             if ($aReturn && is_array($aReturn)) {
                 // echo "DEBUG: IDP - storing cache<br>";
-                file_put_contents($this->listcache, json_encode($aReturn));
+                file_put_contents($this->_sCachefile, json_encode($aReturn));
             }
         } else {
             // echo "DEBUG: IDP - reading cache<br>";
-            $aReturn = json_decode(file_get_contents($this->listcache), 1);
+            $aReturn = json_decode(file_get_contents($this->_sCachefile), 1);
         }
     
         return isset($aReturn) && is_array($aReturn) ? $aReturn : [];
@@ -63,7 +72,7 @@ class shibd_discofeed
 
     /**
      * Get list of active IDPs
-     * @return mixed
+     * @return array
      */
     public function getIdps(): array
     {
@@ -73,10 +82,22 @@ class shibd_discofeed
         if (is_array($aAllIdps) && count($aAllIdps)) {
             foreach ($aAllIdps as $aEntry) {
                 $sEntityId = $aEntry['entityID'];
+                $sIdpDomain = parse_url($sEntityId, PHP_URL_HOST);
+                $sIdpTld = preg_filter('/^.*?\.([^\.]+)$/', '$1', $sIdpDomain);
     
                 if (in_array($sEntityId, $this->aConfig['idps'])) {
     
-                    $sLabel = $aEntry['DisplayNames'][0]['value'] ?? parse_url($sEntityId, PHP_URL_HOST);
+                    $idxText=0;
+                    foreach($aEntry['DisplayNames'] as $i=>$aLangitem){
+                        if($aLangitem['lang']==$this->lang){
+                            $idxText=$i;
+                        }
+                    }
+
+                    $sLabel = $aEntry['DisplayNames'][$idxText]['value'] ?? parse_url($sEntityId, PHP_URL_HOST);
+                    $sDescription = $aEntry['Descriptions'][$idxText]['value'] ?? '';
+                    $sKeywords = $aEntry['Keywords'][$idxText]['value'] ?? '';
+
                     $sImage = $aEntry['Logos'][1]['value'] ?? ($aEntry['Logos'][0]['value'] ?? '');
     
                     $sUrl = $this->url_login
@@ -85,14 +106,17 @@ class shibd_discofeed
                         . "&target=" . urlencode($this->SELFURL.($aConfig['return-url']??''))
                         ;
     
-                    $aReturn[] = [
-                        'label' => $sLabel,
-                        'image' => $sImage,
-                        'url' => $sUrl,
-    
-                        // for debugging
-                        '_entity' => $aEntry
-                    ];
+                    $aReturn[] = array_merge([
+                        '_label' => $sLabel,
+                        '_description' => $sDescription,
+                        '_keywords' => $sKeywords,
+                        '_image' => $sImage,
+                        '_url' => $sUrl,
+                        '_idpdomain' => $sIdpDomain,
+                        '_tld' => $sIdpTld,
+                        ], 
+                        $aEntry
+                    );
                 }
             }
         }
diff --git a/config.php.dist b/config.php.dist
index 1c045e9..d50454d 100644
--- a/config.php.dist
+++ b/config.php.dist
@@ -5,6 +5,9 @@ return [
     'mode' => 'boxes',
     // 'mode' => 'wayf',
 
+    // -- language to fetch texts in discofeed
+    'lang' => 'en',
+
     // 'text-info' => '+++ Hinweis +++ Hinweis +++ Hinweis +++ Hinweis +++',
 
     'text-before' => '<p>Studierende und Dozenten nutzen in der Regel das nachfolgende "Anmelden":</p>',
@@ -23,4 +26,8 @@ return [
     
     'return-url' => '/shib_login.php'
 
+    // -- cache for discofeed
+    'cachefile' => 'discofeed.json',
+    'cachettl' => 60*60,
+
 ];
\ No newline at end of file
diff --git a/functions.js b/functions.js
index 52cb975..a10c998 100644
--- a/functions.js
+++ b/functions.js
@@ -31,20 +31,27 @@ function applyfilter(){
 
         for(var i=0; i<document.getElementsByClassName('idp').length; i++){
             var idp = document.getElementsByClassName('idp')[i];
-            var bShow=idp.innerText.toLowerCase().indexOf(q.toLowerCase())>=0;
-            if(q==""){
-                bShow=true;
-            }
-            idp.style.display = bShow ? 'block' : 'none';
+
+            var bShow=(q==""
+                ? true
+                  // strip innerHTML and compare lowercase of it with lowercase of query
+                : idp.innerHTML.replace(/<[^>]*>/g, "").toLowerCase().indexOf(q.toLowerCase())>=0
+            );
+                
+            idp.className = bShow ? 'idp' : 'idp hide';
             document.getElementById('resetfilter').style.display = (q>"") ? 'inline': 'none';
         }
         localStorage.setItem(sLsvar,q);
     };
 }
 
+function setFilter(sNewFiltervalue){
+    document.getElementById('filter').value=sNewFiltervalue;
+    applyfilter();    
+}
+
 function resetFilter(){
-    document.getElementById('filter').value="";
-    applyfilter();
+    setFilter("");
 }
 /**
  * Enable filter box if static_link is in use
@@ -57,8 +64,29 @@ function showFilterBox(){
         if(!q){
             q='';
         }
+
+        var btnList = '';
+
+        var aTlds = [];
+        for(var i=0; i<document.getElementsByClassName('idp').length; i++){
+            var idp = document.getElementsByClassName('idp')[i];
+            var sText=idp.innerHTML.replace(/<[^>]*>/g, "").split('.').pop().trim().toUpperCase();
+            if (sText){
+                aTlds[sText]=1;
+            }
+        }
+        // if(aTlds.length){            
+            for(var tld in aTlds){
+                btnList+=' <a href="#" class="filterbutton" onclick="setFilter(\' .'+tld+'\'); return false;">.'+tld+'</a> ';
+            }
+        // }
+
         oFilter.style.display = 'block';
-        oFilter.innerHTML='<input type="text" id="filter" placeholder="" onchange="applyfilter()" onkeydown="applyfilter()" onkeyup="applyfilter()" value="'+q+'"/><button id="resetfilter" onclick="resetFilter(); return false;"> X </button>';
+
+        oFilter.innerHTML='<input type="text" id="filter" placeholder="" onchange="applyfilter()" onkeydown="applyfilter()" onkeyup="applyfilter()" value="'+q+'"/>'
+            +'<a id="resetfilter" onclick="resetFilter(); return false;"> X </a><br>'
+            + (btnList ? '<br>'+btnList : '' )
+            ;
         applyfilter();
         for(var i=0; i<document.getElementsByClassName('idp').length; i++){
             var idp = document.getElementsByClassName('idp')[i];
diff --git a/inc_functions.php b/inc_functions.php
index 4342860..3ce963d 100644
--- a/inc_functions.php
+++ b/inc_functions.php
@@ -9,23 +9,25 @@
 // Source: https://git-repo.iml.unibe.ch/iml-open-source/login-aai
 // ======================================================================
 
-// WIP:
-// require 'classes/shibd_discofeed.class.php';
-// $oD = new shibd_discofeed();
-// print_r($oD->getAllIdps());
 
-$SELFURL = isset($_SERVER['SERVER_NAME']) ? "https://" . $_SERVER['SERVER_NAME'] : '';
+// ----------------------------------------------------------------------
+// INIT
+// ----------------------------------------------------------------------
+
+require 'classes/shibd_discofeed.class.php';
 
-$url_list = "$SELFURL/Shibboleth.sso/DiscoFeed";
-$listcache = "discofeed.json";
-$ttlcache = 60 * 10;
 
 // get the user config
 if (!file_exists('config.php')) {
     die("ERROR: file config.php does not exist yet.");
 }
 
+
 $aConfig = require 'config.php';
+$SELFURL = isset($_SERVER['SERVER_NAME']) ? "https://" . $_SERVER['SERVER_NAME'] : '';
+$oDiscofeed = new shibd_discofeed($aConfig, $SELFURL);
+$aIdplist = $oDiscofeed->getIdps();
+
 
 // ----------------------------------------------------------------------
 // functions
@@ -42,63 +44,6 @@ function showMessage(string $sLevel, string $sMessage)
     echo "<div class=\"msg $sLevel\">$sMessage</div>";
 }
 
-/**
- * Get List if IDPs from cache file if possible or from Shibboleth Disco feed
- * @return array
- */
-function getAllIdps(): array
-{
-    global $listcache, $ttlcache, $url_list, $aConfig;
-
-    if (!file_exists($listcache) || filemtime($listcache) < time() - $ttlcache) {
-        // echo "DEBUG: IDP - reading from Shibboleth<br>";
-        $aReturn = json_decode(file_get_contents($url_list), 1);
-
-        if ($aReturn && is_array($aReturn)) {
-            // echo "DEBUG: IDP - storing cache<br>";
-            file_put_contents($listcache, json_encode($aReturn));
-        }
-    } else {
-        // echo "DEBUG: IDP - reading cache<br>";
-        $aReturn = json_decode(file_get_contents($listcache), 1);
-    }
-
-    return isset($aReturn) && is_array($aReturn) ? $aReturn : [];
-}
-
-/**
- * Get list of active IDPs
- * @return mixed
- */
-function getIdps()
-{
-    global $aConfig, $SELFURL;
-    $aAllIdps = getAllIdps();
-
-    if (is_array($aAllIdps) && count($aAllIdps)) {
-        foreach ($aAllIdps as $aEntry) {
-            $sEntityId = $aEntry['entityID'];
-
-            if (in_array($sEntityId, $aConfig['idps'])) {
-
-                $sLabel = $aEntry['DisplayNames'][0]['value'] ?? parse_url($sEntityId, PHP_URL_HOST);
-                $sImage = $aEntry['Logos'][1]['value'] ?? ($aEntry['Logos'][0]['value'] ?? '');
-
-                $sUrl = "$SELFURL/Shibboleth.sso/Login?entityID=" . urlencode($sEntityId) . "&target=" . urlencode($SELFURL.$aConfig['return-url']??'');
-
-                $aReturn[] = [
-                    'label' => $sLabel,
-                    'image' => $sImage,
-                    'url' => $sUrl,
-
-                    // for debugging
-                    '_entity' => $aEntry
-                ];
-            }
-        }
-    }
-    return $aReturn;
-}
 
 /**
  * Get a list of static links for browsers without javascript
@@ -108,8 +53,9 @@ function getIdps()
 function getStaticlinks($aIdplist){
     $sReturn='';
     foreach ($aIdplist as $aEntry) {
-        $sReturn .= '<a href="' . $aEntry['url']. '">' . $aEntry['label'] . '</a><br>' . "\n";
+        $sReturn .= '<a href="' . $aEntry['_url']. '">' . $aEntry['_label'] . '</a><br>' . "\n";
     }
     return $sReturn;
 
-}
\ No newline at end of file
+}
+
diff --git a/inc_mode_boxes.php b/inc_mode_boxes.php
index ce7599a..a439eeb 100644
--- a/inc_mode_boxes.php
+++ b/inc_mode_boxes.php
@@ -9,21 +9,24 @@
 // Source: https://git-repo.iml.unibe.ch/iml-open-source/login-aai
 // ======================================================================
 
-$sOut = '';
-// $sOut.='<pre>'.print_r($aIdplist, 1).'</pre>';
-
 if (is_array($aIdplist) && count($aIdplist)) {
-    $sOut .= '<div id="filterbox"></div><br>';
+    $sOut = '';
+    $sOut .= '<div id="filterbox"></div><div class="boxes">';
     foreach ($aIdplist as $aEntry) {
-        $sOut .= '<div class="idp">
-                    <a href="' . $aEntry['url']. '">' . $aEntry['label'] . '<br>
-                    <img src="' . $aEntry['image'] . '"><br>
+        $sOut .= '
+                    <a href="' . $aEntry['_url']. '" 
+                        class="idp"
+                        title="' . $aEntry['_description'] . '"
+                        >' . $aEntry['_label'] . '<br>
+                        <span class="hidden">' . $aEntry['_description'] . ' ' . $aEntry['_keywords'] . ' .'. $aEntry['_tld'] . '</span>
+                        <img src="' . $aEntry['_image'] . '"><br>
                     </a>
-                </div>'
+                '
                 . "\n"
                 ;
     }
-    echo "$sOut<div style='clear:both'></div>";
+    $sOut.='</div><div style="clear:both"></div>';
+    echo $sOut;
 } else {
     echo '<div class="msg error">No IDPs found in Discovery Feed.</div>';
 }
diff --git a/index.php b/index.php
index eb5bc13..50463e3 100644
--- a/index.php
+++ b/index.php
@@ -14,11 +14,8 @@
     ======================================================================
 */
 
-
     require 'inc_functions.php';
 
-
-
 ?><!DOCTYPE html>
 <html lang="" dir="">
 
@@ -44,8 +41,6 @@
 
         <?php
 
-            $aIdplist=getIdps();
-
             // --- messages
             echo $SELFURL ? '' : showMessage('error', 'SELFURL is not set. $_SERVER[\'SERVER_NAME\'] is not available.');
             echo $aConfig['text-info'] ? showMessage('info', $aConfig['text-info']) : ''; 
@@ -64,7 +59,7 @@
     </div>
     <script type="text/javascript" defer src="functions.js"></script>
 
-    <footer><?php echo $aConfig['title'] ?? 'AAI Login'; ?><br>AAI Login v0.1 - <a href="https://git-repo.iml.unibe.ch/iml-open-source/login-aai">Source</a></a></footer>
+    <footer><?php echo $aConfig['title'] ?? 'AAI Login'; ?><br>AAI Login v0.2 - <a href="https://git-repo.iml.unibe.ch/iml-open-source/login-aai">Source</a></a></footer>
 </body>
 
 </html>
\ No newline at end of file
diff --git a/login_aai.css b/login_aai.css
index 6c4a61d..5dfc345 100644
--- a/login_aai.css
+++ b/login_aai.css
@@ -4,7 +4,7 @@
 
     --h1-color: #68a;
     --h2-color: #68a;
-    --a-color: #68a;
+    --a-color: #45c;
 
     --content-bg: #fff;
     --content-border: 3px solid #d0d8e0;
@@ -28,6 +28,10 @@
     --inputfilter-border: 2px solid #eee;
     --inputfilter-color:#789;
 
+    --btn-border: 2px solid #ddd;
+    --btn-bg:#eee;
+    --btn-color: #888;
+
     --resetfilter-border: 2px solid #dcc;
     --resetfilter-bg:#edd;
     --resetfilter-color: #800;
@@ -84,9 +88,47 @@ footer{
     margin: 10em auto 5em;
     text-align: right; 
 }
-/** ---------- mode boxes */
 
-div.idp a{ 
+/** ---------- mode boxes: filterbar on top */
+
+div#filterbox{
+    margin-bottom: 1em;
+}
+div#filterbox a.filterbutton, a#resetfilter{
+    background: var(--btn-bg);
+    border: var(--btn-border);
+    border-radius: 0.2em;
+    color: var(--btn-color);
+    font-size: 130%;
+    margin: 0 0.3em 0 0;
+    opacity: 1;
+    padding: 0.5em;
+    text-decoration: none;
+}
+input#filter{
+    border: var(--inputfilter-border);
+    padding: 0.5em;
+    color:var(--inputfilter-color);
+    font-size: 130%;
+    width: 20em;
+}
+a#resetfilter{
+    background:var(--resetfilter-bg);
+    border: var(--resetfilter-border);
+    color: var(--resetfilter-color);
+    font-weight: bold;
+    float: right;
+    margin: 0 0 0 0.3em;
+    padding: 0.5em 2em;
+}
+a#resetfilter:hover{
+    opacity: 0.5;
+}
+
+
+/** ---------- mode boxes */
+.hidden{display: none;}
+div.boxes a.idp{ 
     border: var(--idp-border); 
     border-radius: 1em; 
     box-shadow: var(--idp-shadow);
@@ -95,28 +137,21 @@ div.idp a{
     height: 7em; 
     margin: 0 1em 1em 0; 
     overflow: hidden;
-    padding: 1em;
+    padding: 0.5em;
     text-align: center;
+    text-decoration: none;
+    transition: all 0.1s ease-in-out;
     width: 10em;
 }
-div.idp img { height: 80px;}
-div.idp a:hover{ 
+div.boxes .idp img { height: 80px;}
+div.boxes a.idp:hover{ 
     box-shadow: var(--idp-hover-shadow);
 }
-
-input#filter{
-    border: var(--inputfilter-border);
-    padding: 0.5em;
-    color:var(--inputfilter-color);
-    font-size: 130%;
-    width: 80%;
+div.boxes a.hide{
+    border: none;
+    height: 0;
+    margin:0;
+    padding:0;
+    width: 0;
 }
-button#resetfilter{
-    border: var(--resetfilter-border);
-    font-weight: bold;
-    font-size: 130%;
-    padding: 0.5em 2em;
-    background:var(--resetfilter-bg);
-    color: var(--resetfilter-color);
-    margin-left: 1em;
-}
\ No newline at end of file
+
-- 
GitLab