diff --git a/Readme.md b/Readme.md index cd2e6f577348acd11b1b3b40ec862b401ea1fdd3..1621bf71122201dc6b3bc44fb04fcee43a12184f 100644 --- a/Readme.md +++ b/Readme.md @@ -16,8 +16,17 @@ We used the WAYF script in the login page so far. When editing login pages in Ilias 9 then javascript is now filtered out while saving. That's why we couldn't embed our current WAYF script anymore. This is a standalone login page that offers a list of organisations and can be customized. +This reads the discofeed. By a given positive list of Idps it shows a list of identity providers to login on the visitors organisation. +  +## Features + +* Displays a list of allowed Identity providers +* The list is filtered during typing in the search field. When entering multiple words all keywords must match. +* The filter is stored in localstorage of the webbrowser. On reload or the next week the last filter will be activated. +* A set of filter buttons by TLD will be generated. It is shown when minimum 2 TLDs were found. + ## Requirements * PHP 8.2 diff --git a/classes/shibd_discofeed.class.php b/classes/shibd_discofeed.class.php index 845846249692da2e06a612a2b2c6b96011074e56..a982228ce66278172e2c8d62499e2cfed8a0344c 100644 --- a/classes/shibd_discofeed.class.php +++ b/classes/shibd_discofeed.class.php @@ -1,17 +1,54 @@ <?php - +/** + * ====================================================================== + * + * AAI LOGIN WITH SHIBBOLETH HANDLING MULTIPLE ORGANIZATIONS + * + * included functions + * License: GNU GPL 3.0 + * Source: https://git-repo.iml.unibe.ch/iml-open-source/login-aai + * ====================================================================== + */ class shibd_discofeed { - protected string $url_list = '/Shibboleth.sso/DiscoFeed'; - protected string $url_login = '/Shibboleth.sso/Login'; + /** + * Url to the discofeed that returns a json with al idps + * @var string + */ + protected string $_sDiscofeedUrl = '/Shibboleth.sso/DiscoFeed'; + + /** + * Url to generate a static Shibboleth login url + * @var string + */ + protected string $_sShibLoginUrl = '/Shibboleth.sso/Login'; + /** + * Language to search for in the discofeed; a 2 letter code + * @var string + */ protected string $lang = 'en'; // caching of discofeed + + /** + * Filename of the cache file for the Shibboleth discofeed + * @var string + */ protected string $_sCachefile = 'discofeed.json'; + + /** + * Caching time for the discofeed cache in seconds + * @var int + */ protected int $_iCacheTtl; - protected array $aConfig = []; + /** + * Self base URL of the current app tu build Shibboleth links + * @var string + */ + protected string $SELFURL = ''; + // protected array $aConfig = []; // ---------------------------------------------------------------------- @@ -33,9 +70,10 @@ class shibd_discofeed if(!$SELFURL) { 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->_sDiscofeedUrl = "$SELFURL$this->_sDiscofeedUrl"; + $this->_sShibLoginUrl = "$SELFURL$this->_sShibLoginUrl"; $this->_sCachefile = dirname(__DIR__).'/' .($aConfig['cachefile']??'discofeed.json') @@ -56,7 +94,7 @@ class shibd_discofeed { 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); + $aReturn = json_decode(file_get_contents($this->_sDiscofeedUrl), 1); if ($aReturn && is_array($aReturn)) { // echo "DEBUG: IDP - storing cache<br>"; @@ -100,13 +138,15 @@ class shibd_discofeed $sImage = $aEntry['Logos'][1]['value'] ?? ($aEntry['Logos'][0]['value'] ?? ''); - $sUrl = $this->url_login + // see also https://help.switch.ch/aai/guides/discovery/login-link-composer/ + $sUrl = $this->_sShibLoginUrl . '?entityID='. urlencode($sEntityId) . "&target=" . urlencode($sEntityId) . "&target=" . urlencode($this->SELFURL.($aConfig['return-url']??'')) ; - $aReturn[] = array_merge([ + $sKey=$sLabel; + $aReturn[$sKey] = array_merge([ '_label' => $sLabel, '_description' => $sDescription, '_keywords' => $sKeywords, @@ -120,6 +160,7 @@ class shibd_discofeed } } } - return $aReturn; + ksort($aReturn); + return array_values($aReturn); } } \ No newline at end of file diff --git a/docs/images/login_mode_boxes.png b/docs/images/login_mode_boxes.png index 23ed0c93ec671500d961630e18b0b1790359ad0c..cf93cfe48b6ae2e08742a0e9d10d3da11a6cf6c6 100644 Binary files a/docs/images/login_mode_boxes.png and b/docs/images/login_mode_boxes.png differ diff --git a/functions.js b/functions.js index a10c998373670e2818081cda834abc482cc7196c..5740f00b44fbbce18b8208582da7efc9d55ec3f1 100644 --- a/functions.js +++ b/functions.js @@ -24,33 +24,62 @@ var sLsvar = 'aailogin-q'; /** * Apply filter and reduce listed Idps */ -function applyfilter(){ +function applyfilter() { - if(oFilter){ + if (oFilter) { var q = document.getElementById('filter').value; + var aQ = q.split(" "); - for(var i=0; i<document.getElementsByClassName('idp').length; i++){ + // reduce boxes based on filter using AND condition + for (var i = 0; i < document.getElementsByClassName('idp').length; i++) { var idp = document.getElementsByClassName('idp')[i]; - var bShow=(q=="" - ? true - // strip innerHTML and compare lowercase of it with lowercase of query - : idp.innerHTML.replace(/<[^>]*>/g, "").toLowerCase().indexOf(q.toLowerCase())>=0 - ); - + var bShow = true; + if (q != "") { + var sText = idp.innerHTML.replace(/<[^>]*>/g, ""); + for (var iPart = 0; iPart < aQ.length; iPart++) { + var qPart = aQ[iPart]; + bShow = bShow & sText.toLowerCase().indexOf(qPart.toLowerCase()) >= 0; + + } + } idp.className = bShow ? 'idp' : 'idp hide'; - document.getElementById('resetfilter').style.display = (q>"") ? 'inline': 'none'; + document.getElementById('resetfilter').style.display = (q > "") ? 'inline' : 'none'; + } + + var aBtns = document.getElementsByClassName('filterbutton'); + for (var i = 0; i < aBtns.length; i++) { + var bMarked = false; + for (var iPart = 0; iPart < aQ.length; iPart++) { + var qPart = aQ[iPart]; + var id2search = 'filterbtn-dot-' + qPart.replace(/^./, ''); + var qPart = aQ[iPart]; + if(aBtns[i].id == id2search){ + bMarked = true; + break; + } + } + aBtns[i].className = bMarked ? 'filterbutton active' : 'filterbutton'; } - localStorage.setItem(sLsvar,q); + + document.getElementById('filter').focus(); + localStorage.setItem(sLsvar, q); }; } -function setFilter(sNewFiltervalue){ - document.getElementById('filter').value=sNewFiltervalue; - applyfilter(); +/** + * Set a new filter value or reset it + * @param {string} sNewFiltervalue New value to write into the filter field + */ +function setFilter(sNewFiltervalue) { + document.getElementById('filter').value = sNewFiltervalue; + applyfilter(); } -function resetFilter(){ +/** + * Reset the filter and show all Items + */ +function resetFilter() { setFilter(""); } /** @@ -58,39 +87,24 @@ function resetFilter(){ * It renders an input field, reads the last search value from local storage * and apllies this filter */ -function showFilterBox(){ - if(oFilter){ - var q=localStorage.getItem(sLsvar); - if(!q){ - q=''; +function showFilterBox() { + if (oFilter) { + var q = localStorage.getItem(sLsvar); + 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+'"/>' - +'<a id="resetfilter" onclick="resetFilter(); return false;"> X </a><br>' - + (btnList ? '<br>'+btnList : '' ) + oFilter.innerHTML = '<input type="text" id="filter" placeholder="" onchange="applyfilter()" onkeydown="applyfilter()" onkeyup="applyfilter()" value=""/>' + + '<a id="resetfilter" onclick="resetFilter(); return false;"> X </a><br>' ; - applyfilter(); - for(var i=0; i<document.getElementsByClassName('idp').length; i++){ + setFilter(q); + + for (var i = 0; i < document.getElementsByClassName('idp').length; i++) { var idp = document.getElementsByClassName('idp')[i]; - idp.setAttribute("onclick","localStorage.setItem(sLsvar,this.innerText);"); + idp.setAttribute("onclick", "localStorage.setItem(sLsvar,this.innerText);"); } }; }; diff --git a/inc_functions.php b/inc_functions.php index 3ce963d45c38a09dc8a1f856cd208aa10ba22369..0152efb0c39abaf9897e48f590422237d520ab3f 100644 --- a/inc_functions.php +++ b/inc_functions.php @@ -16,7 +16,6 @@ require 'classes/shibd_discofeed.class.php'; - // get the user config if (!file_exists('config.php')) { die("ERROR: file config.php does not exist yet."); @@ -35,6 +34,7 @@ $aIdplist = $oDiscofeed->getIdps(); /** * Show a info or error message + * * @param string $sLevel level: one of "info", "error" * @param string $sMessage Message to show * @return void @@ -47,6 +47,8 @@ function showMessage(string $sLevel, string $sMessage) /** * Get a list of static links for browsers without javascript + * see also https://help.switch.ch/aai/guides/discovery/login-link-composer/ + * * @param array $aIdplist * @return string */ @@ -59,3 +61,4 @@ function getStaticlinks($aIdplist){ } +// ---------------------------------------------------------------------- diff --git a/inc_mode_boxes.php b/inc_mode_boxes.php index 0cf2051d675eb6e917dc7ed6424fef2db331a048..32ee556a2afad4e114590d1d29373b4eb2dfbab6 100644 --- a/inc_mode_boxes.php +++ b/inc_mode_boxes.php @@ -11,22 +11,41 @@ if (is_array($aIdplist) && count($aIdplist)) { $sOut = ''; - $sOut .= '<div id="filterbox"></div><div class="boxes">'; + + $sDomainFilter = ''; + $aTld = []; + foreach ($aIdplist as $aEntry) { + $aTld[$aEntry['_tld']] = 1; $sOut .= ' <a href="' . $aEntry['_url']. '" class="idp" - title="' . str_replace('"', '',$aEntry['_description']) . '" - >' . htmlentities($aEntry['_label']) . '<br> - <span class="hidden">' . str_replace('"', '', $aEntry['_description'] . ' ' . $aEntry['_keywords']) . ' .'. $aEntry['_tld'] . '</span> + title="' . strip_tags($aEntry['_description']) . '" + > + <span>' . htmlentities($aEntry['_label']) . '</span><br> + <span class="hidden"> .'. $aEntry['_tld'] . '</span> <img src="' . $aEntry['_image'] . '"><br> </a> ' - . "\n" ; } - $sOut.='</div><div style="clear:both"></div>'; - echo $sOut; + + // Generate a list of TLDs to filter by it. + if(count($aTld)>1) { + ksort($aTld); + foreach(array_keys($aTld) as $sTld) { + $sId='filterbtn-dot-'.str_replace('.', '', $sTld); + $sDomainFilter.='<a href="#" id="'.$sId.'" class="filterbutton" onclick="setFilter(\'.'.$sTld.' \'); return false;">.'.$sTld.'</a> '; + } + $sDomainFilter = '<div id="filterByDomain">' . $sDomainFilter . '</div>'; + } + + echo '<div id="filterbox"></div>' + . $sDomainFilter + .'<div class="boxes"> + '.$sOut.' + </div><div style="clear:both"></div>'; + } else { echo '<div class="msg error">No IDPs found in Discovery Feed.</div>'; } diff --git a/index.php b/index.php index 50463e30d9cd836b80e7d8cb331735ba4b136abe..ae5b490ab73697d220b536b9aa8b2c372f138b0c 100644 --- a/index.php +++ b/index.php @@ -59,7 +59,7 @@ </div> <script type="text/javascript" defer src="functions.js"></script> - <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> + <footer><strong><?php echo $aConfig['title'] ?? 'AAI Login'; ?></strong><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 5dfc3452daf754a0f45cdd7863265a224ab42bdd..101f392321569716b9069018791a78ac888ff0b5 100644 --- a/login_aai.css +++ b/login_aai.css @@ -28,8 +28,8 @@ --inputfilter-border: 2px solid #eee; --inputfilter-color:#789; - --btn-border: 2px solid #ddd; - --btn-bg:#eee; + --btn-border: 2px solid #eee; + --btn-bg:#f8f8f8; --btn-color: #888; --resetfilter-border: 2px solid #dcc; @@ -53,9 +53,11 @@ body { h1{color:var(--h1-color);} h2{color: var(--h2-color);} +div#head, div.content, footer { + width: 70%; +} div#head { margin: 1em auto 1em; - width: 60%; } div.content { background: var(--content-bg); @@ -64,7 +66,6 @@ div.content { box-shadow: var(--content-shadow); margin: 2em auto 2em; padding: 2em; - width: 60%; } div.msg{ border-radius: 1em; @@ -83,7 +84,6 @@ div.info{ } footer{ - width: 60%; color: var(--footer-color); margin: 10em auto 5em; text-align: right; @@ -94,7 +94,12 @@ footer{ div#filterbox{ margin-bottom: 1em; } -div#filterbox a.filterbutton, a#resetfilter{ +#filterByDomain{ + margin-top: 3em; + margin-bottom: 3em; +} + +a.filterbutton, a#resetfilter{ background: var(--btn-bg); border: var(--btn-border); border-radius: 0.2em; @@ -105,12 +110,16 @@ div#filterbox a.filterbutton, a#resetfilter{ padding: 0.5em; text-decoration: none; } +a.filterbutton.active{ + background: #fff; + border-top-color: #68a; +} input#filter{ border: var(--inputfilter-border); padding: 0.5em; color:var(--inputfilter-color); font-size: 130%; - width: 20em; + width: 70%; } a#resetfilter{ background:var(--resetfilter-bg); @@ -141,9 +150,9 @@ div.boxes a.idp{ text-align: center; text-decoration: none; transition: all 0.1s ease-in-out; - width: 10em; + width: 14em; } -div.boxes .idp img { height: 80px;} +div.boxes .idp img { height: 48px;} div.boxes a.idp:hover{ box-shadow: var(--idp-hover-shadow); }