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