diff --git a/Dockerfile b/Dockerfile index a47c16e77b0d959b02758b77382719b6767131e5..e5cb9d3e5e2bb2fe9ab9d15c0b4f4d992b0de164 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,8 @@ RUN apt-get -q update && \ apt-get -y autoremove && \ apt-get -y clean -RUN a2enmod rewrite +RUN a2enmod rewrite && \ + a2enmod shib #RUN git clone https://github.com/Edugate/Jagger /opt/rr3 && cd /opt/rr3 && git checkout v1.8.0 RUN git clone https://github.com/Edugate/Jagger /opt/rr3 @@ -25,6 +26,9 @@ RUN cd /opt/rr3 && bash install.sh && \ cd /opt/rr3/application/config && \ cp memcached-default.php memcached.php +COPY ./conf/etc/shibboleth/keygen.sh /etc/shibboleth/keygen.sh +COPY ./conf/etc/shibboleth-ds/ /etc/shibboleth-ds/ + COPY ./conf/etc/registry/application/config/config.php /opt/rr3/application/config/config.php COPY ./conf/etc/registry/application/config/config_rr.php /opt/rr3/application/config/config_rr.php COPY ./conf/etc/registry/application/config/database.php /opt/rr3/application/config/database.php diff --git a/conf/etc/apache2/sites-available/000-default.conf b/conf/etc/apache2/sites-available/000-default.conf index 814361ccd5dea3549148ad2d0bacf3103c12411f..139285e82b1db66f1cf4eb783de797c907a93bb3 100644 --- a/conf/etc/apache2/sites-available/000-default.conf +++ b/conf/etc/apache2/sites-available/000-default.conf @@ -9,19 +9,19 @@ #ServerName www.example.com ServerAdmin webmaster@localhost - DocumentRoot /opt/rr3 + DocumentRoot /opt/rr3/ ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined - Alias /rr3 /opt/rr3 - <Directory /opt/rr3> + Alias / /opt/rr3/ + <Directory /opt/rr3/> Require all granted RewriteEngine On - RewriteBase /rr3 + RewriteBase / RewriteCond $1 !^(Shibboleth\.sso|index\.php|logos|signedmetadata|flags|images|app|schemas|fonts|styles|images|js|robots\.txt|pub|includes) - RewriteRule ^(.*)$ /rr3/index.php?/$1 [L] + RewriteRule ^(.*)$ /index.php?/$1 [L] </Directory> <Directory /opt/rr3/application> @@ -34,6 +34,24 @@ require valid-user </Location> + <Location /shibboleth-ds> + Require all granted + <IfModule mod_shib.c> + AuthType shibboleth + ShibRequestSetting requireSession false + require shibboleth + </IfModule> + </Location> + <Directory /etc/shibboleth-ds/> + Require all granted + </Directory> + + Alias /shibboleth-ds/idpselect_config.js /etc/shibboleth-ds/idpselect_config.js + Alias /shibboleth-ds/idpselect.js /etc/shibboleth-ds/idpselect.js + Alias /shibboleth-ds/idpselect.css /etc/shibboleth-ds/idpselect.css + Alias /shibboleth-ds/index.html /etc/shibboleth-ds/index.html + Alias /shibboleth-ds/blank.gif /etc/shibboleth-ds/blank.gif + # For most configuration files from conf-available/, which are # enabled or disabled at a global level, it is possible to # include a line for only one particular virtual host. For example the diff --git a/conf/etc/entrypoint b/conf/etc/entrypoint index d96b1a5790a432e47b2c98b006a2f97c41e553d6..76eef498813f2242d291a11ff9d04d44a0386490 100755 --- a/conf/etc/entrypoint +++ b/conf/etc/entrypoint @@ -45,4 +45,9 @@ cd /opt/rr3/application ./doctrine orm:generate-proxies +cd /etc/shibboleth/ +./keygen.sh -n "sp-signing" -h ${FAAS_REGISTRY_HOSTNAME} || echo "file exists" +cp /etc/shibboleth/sp-signing-cert.pem /etc/shibboleth/sp-encrypt-cert.pem +cp /etc/shibboleth/sp-signing-key.pem /etc/shibboleth/sp-encrypt-key.pem + exec supervisord -c /etc/supervisord.conf diff --git a/conf/etc/shibboleth-ds/LICENSE.txt b/conf/etc/shibboleth-ds/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd5b3a58aa1849f452abc9b5cd1638dc71a5e482 --- /dev/null +++ b/conf/etc/shibboleth-ds/LICENSE.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/conf/etc/shibboleth-ds/blank.gif b/conf/etc/shibboleth-ds/blank.gif new file mode 100644 index 0000000000000000000000000000000000000000..2799b45c6591f1db05c8c00bd1fd0c5c01f57614 Binary files /dev/null and b/conf/etc/shibboleth-ds/blank.gif differ diff --git a/conf/etc/shibboleth-ds/idpselect.css b/conf/etc/shibboleth-ds/idpselect.css new file mode 100644 index 0000000000000000000000000000000000000000..9b480b4bd15be76734866edb03d138adbd335577 --- /dev/null +++ b/conf/etc/shibboleth-ds/idpselect.css @@ -0,0 +1,213 @@ +/* Top level is idpSelectIdPSelector */ +#idpSelectIdPSelector +{ + width: 389px; + text-align: left; + background-color: #FFFFFF; + border: 2px #A40000 solid; + padding: 10px; +} + +/* Next down are the idpSelectPreferredIdPTile, idpSelectIdPEntryTile & idpSelectIdPListTile */ + +/** + * The preferred IdP tile (if present) has a specified height, so + * we can fit the preselected * IdPs in there + */ +#idpSelectPreferredIdPTile +{ + height:138px; /* Force the height so that the selector box + * goes below when there is only one preslect + */ +} +#idpSelectPreferredIdPTileNoImg +{ + height:60px; +} + +/*** + * The preselect buttons + */ +div.IdPSelectPreferredIdPButton +{ + margin: 3px; + width: 120px; /* Make absolute because 3 of these must fit inside + div.IdPSelect{width} with not much each side. */ + float: left; +} + +/* + * Make the entire box look like a hyperlink + */ +div.IdPSelectPreferredIdPButton a +{ + float: left; + width: 99%; /* Need a specified width otherwise we'll fit + the contents which we don't want because + they have auto margins */ + +} +div.IdPSelectTextDiv{ + height: 3.5ex; /* Add some height to separate the text from the boxes */ + font-size: 15px; + clear: left; +} + +div.IdPSelectPreferredIdPImg +{ +/* max-width: 95%; */ + height: 69px; /* We need the absolute height to force all buttons to the same size */ + margin: 2px; +} + +img.IdPSelectIdPImg { + width:auto; +} + +div.IdPSelectautoDispatchTile { + display: block; +} + +div.IdPSelectautoDispatchArea { + margin-top: 30px ; +} + +div.IdPSelectPreferredIdPButton img +{ + display: block; /* Block display to allow auto centring */ + max-width: 114px; /* Specify max to allow scaling, percent does work */ + max-height: 64px; /* Specify max to allow scaling, percent doesn't work */ + margin-top: 3px ; + margin-bottom: 3px ; + border: solid 0px #000000; /* Strip any embellishments the brower may give us */ + margin-left: auto; /* Auto centring */ + margin-right: auto; /* Auto centring */ + +} + +div.IdPSelectPreferredIdPButton div.IdPSelectTextDiv +{ + text-align: center; + font-size: 12px; + font-weight: normal; + max-width: 95%; + height: 30px; /* Specify max height to allow two lines. The + * Javascript controlls the max length of the + * strings + */ +} + +/* + * Force the size of the selectors and the buttons + */ +#idpSelectInput, #idpSelectSelector +{ + width: 80%; +} +/* + * For some reason a <select> width includes the border and an + * <input> doesn't hence we have to force a margin onto the <select> + */ +#idpSelectSelector +{ + margin-left: 2px; + margin-right: 2px; + +} +#idpSelectSelectButton, #idpSelectListButton +{ + margin-left: 5px; + width: 16%; +} +#idpSelectSelectButton +{ + padding-left: 2px; + padding-right: 2px; +} + +/* + * change underlining of HREFS + */ +#idpSelectIdPSelector a:link +{ + text-decoration: none; +} + +#idpSelectIdPSelector a:visited +{ + text-decoration: none; +} + +#idpSelectIdPSelector a:hover +{ + text-decoration: underline; +} + + + +/* + * Arrange to have the dropdown/list aref on the left and the + * help button on the right + */ + +a.IdPSelectDropDownToggle +{ + display: inline-block; + width: 80%; +} + +a.IdPSelectHelpButton +{ + display: inline-block; + text-align: right; + width: 20%; +} + +/** + * Drop down (incremental search) stuff - see the associated javascript for reference + */ +ul.IdPSelectDropDown { + -moz-box-sizing: border-box; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: small; + box-sizing: border-box; + list-style: none; + padding-left: 0px; + border: 1px solid black; + z-index: 6; + position: absolute; +} + +ul.IdPSelectDropDown li { + background-color: white; + cursor: default; + padding: 0px 3px; +} + +ul.IdPSelectDropDown li.IdPSelectCurrent { + background-color: #3366cc; + color: white; +} + +/* Legacy */ +div.IdPSelectDropDown { + -moz-box-sizing: border-box; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: small; + box-sizing: border-box; + border: 1px solid black; + z-index: 6; + position: absolute; +} + +div.IdPSelectDropDown div { + background-color: white; + cursor: default; + padding: 0px 3px; +} + + div.IdPSelectDropDown div.IdPSelectCurrent { + background-color: #3366cc; + color: white; +} +/* END */ diff --git a/conf/etc/shibboleth-ds/idpselect.js b/conf/etc/shibboleth-ds/idpselect.js new file mode 100644 index 0000000000000000000000000000000000000000..b9c191b26a6598585173712a072340b8f3303ab9 --- /dev/null +++ b/conf/etc/shibboleth-ds/idpselect.js @@ -0,0 +1 @@ +function IdPSelectLanguages(){this.langBundles={en:{"fatal.divMissing":'<div> specified as "insertAtDiv" could not be located in the HTML',"fatal.noXMLHttpRequest":"Browser does not support XMLHttpRequest, unable to load IdP selection data","fatal.wrongProtocol":'Policy supplied to DS was not "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"',"fatal.wrongEntityId":"entityId supplied by SP did not match configuration","fatal.noData":"Metadata download returned no data","fatal.loadFailed":"Failed to download metadata from ","fatal.noparms":"No parameters to discovery session and no defaultReturn parameter configured","fatal.noReturnURL":"No URL return parameter provided","fatal.badProtocol":"Return parameter must start with https:// or http://","fatal.badReturnString":"Return parameter is not whitelisted","idpPreferred.label":"Use a suggested selection:","idpEntry.label":"Or enter your organization's name","idpEntry.NoPreferred.label":"Enter your organization's name","idpList.label":"Or select your organization from the list below","idpList.NoPreferred.label":"Select your organization from the list below","idpList.defaultOptionLabel":"Please select your organization...","idpList.showList":"Allow me to pick from a list","idpList.showSearch":"Allow me to specify the site","submitButton.label":"Continue",helpText:"Help",defaultLogoAlt:"DefaultLogo","autoFollow.message":"Always follows this selection","autoFollow.never":"Never","autoFollow.time0":"One day","autoFollow.time1":"3 months","autoFollow.time2":"9 months"},de:{"fatal.divMissing":"Das notwendige Div Element fehlt","fatal.noXMLHttpRequest":"Ihr Webbrowser unterst\u00fctzt keine XMLHttpRequests, IdP-Auswahl kann nicht geladen werden","fatal.wrongProtocol":'DS bekam eine andere Policy als "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"',"fatal.wrongEntityId":"Die entityId ist nicht korrekt","fatal.noData":"Heruntergeladene Metadata waren leer","fatal.loadFailed":"Metadaten konnten nicht heruntergeladen werden: ","fatal.noparms":"Parameter f\u00fcr das Discovery Service oder 'defaultReturn' fehlen","fatal.noReturnURL":"URL return Parmeter fehlt","fatal.badProtocol":"return Request muss mit https:// oder http:// beginnen","fatal.badReturnString":"Return Parameter ist nicht auf Positivliste enthalten","idpPreferred.label":"Vorherige Auswahl:","idpEntry.label":"Oder geben Sie den Namen (oder Teile davon) an:","idpEntry.NoPreferred.label":"Namen (oder Teile davon) der Institution angeben:","idpList.label":"Oder w\u00e4hlen Sie Ihre Institution aus einer Liste:","idpList.NoPreferred.label":"Institution aus folgender Liste w\u00e4hlen:","idpList.defaultOptionLabel":"W\u00e4hlen Sie Ihre Institution aus...","idpList.showList":"Institution aus einer Liste w\u00e4hlen","idpList.showSearch":"Institution selbst angeben","submitButton.label":"OK",helpText:"Hilfe",defaultLogoAlt:"Standard logo","autoFollow.message":"Auswahl merken und diesen Dialog nicht mehr anzeigen","autoFollow.never":"Nicht merken","autoFollow.time0":"Nur f\u00fcr Heute","autoFollow.time1":"Drei Monate lang","autoFollow.time2":"Neun Monate lang"},ja:{"fatal.divMissing":'"insertAtDiv" の ID を持つ <div> が HTML 中に存在しません',"fatal.noXMLHttpRequest":"ブラウザが XMLHttpRequest をサポートしていないので IdP 情報を取得できません","fatal.wrongProtocol":'DSへ渡された Policy パラメータが "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single" ではありません',"fatal.wrongEntityId":"SP から渡された entityId が設定値と異なります","fatal.noData":"メタデータが空です","fatal.loadFailed":"次の URL からメタデータをダウンロードできませんでした: ","fatal.noparms":"DSにパラメータが渡されておらず defaultReturn も設定されていません","fatal.noReturnURL":"戻り URL が指定されていません","fatal.badProtocol":"戻り URL は https:// か http:// で始まらなければなりません","idpPreferred.label":"選択候補の IdP:","idpEntry.label":"もしくはあなたの所属機関名を入力してください","idpEntry.NoPreferred.label":"あなたの所属機関名を入力してください","idpList.label":"もしくはあなたの所属機関を選択してください","idpList.NoPreferred.label":"あなたの所属機関を一覧から選択してください","idpList.defaultOptionLabel":"所属機関を選択してください...","idpList.showList":"一覧から選択する","idpList.showSearch":"機関名を入力する","submitButton.label":"選択","autoFollow.message":"次の期間選択した機関に自動的に遷移する:","autoFollow.never":"自動遷移しない","autoFollow.time0":"1日","autoFollow.time1":"3か月","autoFollow.time2":"9か月","fatal.badReturnString":"戻り URL が不正です",helpText:"ヘルプ",defaultLogoAlt:"ロゴ未設定"},"pt-br":{"fatal.divMissing":'A tag <div> com "insertAtDiv" não foi encontrada no arquivo HTML',"fatal.noXMLHttpRequest":'Seu navegador não suporta "XMLHttpRequest", impossível de carregador os dados do IdP selecionado',"fatal.wrongProtocol":'A política "Policy" fornecida para o DS não foi "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"',"fatal.wrongEntityId":"entityId oferecido pelo SP não confere com o da configuração","fatal.noData":"O arquivo de metadados não retornou nada;","fatal.loadFailed":"Falhou ao realizar download do metadado de ","fatal.noparms":'Sem parâmetros para sessão de descoberta e sem parâmetro "defaultReturn" configurado',"fatal.noReturnURL":"Não foi definida um endereço (URL) de retorno no parâmetro","fatal.badProtocol":"Retorno do endereço requisitado deve começar com https:// ou http://","idpPreferred.label":"Use estas Instituições sugeridas: ","idpEntry.label":"Ou informe o nome da sua Instituição","idpEntry.NoPreferred.label":"Informe o nome da sua Instituição","idpList.label":"Ou selecione sua Instituição através da lista abaixo","idpList.NoPreferred.label":"Selecione sua Instituição através da lista abaixo","idpList.defaultOptionLabel":"Por favor, selecione sua Instituição: ","idpList.showList":"Permitir que eu escolha um IdP através de uma lista","idpList.showSearch":"Permitir que eu especifique o IdP","submitButton.label":"Continuar ",helpText:"Ajuda",defaultLogoAlt:"Logo padrão"}}}function TypeAheadControl(l,f,j,g,i,b,h,e,a,c,d,k){this.elementList=l;this.textBox=f;this.origin=j;this.submit=g;this.results=0;this.alwaysShow=c;this.maxResults=d;this.ie6hack=a;this.maxchars=i;this.getName=b;this.getEntityId=h;this.geticon=e;this.getKeywords=k}TypeAheadControl.prototype.draw=function(b){var a=this;this.dropDown=document.createElement("ul");this.dropDown.className="IdPSelectDropDown";this.dropDown.style.visibility="hidden";this.dropDown.style.width=this.textBox.offsetWidth;this.dropDown.current=-1;this.textBox.setAttribute("role","listbox");document.body.appendChild(this.dropDown);this.textBox.setAttribute("role","combobox");this.textBox.setAttribute("aria-controls","IdPSelectDropDown");this.textBox.setAttribute("aria-owns","IdPSelectDropDown");this.dropDown.onmouseover=function(c){if(!c){c=window.event}var d;if(c.target){d=c.target}if(typeof d=="undefined"){d=c.srcElement}a.select(d)};this.dropDown.onmousedown=function(c){if(-1!=a.dropDown.current){a.textBox.value=a.results[a.dropDown.current][0]}};this.textBox.onkeyup=function(c){if(!c){c=window.event}a.handleKeyUp(c)};this.textBox.onkeydown=function(c){if(!c){c=window.event}a.handleKeyDown(c)};this.textBox.onblur=function(){a.hideDrop()};this.textBox.onfocus=function(){a.handleChange()};if(null==b||b){this.textBox.focus()}};TypeAheadControl.prototype.getPossible=function(b){var h=[];var j=0;var f=0;var e=0;var g;var i;b=b.toLowerCase();while(f<=this.maxResults&&j<this.elementList.length){var a=false;var c=this.getName(this.elementList[j]);if(c.toLowerCase().indexOf(b)!=-1){a=true}if(!a&&this.getEntityId(this.elementList[j]).toLowerCase().indexOf(b)!=-1){a=true}if(!a){var d=this.getKeywords(this.elementList[j]);if(null!=d&&d.toLowerCase().indexOf(b)!=-1){a=true}}if(a){h[f]=[c,this.getEntityId(this.elementList[j]),this.geticon(this.elementList[j])];f++}j++}this.dropDown.current=-1;return h};TypeAheadControl.prototype.handleKeyUp=function(b){var a=b.keyCode;if(27==a){this.textBox.value="";this.handleChange()}else{if(8==a||32==a||(a>=46&&a<112)||a>123){this.handleChange()}}};TypeAheadControl.prototype.handleKeyDown=function(b){var a=b.keyCode;if(38==a){this.upSelect()}else{if(40==a){this.downSelect()}}};TypeAheadControl.prototype.hideDrop=function(){var a=0;if(null!==this.ie6hack){while(a<this.ie6hack.length){this.ie6hack[a].style.visibility="visible";a++}}this.dropDown.style.visibility="hidden";this.textBox.setAttribute("aria-expanded","false");if(-1==this.dropDown.current){this.doUnselected()}};TypeAheadControl.prototype.showDrop=function(){var a=0;if(null!==this.ie6hack){while(a<this.ie6hack.length){this.ie6hack[a].style.visibility="hidden";a++}}this.dropDown.style.visibility="visible";this.dropDown.style.width=this.textBox.offsetWidth+"px";this.textBox.setAttribute("aria-expanded","true")};TypeAheadControl.prototype.doSelected=function(){this.submit.disabled=false};TypeAheadControl.prototype.doUnselected=function(){this.submit.disabled=true;this.textBox.setAttribute("aria-activedescendant","")};TypeAheadControl.prototype.handleChange=function(){var b=this.textBox.value;var a=this.getPossible(b);if(0===b.length||0===a.length||(!this.alwaysShow&&this.maxResults<a.length)){this.hideDrop();this.doUnselected();this.results=[];this.dropDown.current=-1}else{this.results=a;this.populateDropDown(a);if(1==a.length){this.select(this.dropDown.childNodes[0]);this.doSelected()}else{this.doUnselected()}}};TypeAheadControl.prototype.populateDropDown=function(d){this.dropDown.innerHTML="";var c=0;var a;var b;var f;while(c<d.length){a=document.createElement("li");a.id="IdPSelectOption"+c;f=d[c][0];if(null!==d[c][2]){b=document.createElement("img");b.src=d[c][2];b.width=16;b.height=16;b.alt="";a.appendChild(b);if(f.length>this.maxchars-2){f=f.substring(0,this.maxchars-2)}f=" "+f}else{if(f.length>this.maxchars){f=f.substring(0,this.maxchars)}}a.appendChild(document.createTextNode(f));a.setAttribute("role","option");this.dropDown.appendChild(a);c++}var e=this.getXY();this.dropDown.style.left=e[0]+"px";this.dropDown.style.top=e[1]+"px";this.showDrop()};TypeAheadControl.prototype.getXY=function(){var a=this.textBox;var c=0;var b=a.offsetHeight;while(a.tagName!="BODY"){c+=a.offsetLeft;b+=a.offsetTop;a=a.offsetParent}c+=a.offsetLeft;b+=a.offsetTop;return[c,b]};TypeAheadControl.prototype.select=function(b){var a=0;var c;this.dropDown.current=-1;this.doUnselected();while(a<this.dropDown.childNodes.length){c=this.dropDown.childNodes[a];if(c==b){c.className="IdPSelectCurrent";c.setAttribute("aria-selected","true");this.textBox.setAttribute("aria-activedescendant","IdPSelectOption"+a);this.doSelected();this.dropDown.current=a;this.origin.value=this.results[a][1];this.origin.textValue=this.results[a][0]}else{c.setAttribute("aria-selected","false");c.className=""}a++}this.textBox.focus()};TypeAheadControl.prototype.downSelect=function(){if(this.results.length>0){if(-1==this.dropDown.current){this.dropDown.current=0;this.dropDown.childNodes[0].className="IdPSelectCurrent";this.dropDown.childNodes[0].setAttribute("aria-selected","true");this.textBox.setAttribute("aria-activedescendant","IdPSelectOption"+0);this.doSelected();this.origin.value=this.results[0][1];this.origin.textValue=this.results[0][0]}else{if(this.dropDown.current<(this.results.length-1)){this.dropDown.childNodes[this.dropDown.current].className="";this.dropDown.current++;this.dropDown.childNodes[this.dropDown.current].className="IdPSelectCurrent";this.dropDown.childNodes[this.dropDown.current].setAttribute("aria-selected","true");this.textBox.setAttribute("aria-activedescendant","IdPSelectOption"+this.dropDown.current);this.doSelected();this.origin.value=this.results[this.dropDown.current][1];this.origin.textValue=this.results[this.dropDown.current][0]}}}};TypeAheadControl.prototype.upSelect=function(){if((this.results.length>0)&&(this.dropDown.current>0)){this.dropDown.childNodes[this.dropDown.current].className="";this.dropDown.current--;this.dropDown.childNodes[this.dropDown.current].className="IdPSelectCurrent";this.dropDown.childNodes[this.dropDown.current].setAttribute("aria-selected","true");this.textBox.setAttribute("aria-activedescendant","IdPSelectOption"+this.dropDown.current);this.doSelected();this.origin.value=this.results[this.dropDown.current][1];this.origin.textValue=this.results[this.dropDown.current][0]}};function IdPSelectUI(){var r;var W="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var aJ;var S;var aB;var an;var aa;var d;var F;var n;var y;var k;var aw;var g;var t;var ab;var ah;var D;var ag;var R;var h;var K;var au;var N;var T;var al;var ax;var aq;var c;var af;var G;var Q;var Z;var P;var U;var j;var az;var V;var aL="idpSelect";var aj="IdPSelect";var ar;var B="";var Y="";var aA=[];var aG="entityID";this.draw=function(aP){aJ=document.getElementById(aP.insertAtDiv);if(!m(aP)){return}if(!aJ){O(A("fatal.divMissing"));return}if((null!=Q)&&(null!=i(Q))){var aM=b();if(aM.length!=0){var aO=aG+"="+encodeURIComponent(aM[0]);if(B.indexOf("?")==-1){aO="?"+aO}else{aO="&"+aO}q(aJ,B+aO);return}}if(!f(aP.dataSource)){return}aK();u(aP.hiddenIdPs);r.sort(function(aR,aQ){return aE(aR).localeCompare(aE(aQ))});var aN=ac();aJ.appendChild(aN);ar.draw(aP.setFocusTextBox)};var m=function(aT){var aN;D=aT.preferredIdP;ag=aT.maxPreferredIdPs;R=aT.helpURL;h=aT.ie6Hack;K=aT.samlIdPCookieTTL;al=aT.alwaysShow;ax=aT.maxResults;aq=aT.ignoreKeywords;if(aT.showListFirst){c=aT.showListFirst}else{c=false}if(aT.noWriteCookie){af=aT.noWriteCookie}else{af=false}if(aT.ignoreURLParams){G=aT.ignoreURLParams}else{G=false}F=aT.defaultLogo;n=aT.defaultLogoWidth;y=aT.defaultLogoHeight;k=aT.minWidth;aw=aT.minHeight;g=aT.maxWidth;t=aT.maxHeight;ab=aT.bestRatio;if(null==aT.doNotCollapse){ah=true}else{ah=aT.doNotCollapse}N=aT.maxIdPCharsButton;au=aT.maxIdPCharsDropDown;T=aT.maxIdPCharsAltTxt;Q=aT.autoFollowCookie;Z=aT.autoFollowCookieTTLs;var a3;if(typeof navigator=="undefined"){a3=aT.defaultLanguage}else{a3=navigator.language||navigator.userLanguage||aT.defaultLanguage}a3=a3.toLowerCase();if(a3.indexOf("-")>0){aB=a3.substring(0,a3.indexOf("-"))}var aX=new IdPSelectLanguages();an=aT.defaultLanguage;if(typeof aT.langBundles!="undefined"&&typeof aT.langBundles[a3]!="undefined"){aa=aT.langBundles[a3]}else{if(typeof aX.langBundles[a3]!="undefined"){aa=aX.langBundles[a3]}else{if(typeof aB!="undefined"){if(typeof aT.langBundles!="undefined"&&typeof aT.langBundles[aB]!="undefined"){aa=aT.langBundles[aB]}else{if(typeof aX.langBundles[aB]!="undefined"){aa=aX.langBundles[aB]}}}}}if(typeof aT.langBundles!="undefined"&&typeof aT.langBundles[aT.defaultLanguage]!="undefined"){d=aT.langBundles[aT.defaultLanguage]}else{d=aX.langBundles[aT.defaultLanguage]}if(!d){O("No languages work");return false}if(!aa){s("No language support for "+a3);aa=d}if(aT.testGUI){return true}var aY="urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single";var a1;var a2=false;var aR;var aU;var aQ=window;while(null!==aQ.parent&&aQ!==aQ.parent){aQ=aQ.parent}var aW=aQ.location;var aS=aW.search;if(G||null==aS||0==aS.length||aS.charAt(0)!="?"){if((null==aT.defaultReturn)&&!G){O(A("fatal.noparms"));return false}aN=aT.myEntityID;B=aT.defaultReturn;if(null!=aT.defaultReturnIDParam){aG=aT.defaultReturnIDParam}}else{aS=aS.substring(1);aR=aS.split("&");if(aR.length===0){O(A("fatal.noparms"));return false}for(a1=0;a1<aR.length;a1++){aU=aR[a1].split("=");if(aU.length!=2){continue}if(aU[0]=="entityID"){aN=decodeURIComponent(aU[1])}else{if(aU[0]=="return"){B=decodeURIComponent(aU[1])}else{if(aU[0]=="returnIDParam"){aG=decodeURIComponent(aU[1])}else{if(aU[0]=="policy"){aY=decodeURIComponent(aU[1])}else{if(aU[0]=="isPassive"){a2=(aU[1].toUpperCase()=="TRUE")}}}}}}}var aM;if(null==aT.allowableProtocols){aM=["urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"]}else{aM=aT.allowableProtocols}var a0=false;for(var a1=0;a1<aM.length;a1++){var aZ=aM[a1];if(aY==aZ){a0=true;break}}if(!a0){O(A("fatal.wrongProtocol"));return false}if(aT.myEntityID!==null&&aT.myEntityID!=aN){O(A("fatal.wrongEntityId")+'"'+aN+'" != "'+aT.myEntityID+'"');return false}if(null===B||B.length===0){O(A("fatal.noReturnURL"));return false}if(!ao(B)){O(A("fatal.badProtocol"));return false}if(!e(aT.returnWhiteList,B)){ae(A("fatal.badReturnString"));return false}if(a2){var aP=b();var aV=document.getElementById(parmsSupplied.insertAtDiv);if(aP.length==0){q(aV,B);return false}else{var aO=aG+"="+encodeURIComponent(aP[0]);if(B.indexOf("?")==-1){aO="?"+aO}else{aO="&"+aO}q(aV,B+aO);return false}}a1=B.indexOf("?");if(a1<0){Y=B;return true}Y=B.substring(0,a1);aS=B.substring(a1+1);aR=aS.split("&");for(a1=0;a1<aR.length;a1++){aU=aR[a1].split("=");if(aU.length!=2){continue}aU[1]=decodeURIComponent(aU[1]);aA.push(aU)}return true};var aK=function(){var aO=[];var aN;for(aN=0;aN<r.length;){var aM=z(r[aN]);if(null==aO[aM]){aO[aM]=aM;aN=aN+1}else{r.splice(aN,1)}}};var u=function(aO){if(null==aO||0==aO.length){return}var aN;var aM;for(aN=0;aN<aO.length;aN++){for(aM=0;aM<r.length;aM++){if(z(r[aM])==aO[aN]){r.splice(aM,1);break}}}};var ao=function(aN){if(null===aN){return false}var aM="://";var aO=aN.indexOf(aM);if(aO<0){return false}aN=aN.substring(0,aO);if(aN=="http"||aN=="https"){return true}return false};var e=function(aM,aN){if(null==aM){return true}for(var aO=0;aO<aM.length;aO++){var aP=new RegExp(aM[aO]);if(aP.test(aN)){return true}}return false};var aI=function(){if(null==navigator){return false}var aM=navigator.appName;if(null==aM){return false}return(aM=="Microsoft Internet Explorer")};var q=function(aN,aO){var aM=document.createElement("a");aM.href=aO;aN.appendChild(aM);aM.click()};var f=function(aP){var aO=null;try{aO=new XMLHttpRequest()}catch(aN){}if(null==aO){try{aO=new ActiveXObject("Microsoft.XMLHTTP")}catch(aN){}}if(null==aO){try{aO=new ActiveXObject("MSXML2.XMLHTTP.3.0")}catch(aN){}}if(null==aO){O(A("fatal.noXMLHttpRequest"));return false}if(aI()){aP+="?random="+(Math.random()*1000000)}aO.open("GET",aP,false);if(typeof aO.overrideMimeType=="function"){aO.overrideMimeType("application/json")}aO.send(null);if(aO.status==200){var aM=aO.responseText;if(aM===null){O(A("fatal.noData"));return false}r=JSON.parse(aM)}else{O(A("fatal.loadFailed")+aP);return false}return true};var ad=function(aM){for(var aN=0;aN<r.length;aN++){if(z(r[aN])==aM){return r[aN]}}return null};var H=function(aT,aN){var aS=function(aW){var aU=null;var aV;if(null==aT.Logos){return null}for(aV in aT.Logos){if(aT.Logos[aV].lang==aW&&aT.Logos[aV].width!=null&&aT.Logos[aV].width>=k&&aT.Logos[aV].height!=null&&aT.Logos[aV].height>=aw){if(aU===null){aU=aT.Logos[aV]}else{me=Math.abs(ab-Math.log(aT.Logos[aV].width/aT.Logos[aV].height));him=Math.abs(ab-Math.log(aU.width/aU.height));if(him>me){aU=aT.Logos[aV]}}}}return aU};var aP=null;var aO=document.createElement("img");am(aO,"IdPImg");aP=aS(S);if(null===aP&&typeof aB!="undefined"){aP=aS(aB)}if(null===aP){aP=aS(null)}if(null===aP){aP=aS(an)}if(null===aP){if(!aN){return null}aO.src=F;aO.width=n;aO.height=y;aO.alt=A("defaultLogoAlt");return aO}aO.src=aP.value;var aQ=aE(aT);if(aQ.length>T){aQ=aQ.substring(0,T)+"..."}aO.alt=aQ;var aM=aP.width;var aR=aP.height;if(aM>g){aR=(g/aM)*aR;aM=g}if(aR>t){aM=(t/aR)*aM;aR=t}aO.setAttribute("width",aM);aO.setAttribute("height",aR);return aO};var ac=function(){var aN=ap("IdPSelector");var aM;aM=aC(aN);o(aN,aM);X(aN,aM);if(null!=Q){C(aN)}return aN};var M=function(aO,aV,aN){var aM=ap(undefined,"PreferredIdPButton");var aU=document.createElement("a");var aT=aG+"="+encodeURIComponent(z(aO));var aP=B;var aR=H(aO,aN);if(aP.indexOf("?")==-1){aT="?"+aT}else{aT="&"+aT}aU.href=aP+aT;aU.onclick=function(){aH(z(aO))};if(null!=aR){var aW=ap(undefined,"PreferredIdPImg");aW.appendChild(aR);aU.appendChild(aW)}var aS=ap(undefined,"TextDiv");var aQ=aE(aO);if(aQ.length>N){aQ=aQ.substring(0,N)+"..."}aM.title=aQ;aS.appendChild(document.createTextNode(aQ));aU.appendChild(aS);aM.appendChild(aU);return aM};var aF=function(aM,aP){var aO=ap(undefined,"TextDiv");var aN=document.createTextNode(A(aP));aO.appendChild(aN);aM.appendChild(aO)};var a=function(aM,aO){if(null===aO||0===aO.length||"-"==aO.value){return}var aN=0;while(aN<aM.options.length){if(aM.options[aN].value==aO){aM.options[aN].selected=true;break}aN++}};var aC=function(aR){var aQ=L();if(0===aQ.length){return false}var aM=ah;for(var aO=0;aO<ag&&aO<aQ.length;aO++){if(aQ[aO]&&H(aQ[aO],false)){aM=true}}var aP;if(aM){aP=ap("PreferredIdPTile")}else{aP=ap("PreferredIdPTileNoImg")}aF(aP,"idpPreferred.label");for(var aO=0;aO<ag&&aO<aQ.length;aO++){if(aQ[aO]){var aN=M(aQ[aO],aO,aM);aP.appendChild(aN)}}aR.appendChild(aP);return true};var ai=function(){var aN=document.createElement("form");U.appendChild(aN);aN.action=Y;aN.method="GET";aN.setAttribute("autocomplete","OFF");var aM=0;for(aM=0;aM<aA.length;aM++){var aO=document.createElement("input");aO.setAttribute("type","hidden");aO.name=aA[aM][0];aO.value=aA[aM][1];aN.appendChild(aO)}return aN};var o=function(aT,aN){U=ap("IdPEntryTile");if(c){U.style.display="none"}var aO=document.createElement("label");aO.setAttribute("for",aL+"Input");if(aN){aF(aO,"idpEntry.label")}else{aF(aO,"idpEntry.NoPreferred.label")}var aR=ai();aR.appendChild(aO);var aQ=document.createElement("input");aR.appendChild(aQ);aQ.type="text";l(aQ,"Input");var aS=document.createElement("input");aS.setAttribute("type","hidden");aR.appendChild(aS);aS.name=aG;aS.value="-";var aP=v("Select");aP.disabled=true;aR.appendChild(aP);aR.onsubmit=function(){if(null===aS.value||0===aS.value.length||"-"==aS.value){return false}aQ.value=aS.textValue;aH(aS.value);return true};ar=new TypeAheadControl(r,aQ,aS,aP,au,aE,z,ak,h,al,ax,I);var aM=document.createElement("a");aM.appendChild(document.createTextNode(A("idpList.showList")));aM.href="#";am(aM,"DropDownToggle");aM.onclick=function(){U.style.display="none";a(az,aS.value);j.style.display="";V.focus();return false};U.appendChild(aM);x(U);aT.appendChild(U)};var X=function(aM,aP){j=ap("IdPListTile");if(!c){j.style.display="none"}var aT=document.createElement("label");aT.setAttribute("for",aL+"Selector");if(aP){aF(aT,"idpList.label")}else{aF(aT,"idpList.NoPreferred.label")}az=document.createElement("select");l(az,"Selector");az.name=aG;j.appendChild(az);var aU=p("-",A("idpList.defaultOptionLabel"));aU.selected=true;az.appendChild(aU);var aO;for(var aQ=0;aQ<r.length;aQ++){aO=r[aQ];aU=p(z(aO),aE(aO));az.appendChild(aU)}var aN=ai();aN.appendChild(aT);aN.appendChild(az);aN.onsubmit=function(){if(az.selectedIndex<1){return false}aH(az.options[az.selectedIndex].value);return true};var aR=v("List");V=aR;aN.appendChild(aR);j.appendChild(aN);var aS=document.createElement("a");aS.appendChild(document.createTextNode(A("idpList.showSearch")));aS.href="#";am(aS,"DropDownToggle");aS.onclick=function(){U.style.display="";j.style.display="none";return false};j.appendChild(aS);x(j);aM.appendChild(j)};var C=function(aP){var aN="IdPSelectAutoDisp";autoDispatchTile=ap(undefined,"autoDispatchArea");autoDispatchTile.appendChild(document.createTextNode(A("autoFollow.message")));var aM=document.createElement("input");aM.setAttribute("type","radio");aM.setAttribute("checked","checked");aM.setAttribute("name",aN);aM.onclick=function(){E(0)};div=ap(undefined,"autoDispatchTile");div.appendChild(aM);div.appendChild(document.createTextNode(A("autoFollow.never")));autoDispatchTile.appendChild(div);var aO;for(aO=0;aO<Z.length;aO++){aM=document.createElement("input");aM.setAttribute("type","radio");aM.setAttribute("name",aN);aM.life=Z[aO];aM.onclick=function(){var aQ=this.life;E(aQ)};div=ap(undefined,"autoDispatchTile");div.appendChild(aM);div.appendChild(document.createTextNode(A("autoFollow.time"+aO)));autoDispatchTile.appendChild(div)}aP.appendChild(autoDispatchTile)};var v=function(aN){var aM=document.createElement("input");aM.setAttribute("type","submit");aM.value=A("submitButton.label");l(aM,aN+"Button");return aM};var x=function(aN){var aM=document.createElement("a");aM.href=R;aM.appendChild(document.createTextNode(A("helpText")));am(aM,"HelpButton");aN.appendChild(aM)};var ap=function(aO,aM){var aN=document.createElement("div");if(undefined!==aO){l(aN,aO)}if(undefined!==aM){am(aN,aM)}return aN};var p=function(aN,aO){var aM=document.createElement("option");aM.value=aN;if(aO.length>au){aO=aO.substring(0,au)}aM.appendChild(document.createTextNode(aO));return aM};var l=function(aN,aM){aN.id=aL+aM};var am=function(aN,aM){aN.setAttribute("class",aj+aM)};var aD=function(aM){return document.getElementById(aL+aM)};var aH=function(aM){J(aM);at(P)};var A=function(aM){var aN=aa[aM];if(!aN){aN=d[aM]}if(!aN){aN="Missing message for "+aM}return aN};var z=function(aM){return aM.entityID};var ak=function(aO){var aM;if(null==aO.Logos){return null}for(aM=0;aM<aO.Logos.length;aM++){var aN=aO.Logos[aM];if(aN.height=="16"&&aN.width=="16"){if(null==aN.lang||S==aN.lang||(typeof aB!="undefined"&&aB==aN.lang)||an==aN.lang){return aN.value}}}return null};var aE=function(aN){var aM=ay(aN.DisplayNames);if(null!==aM){return aM}s("No Name entry in any language for "+z(aN));return z(aN)};var I=function(aN){if(aq||null==aN.Keywords){return null}var aM=ay(aN.Keywords);return aM};var ay=function(aM){var aN;for(aN in aM){if(aM[aN].lang==S){return aM[aN].value}}if(typeof aB!="undefined"){for(aN in aM){if(aM[aN].lang==aB){return aM[aN].value}}}for(aN in aM){if(aM[aN].lang==null){return aM[aN].value}}for(aN in aM){if(aM[aN].lang==an){return aM[aN].value}}return null};var L=function(){var aQ=[];var aP=0;var aO;var aN;if(null!=D){for(aO=0;aO<D.length&&aO<ag;aO++){aQ[aO]=ad(D[aO]);aP++}}P=b();for(aO=aP,aN=0;aN<P.length&&aO<ag;aN++){var aM=ad(P[aN]);if(typeof aQ.indexOf==="undefined"){aQ.push(aM);aO++}else{if(aQ.indexOf(aM)===-1){aQ.push(aM);aO++}}}return aQ};var J=function(aM){var aN=[];while(0!==P.length){var aO=P.pop();if(aO!=aM){aN.unshift(aO)}}aN.unshift(aM);P=aN;return};var E=function(aO){var aM;if(aO>0){var aN=new Date();cookieTTL=aO*24*60*60*1000;aM=new Date(aN.getTime()+cookieTTL)}else{aM=new Date(0)}document.cookie=Q+"=1;path=/;expires="+aM.toUTCString()};var i=function(aO){var aQ,aN;var aR;aR=document.cookie.split(";");for(aQ=0;aQ<aR.length;aQ++){var aP=aR[aQ];var aM=aP.indexOf("=");var aS=aP.substring(0,aM);if(aO==(aS.replace(/^\s+|\s+$/g,""))){return aP.substring(aM+1)}}return null};var b=function(){var aM=[];var aN;var aO=i("_saml_idp");if(aO!=null){aO=aO.replace(/^\s+|\s+$/g,"");aO=aO.replace("+","%20");aO=aO.split("%20");for(aN=aO.length;aN>0;aN--){if(0===aO[aN-1].length){continue}var aP=av(decodeURIComponent(aO[aN-1]));if(aP.length>0){aM.push(aP)}}}return aM};var at=function(aR){var aO=[];var aQ=aR.length;if(af){return}if(aQ>5){aQ=5}for(var aP=aQ;aP>0;aP--){if(aR[aP-1].length>0){aO.push(encodeURIComponent(w(aR[aP-1])))}}var aM=null;if(K){var aN=new Date();cookieTTL=K*24*60*60*1000;aM=new Date(aN.getTime()+cookieTTL)}document.cookie="_saml_idp="+aO.join("%20")+"; path = /"+((aM===null)?"":"; expires="+aM.toUTCString())};var w=function(aV){var aM="",aQ,aO,aN,aU,aT,aS,aR;for(var aP=0;aP<aV.length;){aQ=aV.charCodeAt(aP++);aO=aV.charCodeAt(aP++);aN=aV.charCodeAt(aP++);aU=aQ>>2;aT=((aQ&3)<<4)+(aO>>4);aS=((aO&15)<<2)+(aN>>6);aR=aN&63;if(isNaN(aO)){aS=aR=64}else{if(isNaN(aN)){aR=64}}aM+=W.charAt(aU)+W.charAt(aT)+W.charAt(aS)+W.charAt(aR)}return aM};var av=function(aP){var aN="",aW,aU,aS,aV,aT,aR,aQ;var aO=0;var aM=/[^A-Za-z0-9\+\/\=]/g;aP=aP.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{aV=W.indexOf(aP.charAt(aO++));aT=W.indexOf(aP.charAt(aO++));aR=W.indexOf(aP.charAt(aO++));aQ=W.indexOf(aP.charAt(aO++));aW=(aV<<2)|(aT>>4);aU=((aT&15)<<4)|(aR>>2);aS=((aR&3)<<6)|aQ;aN=aN+String.fromCharCode(aW);if(aR!=64){aN=aN+String.fromCharCode(aU)}if(aQ!=64){aN=aN+String.fromCharCode(aS)}aW=aU=aS="";aV=aT=aR=aQ=""}while(aO<aP.length);return aN};var ae=function(aN){if(aJ){var aM=document.createTextNode(aN);aJ.appendChild(aM)}else{alert("FATAL (NoDiv):"+aN)}};var O=function(aM){alert("FATAL - DISCO UI:"+aM);if(aJ){ae(aM)}};var s=function(){}}(new IdPSelectUI()).draw(new IdPSelectUIParms()); \ No newline at end of file diff --git a/conf/etc/shibboleth-ds/idpselect_config.js b/conf/etc/shibboleth-ds/idpselect_config.js new file mode 100644 index 0000000000000000000000000000000000000000..f4252195f2498d7dcada5a6ab272556f01a3f6d3 --- /dev/null +++ b/conf/etc/shibboleth-ds/idpselect_config.js @@ -0,0 +1,81 @@ + +/** @class IdP Selector UI */ +function IdPSelectUIParms(){ + // + // Adjust the following to fit into your local configuration + // + this.alwaysShow = true; // If true, this will show results as soon as you start typing + this.dataSource = '/Shibboleth.sso/DiscoFeed'; // Where to get the data from + this.defaultLanguage = 'en'; // Language to use if the browser local doesnt have a bundle + this.defaultLogo = 'blank.gif'; // Replace with your own logo + this.defaultLogoWidth = 1; + this.defaultLogoHeight = 1 ; + this.defaultReturn = null; // If non null, then the default place to send users who are not + // Approaching via the Discovery Protocol for example + //this.defaultReturn = "https://example.org/Shibboleth.sso/DS?SAMLDS=1&target=https://example.org/secure"; + this.defaultReturnIDParam = null; + this.returnWhiteList = [ "^https:\/\/example\.org\/Shibboleth\.sso\/Login.*$" , "^https:\/\/example\.com\/Shibboleth\.sso\/Login.*$" ]; + this.helpURL = 'https://wiki.shibboleth.net/confluence/display/SHIB2/DSRoadmap'; + this.ie6Hack = null; // An array of structures to disable when drawing the pull down (needed to + // handle the ie6 z axis problem + this.insertAtDiv = 'idpSelect'; // The div where we will insert the data + this.maxResults = 10; // How many results to show at once or the number at which to + // start showing if alwaysShow is false + this.myEntityID = null; // If non null then this string must match the string provided in the DS parms + this.preferredIdP = null; // Array of entityIds to always show + this.hiddenIdPs = null; // Array of entityIds to delete + this.ignoreKeywords = false; // Do we ignore the <mdui:Keywords/> when looking for candidates + this.showListFirst = false; // Do we start with a list of IdPs or just the dropdown + this.samlIdPCookieTTL = 730; // in days + this.setFocusTextBox = true; // Set to false to supress focus + this.testGUI = false; + + this.autoFollowCookie = null; // If you want auto-dispatch, set this to the cookie name to use + this.autoFollowCookieTTLs = [ 1, 60, 270 ]; // Cookie life (in days). Changing this requires changes to idp_select_languages + + // + // Language support. + // + // The minified source provides "en", "de", "pt-br" and "jp". + // + // Override any of these below, or provide your own language + // + //this.langBundles = { + //'en': { + // 'fatal.divMissing': '<div> specified as "insertAtDiv" could not be located in the HTML', + // 'fatal.noXMLHttpRequest': 'Browser does not support XMLHttpRequest, unable to load IdP selection data', + // 'fatal.wrongProtocol' : 'Policy supplied to DS was not "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"', + // 'fatal.wrongEntityId' : 'entityId supplied by SP did not match configuration', + // 'fatal.noData' : 'Metadata download returned no data', + // 'fatal.loadFailed': 'Failed to download metadata from ', + // 'fatal.noparms' : 'No parameters to discovery session and no defaultReturn parameter configured', + // 'fatal.noReturnURL' : "No URL return parameter provided", + // 'fatal.badProtocol' : "Return request must start with https:// or http://", + // 'idpPreferred.label': 'Use a suggested selection:', + // 'idpEntry.label': 'Or enter your organization\'s name', + // 'idpEntry.NoPreferred.label': 'Enter your organization\'s name', + // 'idpList.label': 'Or select your organization from the list below', + // 'idpList.NoPreferred.label': 'Select your organization from the list below', + // 'idpList.defaultOptionLabel': 'Please select your organization...', + // 'idpList.showList' : 'Allow me to pick from a list', + // 'idpList.showSearch' : 'Allow me to specify the site', + // 'submitButton.label': 'Continue', + // 'helpText': 'Help', + // 'defaultLogoAlt' : 'DefaultLogo' + //} + //}; + + // + // The following should not be changed without changes to the css. Consider them as mandatory defaults + // + this.maxPreferredIdPs = 3; + this.maxIdPCharsButton = 33; + this.maxIdPCharsDropDown = 58; + this.maxIdPCharsAltTxt = 60; + + this.minWidth = 20; + this.minHeight = 20; + this.maxWidth = 115; + this.maxHeight = 69; + this.bestRatio = Math.log(80 / 60); +} diff --git a/conf/etc/shibboleth-ds/index.html b/conf/etc/shibboleth-ds/index.html new file mode 100644 index 0000000000000000000000000000000000000000..a4f85bc907d461c8b24f56dc9e8c5ebc14f54373 --- /dev/null +++ b/conf/etc/shibboleth-ds/index.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" +"http://www.w3.org/TR/html4/loose.dtd"> +<html lang="en"> +<head> + <title>IDP select test bed</title> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-5" /> + <link rel="stylesheet" type="text/css" href="idpselect.css" /> +</head> + +<body> + <div id="idpSelect"></div> + + <script src="idpselect_config.js" type="text/javascript" language="javascript"></script> + + <script src="idpselect.js" type="text/javascript" language="javascript"></script> + + + <noscript> + <!-- If you need to care about non javascript browsers you will need to + generate a hyperlink to a non-js DS. + + To build you will need: + - URL: The base URL of the DS you use + - EI: Your entityId, URLencoded. You can get this from the line that + this page is called with. + - RET: Your return address dlib-adidp.ucs.ed.ac.uk. Again you can get + this from the page this is called with, but beware of the + target%3Dcookie%253A5269905f bit.. + + < href=${URL}?entityID=${EI}&return=${RET} + --> + + Your Browser does not support javascript. Please use + <a href="http://federation.org/DS/DS?entityID=https%3A%2F%2FyourentityId.edu.edu%2Fshibboleth&return=https%3A%2F%2Fyourreturn.edu%2FShibboleth.sso%2FDS%3FSAMLDS%3D1%26target%3Dhttps%3A%2F%2Fyourreturn.edu%2F">this link</a>. + + </noscript> +</body> +</html> diff --git a/conf/etc/shibboleth-ds/nonminimised/idpselect.js b/conf/etc/shibboleth-ds/nonminimised/idpselect.js new file mode 100644 index 0000000000000000000000000000000000000000..08a546ca444add6d8c3a80dfa4ec7a4a6c4fbb1a --- /dev/null +++ b/conf/etc/shibboleth-ds/nonminimised/idpselect.js @@ -0,0 +1,1618 @@ +function IdPSelectUI() { + // + // module locals + // + var idpData; + var base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var idpSelectDiv; + var lang; + var majorLang; + var defaultLang; + var langBundle; + var defaultLangBundle; + var defaultLogo; + var defaultLogoWidth; + var defaultLogoHeight; + var minWidth; + var minHeight; + var maxWidth; + var maxHeight; + var bestRatio; + var doNotCollapse; + + // + // Parameters passed into our closure + // + var preferredIdP; + var maxPreferredIdPs; + var helpURL; + var ie6Hack; + var samlIdPCookieTTL; + var maxIdPCharsDropDown; + var maxIdPCharsButton; + var maxIdPCharsAltTxt; + var alwaysShow; + var maxResults; + var ignoreKeywords; + var showListFirst; + var noWriteCookie; + var ignoreURLParams; + + var autoFollowCookie; + var autoFollowCookieTTLs; + + // + // The cookie contents + // + var userSelectedIdPs; + // + // Anchors used inside autofunctions + // + var idpEntryDiv; + var idpListDiv; + var idpSelect; + var listButton; + + // + // local configuration + // + var idPrefix = 'idpSelect'; + var classPrefix = 'IdPSelect'; + var dropDownControl; + + // + // DS protocol configuration + // + var returnString = ''; + var returnBase=''; + var returnParms= []; + var returnIDParam = 'entityID'; + + // ************************************* + // Public functions + // ************************************* + + /** + Draws the IdP Selector UI on the screen. This is the main + method for the IdPSelectUI class. + */ + this.draw = function(parms){ + + // + // try to set up the div first so that errors from setUpLocals + // will go to the screen. + // + idpSelectDiv = document.getElementById(parms.insertAtDiv); + + if (!setupLocals(parms)) { + return; + } + + if(!idpSelectDiv){ + fatal(getLocalizedMessage('fatal.divMissing')); + return; + } + + // + // Quick test for auto-dispatch + // + if ((null != autoFollowCookie) && (null != getCookieCalled( autoFollowCookie ))) { + + var prefs = retrieveUserSelectedIdPs(); + if (prefs.length != 0) { + var retString = returnIDParam + '=' + encodeURIComponent(prefs[0]); + // + // Compose up the URL + // + if (returnString.indexOf('?') == -1) { + retString = '?' + retString; + } else { + retString = '&' + retString; + } + // + // Go there + // + dispatchTo(idpSelectDiv, returnString + retString); + return; + } + } + + if (!load(parms.dataSource)) { + return; + } + deDupe(); + stripHidden(parms.hiddenIdPs); + + idpData.sort(function(a,b) {return getLocalizedName(a).localeCompare(getLocalizedName(b));}); + + var idpSelector = buildIdPSelector(); + idpSelectDiv.appendChild(idpSelector); + dropDownControl.draw(parms.setFocusTextBox); + } ; + + // ************************************* + // Private functions + // + // Data Manipulation + // + // ************************************* + + /** + Copies the "parameters" in the function into namesspace local + variables. This means most of the work is done outside the + IdPSelectUI object + */ + + var setupLocals = function (paramsSupplied) { + // + // Copy parameters in + // + var suppliedEntityId; + + preferredIdP = paramsSupplied.preferredIdP; + maxPreferredIdPs = paramsSupplied.maxPreferredIdPs; + helpURL = paramsSupplied.helpURL; + ie6Hack = paramsSupplied.ie6Hack; + samlIdPCookieTTL = paramsSupplied.samlIdPCookieTTL; + alwaysShow = paramsSupplied.alwaysShow; + maxResults = paramsSupplied.maxResults; + ignoreKeywords = paramsSupplied.ignoreKeywords; + if (paramsSupplied.showListFirst) { + showListFirst = paramsSupplied.showListFirst; + } else { + showListFirst = false; + } + if (paramsSupplied.noWriteCookie) { + noWriteCookie = paramsSupplied.noWriteCookie; + } else { + noWriteCookie = false; + } + if (paramsSupplied.ignoreURLParams) { + ignoreURLParams = paramsSupplied.ignoreURLParams; + } else { + ignoreURLParams = false; + } + + defaultLogo = paramsSupplied.defaultLogo; + defaultLogoWidth = paramsSupplied.defaultLogoWidth; + defaultLogoHeight = paramsSupplied.defaultLogoHeight; + minWidth = paramsSupplied.minWidth; + minHeight = paramsSupplied.minHeight; + maxWidth = paramsSupplied.maxWidth; + maxHeight = paramsSupplied.maxHeight; + bestRatio = paramsSupplied.bestRatio; + if (null == paramsSupplied.doNotCollapse) { + doNotCollapse = true; + } else { + doNotCollapse = paramsSupplied.doNotCollapse; + } + + maxIdPCharsButton = paramsSupplied.maxIdPCharsButton; + maxIdPCharsDropDown = paramsSupplied.maxIdPCharsDropDown; + maxIdPCharsAltTxt = paramsSupplied.maxIdPCharsAltTxt; + + autoFollowCookie = paramsSupplied.autoFollowCookie; + autoFollowCookieTTLs = paramsSupplied.autoFollowCookieTTLs; + + var lang; + + if (typeof navigator == 'undefined') { + lang = paramsSupplied.defaultLanguage; + } else { + lang = navigator.language || navigator.userLanguage || paramsSupplied.defaultLanguage; + } + lang = lang.toLowerCase(); + + if (lang.indexOf('-') > 0) { + majorLang = lang.substring(0, lang.indexOf('-')); + } + + var providedLangs = new IdPSelectLanguages(); + + defaultLang = paramsSupplied.defaultLanguage; + + if (typeof paramsSupplied.langBundles != 'undefined' && typeof paramsSupplied.langBundles[lang] != 'undefined') { + langBundle = paramsSupplied.langBundles[lang]; + } else if (typeof providedLangs.langBundles[lang] != 'undefined') { + langBundle = providedLangs.langBundles[lang]; + } else if (typeof majorLang != 'undefined') { + if (typeof paramsSupplied.langBundles != 'undefined' && typeof paramsSupplied.langBundles[majorLang] != 'undefined') { + langBundle = paramsSupplied.langBundles[majorLang]; + } else if (typeof providedLangs.langBundles[majorLang] != 'undefined') { + langBundle = providedLangs.langBundles[majorLang]; + } + } + + if (typeof paramsSupplied.langBundles != 'undefined' && typeof paramsSupplied.langBundles[paramsSupplied.defaultLanguage] != 'undefined') { + defaultLangBundle = paramsSupplied.langBundles[paramsSupplied.defaultLanguage]; + } else { + defaultLangBundle = providedLangs.langBundles[paramsSupplied.defaultLanguage]; + } + + // + // Setup Language bundles + // + if (!defaultLangBundle) { + fatal('No languages work'); + return false; + } + if (!langBundle) { + debug('No language support for ' + lang); + langBundle = defaultLangBundle; + } + + if (paramsSupplied.testGUI) { + // + // no policing of parms + // + return true; + } + // + // Now set up the return values from the URL + // + var policy = 'urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single'; + var i; + var isPassive = false; + var parms; + var parmPair; + var win = window; + while (null !== win.parent && win !== win.parent) { + win = win.parent; + } + var loc = win.location; + var parmlist = loc.search; + if (ignoreURLParams || null == parmlist || 0 == parmlist.length || parmlist.charAt(0) != '?') { + + if ((null == paramsSupplied.defaultReturn)&& !ignoreURLParams) { + + fatal(getLocalizedMessage('fatal.noparms')); + return false; + } + // + // No parameters, so just collect the defaults + // + suppliedEntityId = paramsSupplied.myEntityID; + returnString = paramsSupplied.defaultReturn; + if (null != paramsSupplied.defaultReturnIDParam) { + returnIDParam = paramsSupplied.defaultReturnIDParam; + } + + } else { + parmlist = parmlist.substring(1); + + // + // protect against various hideousness by decoding. We re-encode just before we push + // + + parms = parmlist.split('&'); + if (parms.length === 0) { + + fatal(getLocalizedMessage('fatal.noparms')); + return false; + } + + for (i = 0; i < parms.length; i++) { + parmPair = parms[i].split('='); + if (parmPair.length != 2) { + continue; + } + if (parmPair[0] == 'entityID') { + suppliedEntityId = decodeURIComponent(parmPair[1]); + } else if (parmPair[0] == 'return') { + returnString = decodeURIComponent(parmPair[1]); + } else if (parmPair[0] == 'returnIDParam') { + returnIDParam = decodeURIComponent(parmPair[1]); + } else if (parmPair[0] == 'policy') { + policy = decodeURIComponent(parmPair[1]); + } else if (parmPair[0] == 'isPassive') { + isPassive = (parmPair[1].toUpperCase() == "TRUE"); + } + } + } + // Test protocol + var allowableProtocols; + if (null == paramsSupplied.allowableProtocols) { + allowableProtocols = ["urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"]; + } else { + allowableProtocols = paramsSupplied.allowableProtocols; + } + + var protocolOk = false; + for (var i = 0 ; i < allowableProtocols.length; i++) { + var protocol = allowableProtocols[i]; + if (policy == protocol) { + protocolOk = true; + break; + } + } + + if (!protocolOk) { + fatal(getLocalizedMessage('fatal.wrongProtocol')); + return false; + } + if (paramsSupplied.myEntityID !== null && paramsSupplied.myEntityID != suppliedEntityId) { + fatal(getLocalizedMessage('fatal.wrongEntityId') + '"' + suppliedEntityId + '" != "' + paramsSupplied.myEntityID + '"'); + return false; + } + if (null === returnString || returnString.length === 0) { + fatal(getLocalizedMessage('fatal.noReturnURL')); + return false; + } + if (!validProtocol(returnString)) { + fatal(getLocalizedMessage('fatal.badProtocol')); + return false; + } + if (!validateReturn(paramsSupplied.returnWhiteList, returnString)) { + + fatalNoAlert(getLocalizedMessage('fatal.badReturnString')); + return false; + } + + // + // isPassive + // + if (isPassive) { + var prefs = retrieveUserSelectedIdPs(); + var parentDiv = document.getElementById(parmsSupplied.insertAtDiv); + if (prefs.length == 0) { + // + // no preference, go back + // + dispatchTo(parentDiv, returnString); + return false; + } else { + var retString = returnIDParam + '=' + encodeURIComponent(prefs[0]); + // + // Compose up the URL + // + if (returnString.indexOf('?') == -1) { + retString = '?' + retString; + } else { + retString = '&' + retString; + } + + dispatchTo(parentDiv, returnString + retString); + return false; + } + } + + // + // Now split up returnString + // + i = returnString.indexOf('?'); + if (i < 0) { + returnBase = returnString; + return true; + } + returnBase = returnString.substring(0, i); + parmlist = returnString.substring(i+1); + parms = parmlist.split('&'); + for (i = 0; i < parms.length; i++) { + parmPair = parms[i].split('='); + if (parmPair.length != 2) { + continue; + } + parmPair[1] = decodeURIComponent(parmPair[1]); + returnParms.push(parmPair); + } + return true; + }; + + /** Deduplicate by entityId */ + var deDupe = function() { + var names = []; + var j; + for (j = 0; j < idpData.length; ) { + var eid = getEntityId(idpData[j]); + if (null == names[eid]) { + names[eid] = eid; + j = j + 1; + } else { + idpData.splice(j, 1); + } + } + } + + /** + Strips the supllied IdP list from the idpData + */ + var stripHidden = function(hiddenList) { + + if (null == hiddenList || 0 == hiddenList.length) { + return; + } + var i; + var j; + for (i = 0; i < hiddenList.length; i++) { + for (j = 0; j < idpData.length; j++) { + if (getEntityId(idpData[j]) == hiddenList[i]) { + idpData.splice(j, 1); + break; + } + } + } + } + + + /** + * Strip the "protocol://host" bit out of the URL and check the protocol + * @param the URL to process + * @return whether it starts with http: or https:// + */ + + var validProtocol = function(s) { + if (null === s) { + return false; + } + var marker = "://"; + var protocolEnd = s.indexOf(marker); + if (protocolEnd < 0) { + return false; + } + s = s.substring(0, protocolEnd); + if (s == "http" || s== "https") { + return true; + } + return false; + }; + + /** + * Validate the return String against the regexps + */ + + var validateReturn = function(regexps, testString) { + if (null == regexps) { + return true; + } + + // use .length in case regexps isnt an array + for (var i = 0; i < regexps.length; i++) { + var regexp = new RegExp(regexps[i]); + if (regexp.test(testString)) { + return true; + } + } + return false; + }; + + /** + * We need to cache bust on IE. So how do we know? Use a bigger hammer. + */ + var isIE = function() { + if (null == navigator) { + return false; + } + var browserName = navigator.appName; + if (null == browserName) { + return false; + } + return (browserName == 'Microsoft Internet Explorer') ; + } ; + + /** + * Alternative to location.href=string + * + * Needed to cache bust Firefox + */ + + var dispatchTo = function(theParent, whereTo) { + var aval = document.createElement('a'); + + aval.href = whereTo; + theParent.appendChild(aval); + + aval.click(); + } + + /** + Loads the data used by the IdP selection UI. Data is loaded + from a JSON document fetched from the given url. + + @param {Function} failureCallback A function called if the JSON + document can not be loaded from the source. This function will + passed the {@link XMLHttpRequest} used to request the JSON data. + */ + var load = function(dataSource){ + var xhr = null; + + try { + xhr = new XMLHttpRequest(); + } catch (e) {} + if (null == xhr) { + // + // EDS24. try to get 'Microsoft.XMLHTTP' + // + try { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) {} + } + if (null == xhr) { + // + // EDS35. try to get 'Microsoft.XMLHTTP' + // + try { + xhr = new ActiveXObject('MSXML2.XMLHTTP.3.0'); + } catch (e) {} + } + if (null == xhr) { + fatal(getLocalizedMessage('fatal.noXMLHttpRequest')); + return false; + } + + if (isIE()) { + // + // cache bust (for IE) + // + dataSource += '?random=' + (Math.random()*1000000); + } + + // + // Grab the data + // + xhr.open('GET', dataSource, false); + if (typeof xhr.overrideMimeType == 'function') { + xhr.overrideMimeType('application/json'); + } + xhr.send(null); + + if(xhr.status == 200){ + // + // 200 means we got it OK from as web source + // if locally loading its 0. Go figure + // + var jsonData = xhr.responseText; + if(jsonData === null){ + fatal(getLocalizedMessage('fatal.noData')); + return false; + } + + // + // Parse it + // + + idpData = JSON.parse(jsonData); + + }else{ + fatal(getLocalizedMessage('fatal.loadFailed') + dataSource); + return false; + } + return true; + }; + + /** + Returns the idp object with the given name. + + @param (String) the name we are interested in + @return (Object) the IdP we care about + */ + + var getIdPFor = function(idpName) { + + for (var i = 0; i < idpData.length; i++) { + if (getEntityId(idpData[i]) == idpName) { + return idpData[i]; + } + } + return null; + }; + + /** + Returns a suitable image from the given IdP + + @param (Object) The IdP + @return Object) a DOM object suitable for insertion + + TODO - rather more careful selection + */ + + var getImageForIdP = function(idp, useDefault) { + + var getBestFit = function(language) { + // + // See GetLocalizedEntry + // + var bestFit = null; + var i; + if (null == idp.Logos) { + return null; + } + for (i in idp.Logos) { + if (idp.Logos[i].lang == language && + idp.Logos[i].width != null && + idp.Logos[i].width >= minWidth && + idp.Logos[i].height != null && + idp.Logos[i].height >= minHeight) { + if (bestFit === null) { + bestFit = idp.Logos[i]; + } else { + me = Math.abs(bestRatio - Math.log(idp.Logos[i].width/idp.Logos[i].height)); + him = Math.abs(bestRatio - Math.log(bestFit.width/bestFit.height)); + if (him > me) { + bestFit = idp.Logos[i]; + } + } + } + } + return bestFit; + } ; + + var bestFit = null; + var img = document.createElement('img'); + setClass(img, 'IdPImg'); + + bestFit = getBestFit(lang); + if (null === bestFit && typeof majorLang != 'undefined') { + bestFit = getBestFit(majorLang); + } + if (null === bestFit) { + bestFit = getBestFit(null); + } + if (null === bestFit) { + bestFit = getBestFit(defaultLang); + } + + if (null === bestFit) { + if (!useDefault) { + return null; + } + img.src = defaultLogo; + img.width = defaultLogoWidth; + img.height = defaultLogoHeight; + img.alt = getLocalizedMessage('defaultLogoAlt'); + return img; + } + + img.src = bestFit.value; + var altTxt = getLocalizedName(idp); + if (altTxt.length > maxIdPCharsAltTxt) { + altTxt = altTxt.substring(0, maxIdPCharsAltTxt) + '...'; + } + img.alt = altTxt; + + var w = bestFit.width; + var h = bestFit.height; + if (w>maxWidth) { + h = (maxWidth/w) * h; + w = maxWidth; + } + if (h> maxHeight) { + w = (maxHeight/h) * w; + h = maxHeight; + } + + img.setAttribute('width', w); + img.setAttribute('height', h); + return img; + }; + + // ************************************* + // Private functions + // + // GUI Manipulation + // + // ************************************* + + /** + Builds the IdP selection UI. + + Three divs. PreferredIdPTime, EntryTile and DropdownTile + Optional div AutoDispatchPane + + @return {Element} IdP selector UI + */ + var buildIdPSelector = function(){ + var containerDiv = buildDiv('IdPSelector'); + var preferredTileExists; + preferredTileExists = buildPreferredIdPTile(containerDiv); + buildIdPEntryTile(containerDiv, preferredTileExists); + buildIdPDropDownListTile(containerDiv, preferredTileExists); + if (null != autoFollowCookie) { + buildAutoDispatchPane(containerDiv); + } + return containerDiv; + }; + + /** + Builds a button for the provided IdP + <div class="preferredIdPButton"> + <a href="XYX" onclick=setparm('ABCID')> + <div class= + <img src="https:\\xyc.gif"> <!-- optional --> + XYX Text + </a> + </div> + + @param (Object) The IdP + + @return (Element) preselector for the IdP + */ + + var composePreferredIdPButton = function(idp, uniq, useDefault) { + var div = buildDiv(undefined, 'PreferredIdPButton'); + var aval = document.createElement('a'); + var retString = returnIDParam + '=' + encodeURIComponent(getEntityId(idp)); + var retVal = returnString; + var img = getImageForIdP(idp, useDefault); + // + // Compose up the URL + // + if (retVal.indexOf('?') == -1) { + retString = '?' + retString; + } else { + retString = '&' + retString; + } + aval.href = retVal + retString; + aval.onclick = function () { + selectIdP(getEntityId(idp)); + }; + if (null != img) { + var imgDiv=buildDiv(undefined, 'PreferredIdPImg'); + imgDiv.appendChild(img); + aval.appendChild(imgDiv); + } + + var nameDiv = buildDiv(undefined, 'TextDiv'); + var nameStr = getLocalizedName(idp); + if (nameStr.length > maxIdPCharsButton) { + nameStr = nameStr.substring(0, maxIdPCharsButton) + '...'; + } + div.title = nameStr; + nameDiv.appendChild(document.createTextNode(nameStr)); + aval.appendChild(nameDiv); + + div.appendChild(aval); + return div; + }; + + /** + * Builds and populated a text Div + */ + var buildTextDiv = function(parent, textId) + { + var div = buildDiv(undefined, 'TextDiv'); + var introTxt = document.createTextNode(getLocalizedMessage(textId)); + div.appendChild(introTxt); + parent.appendChild(div); + } ; + + var setSelector = function (selector, selected) { + if (null === selected || 0 === selected.length || '-' == selected.value) { + return; + } + var i = 0; + while (i < selector.options.length) { + if (selector.options[i].value == selected) { + selector.options[i].selected = true; + break; + } + i++; + } + } + + /** + Builds the preferred IdP selection UI (top half of the UI w/ the + IdP buttons) + + <div id=prefix+"PreferredIdPTile"> + <div> [see comprosePreferredIdPButton </div> + [repeated] + </div> + + @return {Element} preferred IdP selection UI + */ + var buildPreferredIdPTile = function(parentDiv) { + + var preferredIdPs = getPreferredIdPs(); + if (0 === preferredIdPs.length) { + return false; + } + + var atLeastOneImg = doNotCollapse; + for(var i = 0 ; i < maxPreferredIdPs && i < preferredIdPs.length; i++){ + if (preferredIdPs[i] && getImageForIdP(preferredIdPs[i], false)) { + atLeastOneImg = true; + } + } + + var preferredIdPDIV; + if (atLeastOneImg) { + preferredIdPDIV = buildDiv('PreferredIdPTile'); + } else { + preferredIdPDIV = buildDiv('PreferredIdPTileNoImg'); + } + + + buildTextDiv(preferredIdPDIV, 'idpPreferred.label'); + + + for(var i = 0 ; i < maxPreferredIdPs && i < preferredIdPs.length; i++){ + if (preferredIdPs[i]) { + var button = composePreferredIdPButton(preferredIdPs[i],i, atLeastOneImg); + preferredIdPDIV.appendChild(button); + } + } + + parentDiv.appendChild(preferredIdPDIV); + return true; + }; + + /** + * Build the <form> from the return parameters + */ + + var buildSelectForm = function () + { + var form = document.createElement('form'); + idpEntryDiv.appendChild(form); + + form.action = returnBase; + form.method = 'GET'; + form.setAttribute('autocomplete', 'OFF'); + var i = 0; + for (i = 0; i < returnParms.length; i++) { + var hidden = document.createElement('input'); + hidden.setAttribute('type', 'hidden'); + hidden.name = returnParms[i][0]; + hidden.value= returnParms[i][1]; + form.appendChild(hidden); + } + + return form; + } ; + + + /** + Build the manual IdP Entry tile (bottom half of UI with + search-as-you-type field). + + <div id = prefix+"IdPEntryTile"> + <form> + <input type="text", id=prefix+"IdPSelectInput/> // select text box + <input type="hidden" /> param to send + <input type="submit" /> + + + @return {Element} IdP entry UI tile + */ + var buildIdPEntryTile = function(parentDiv, preferredTile) { + + + idpEntryDiv = buildDiv('IdPEntryTile'); + if (showListFirst) { + idpEntryDiv.style.display = 'none'; + } + + var label = document.createElement('label'); + label.setAttribute('for', idPrefix + 'Input'); + + if (preferredTile) { + buildTextDiv(label, 'idpEntry.label'); + } else { + buildTextDiv(label, 'idpEntry.NoPreferred.label'); + } + + var form = buildSelectForm(); + form.appendChild(label); + + var textInput = document.createElement('input'); + form.appendChild(textInput); + + textInput.type='text'; + setID(textInput, 'Input'); + + var hidden = document.createElement('input'); + hidden.setAttribute('type', 'hidden'); + form.appendChild(hidden); + + hidden.name = returnIDParam; + hidden.value='-'; + + var button = buildContinueButton('Select'); + button.disabled = true; + form.appendChild(button); + + form.onsubmit = function () { + // + // Make sure we cannot ask for garbage + // + if (null === hidden.value || 0 === hidden.value.length || '-' == hidden.value) { + return false; + } + // + // And always ask for the cookie to be updated before we continue + // + textInput.value = hidden.textValue; + selectIdP(hidden.value); + return true; + }; + + dropDownControl = new TypeAheadControl(idpData, textInput, hidden, button, maxIdPCharsDropDown, getLocalizedName, getEntityId, geticon, ie6Hack, alwaysShow, maxResults, getKeywords); + + var a = document.createElement('a'); + a.appendChild(document.createTextNode(getLocalizedMessage('idpList.showList'))); + a.href = '#'; + setClass(a, 'DropDownToggle'); + a.onclick = function() { + idpEntryDiv.style.display='none'; + setSelector(idpSelect, hidden.value); + idpListDiv.style.display=''; + listButton.focus(); + return false; + }; + idpEntryDiv.appendChild(a); + buildHelpText(idpEntryDiv); + + parentDiv.appendChild(idpEntryDiv); + }; + + /** + Builds the drop down list containing all the IdPs from which a + user may choose. + + <div id=prefix+"IdPListTile"> + <label for="idplist">idpList.label</label> + <form action="URL from IDP Data" method="GET"> + <select name="param from IdP data"> + <option value="EntityID">Localized Entity Name</option> + [...] + </select> + <input type="submit"/> + </div> + + @return {Element} IdP drop down selection UI tile + */ + var buildIdPDropDownListTile = function(parentDiv, preferredTile) { + idpListDiv = buildDiv('IdPListTile'); + if (!showListFirst) { + idpListDiv.style.display = 'none'; + } + + var label = document.createElement('label'); + label.setAttribute('for', idPrefix + 'Selector'); + + if (preferredTile) { + buildTextDiv(label, 'idpList.label'); + } else { + buildTextDiv(label, 'idpList.NoPreferred.label'); + } + + idpSelect = document.createElement('select'); + setID(idpSelect, 'Selector'); + idpSelect.name = returnIDParam; + idpListDiv.appendChild(idpSelect); + + var idpOption = buildSelectOption('-', getLocalizedMessage('idpList.defaultOptionLabel')); + idpOption.selected = true; + + idpSelect.appendChild(idpOption); + + var idp; + for(var i=0; i<idpData.length; i++){ + idp = idpData[i]; + idpOption = buildSelectOption(getEntityId(idp), getLocalizedName(idp)); + idpSelect.appendChild(idpOption); + } + + var form = buildSelectForm(); + form.appendChild(label); + form.appendChild(idpSelect); + + form.onsubmit = function () { + // + // The first entery isn't selectable + // + if (idpSelect.selectedIndex < 1) { + return false; + } + // + // otherwise update the cookie + // + selectIdP(idpSelect.options[idpSelect.selectedIndex].value); + return true; + }; + + var button = buildContinueButton('List'); + listButton = button; + form.appendChild(button); + + idpListDiv.appendChild(form); + + // + // The switcher + // + var a = document.createElement('a'); + a.appendChild(document.createTextNode(getLocalizedMessage('idpList.showSearch'))); + a.href = '#'; + setClass(a, 'DropDownToggle'); + a.onclick = function() { + idpEntryDiv.style.display=''; + idpListDiv.style.display='none'; + return false; + }; + idpListDiv.appendChild(a); + buildHelpText(idpListDiv); + + parentDiv.appendChild(idpListDiv); + }; + + var buildAutoDispatchPane = function(parent) { + var inputName = 'IdPSelectAutoDisp' + + autoDispatchTile = buildDiv(undefined, 'autoDispatchArea'); + + autoDispatchTile.appendChild(document.createTextNode(getLocalizedMessage('autoFollow.message'))); + // + // The "clear" button + // + var but = document.createElement('input'); + but.setAttribute('type', 'radio'); + but.setAttribute('checked', 'checked'); + but.setAttribute('name', inputName); + but.onclick = function () { + setAutoDispatchCookie(0); + } + + div = buildDiv(undefined, 'autoDispatchTile'); + div.appendChild(but); + div.appendChild(document.createTextNode(getLocalizedMessage('autoFollow.never'))); + autoDispatchTile.appendChild(div); + + var i; + for (i = 0; i < autoFollowCookieTTLs.length; i++) { + // + // The timed buttons + // + but = document.createElement('input'); + but.setAttribute('type', 'radio'); + but.setAttribute('name', inputName); + + but.life = autoFollowCookieTTLs[i]; + but.onclick = function () { + var f = this.life; + setAutoDispatchCookie(f); + } + + div = buildDiv(undefined, 'autoDispatchTile'); + div.appendChild(but); + div.appendChild(document.createTextNode( + getLocalizedMessage('autoFollow.time'+i))); + autoDispatchTile.appendChild(div); + } + + parent.appendChild(autoDispatchTile); + } + + /** + Builds the 'continue' button used to submit the IdP selection. + + @return {Element} HTML button used to submit the IdP selection + */ + var buildContinueButton = function(which) { + var button = document.createElement('input'); + button.setAttribute('type', 'submit'); + button.value = getLocalizedMessage('submitButton.label'); + setID(button, which + 'Button'); + + return button; + }; + + /** + Builds an aref to point to the helpURL + */ + + var buildHelpText = function(containerDiv) { + var aval = document.createElement('a'); + aval.href = helpURL; + aval.appendChild(document.createTextNode(getLocalizedMessage('helpText'))); + setClass(aval, 'HelpButton'); + containerDiv.appendChild(aval); + } ; + + /** + Creates a div element whose id attribute is set to the given ID. + + @param {String} id ID for the created div element + @param {String} [class] class of the created div element + @return {Element} DOM 'div' element with an 'id' attribute + */ + var buildDiv = function(id, whichClass){ + var div = document.createElement('div'); + if (undefined !== id) { + setID(div, id); + } + if(undefined !== whichClass) { + + setClass(div, whichClass); + } + return div; + }; + + /** + Builds an HTML select option element + + @param {String} value value of the option when selected + @param {String} label displayed label of the option + */ + var buildSelectOption = function(value, text){ + var option = document.createElement('option'); + option.value = value; + if (text.length > maxIdPCharsDropDown) { + text = text.substring(0, maxIdPCharsDropDown); + } + option.appendChild(document.createTextNode(text)); + return option; + }; + + /** + Sets the attribute 'id' on the provided object + We do it through this function so we have a single + point where we can prepend a value + + @param (Object) The [DOM] Object we want to set the attribute on + @param (String) The Id we want to set + */ + + var setID = function(obj, name) { + obj.id = idPrefix + name; + }; + + var setClass = function(obj, name) { + obj.setAttribute('class', classPrefix + name); + }; + + /** + Returns the DOM object with the specified id. We abstract + through a function to allow us to prepend to the name + + @param (String) the (unprepended) id we want + */ + var locateElement = function(name) { + return document.getElementById(idPrefix + name); + }; + + // ************************************* + // Private functions + // + // GUI actions. Note that there is an element of closure going on + // here since these names are invisible outside this module. + // + // + // ************************************* + + /** + * Base helper function for when an IdP is selected + * @param (String) The UN-encoded entityID of the IdP + */ + + var selectIdP = function(idP) { + updateSelectedIdPs(idP); + saveUserSelectedIdPs(userSelectedIdPs); + }; + + // ************************************* + // Private functions + // + // Localization handling + // + // ************************************* + + /** + Gets a localized string from the given language pack. This + method uses the {@link langBundles} given during construction + time. + + @param {String} messageId ID of the message to retrieve + + @return (String) the message + */ + var getLocalizedMessage = function(messageId){ + + var message = langBundle[messageId]; + if(!message){ + message = defaultLangBundle[messageId]; + } + if(!message){ + message = 'Missing message for ' + messageId; + } + + return message; + }; + + var getEntityId = function(idp) { + return idp.entityID; + }; + + /** + Returns the icon information for the provided idp + + @param (Object) an idp. This should have an array 'names' with sub + elements 'lang' and 'name'. + + @return (String) The localized name + */ + var geticon = function(idp) { + var i; + + if (null == idp.Logos) { + return null; + } + for (i =0; i < idp.Logos.length; i++) { + var logo = idp.Logos[i]; + + if (logo.height == "16" && logo.width == "16") { + if (null == logo.lang || + lang == logo.lang || + (typeof majorLang != 'undefined' && majorLang == logo.lang) || + defaultLang == logo.lang) { + return logo.value; + } + } + } + + return null; + } ; + + /** + Returns the localized name information for the provided idp + + @param (Object) an idp. This should have an array 'names' with sub + elements 'lang' and 'name'. + + @return (String) The localized name + */ + var getLocalizedName = function(idp) { + var res = getLocalizedEntry(idp.DisplayNames); + if (null !== res) { + return res; + } + debug('No Name entry in any language for ' + getEntityId(idp)); + return getEntityId(idp); + } ; + + var getKeywords = function(idp) { + if (ignoreKeywords || null == idp.Keywords) { + return null; + } + var s = getLocalizedEntry(idp.Keywords); + + return s; + } + + var getLocalizedEntry = function(theArray){ + var i; + + // + // try by full name + // + for (i in theArray) { + if (theArray[i].lang == lang) { + return theArray[i].value; + } + } + // + // then by major language + // + if (typeof majorLang != 'undefined') { + for (i in theArray) { + if (theArray[i].lang == majorLang) { + return theArray[i].value; + } + } + } + // + // then by null language in metadata + // + for (i in theArray) { + if (theArray[i].lang == null) { + return theArray[i].value; + } + } + + // + // then by default language + // + for (i in theArray) { + if (theArray[i].lang == defaultLang) { + return theArray[i].value; + } + } + + return null; + }; + + + // ************************************* + // Private functions + // + // Cookie and preferred IdP Handling + // + // ************************************* + + /** + Gets the preferred IdPs. The first elements in the array will + be the preselected preferred IdPs. The following elements will + be those past IdPs selected by a user. The size of the array + will be no larger than the maximum number of preferred IdPs. + */ + var getPreferredIdPs = function() { + var idps = []; + var offset = 0; + var i; + var j; + + // + // populate start of array with preselected IdPs + // + if(null != preferredIdP){ + for(i=0; i < preferredIdP.length && i < maxPreferredIdPs; i++){ + idps[i] = getIdPFor(preferredIdP[i]); + offset++; + } + } + + // + // And then the cookie based ones + // + userSelectedIdPs = retrieveUserSelectedIdPs(); + for (i = offset, j=0; j < userSelectedIdPs.length && i < maxPreferredIdPs; j++){ + var cur_idp = getIdPFor(userSelectedIdPs[j]); + if (typeof idps.indexOf === 'undefined') { + idps.push(cur_idp); + i++; + } + else if (idps.indexOf(cur_idp) === -1) { + idps.push(cur_idp); + i++; + } + } + return idps; + }; + + /** + Update the userSelectedIdPs list with the new value. + + @param (String) the newly selected IdP + */ + var updateSelectedIdPs = function(newIdP) { + + // + // We cannot use split since it does not appear to + // work as per spec on ie8. + // + var newList = []; + + // + // iterate through the list copying everything but the old + // name + // + while (0 !== userSelectedIdPs.length) { + var what = userSelectedIdPs.pop(); + if (what != newIdP) { + newList.unshift(what); + } + } + + // + // And shove it in at the top + // + newList.unshift(newIdP); + userSelectedIdPs = newList; + return; + }; + + /* + Set the autoFollowCookie with a life of the specified number of days + or clear it. + + @parm (integer) days The cookie lifetime if >0. If <=0 clear cookie + + */ + + var setAutoDispatchCookie = function(days) { + var expireDate; + if(days > 0){ + var now = new Date(); + cookieTTL = days * 24 * 60 * 60 * 1000; + expireDate = new Date(now.getTime() + cookieTTL); + } else { + expireDate = new Date(0); + } + document.cookie=autoFollowCookie + '=1;path=/;expires=' + expireDate.toUTCString(); + } + + /** + Gets the value of the cookie with the provided name + + @param (string) name - the name to look for + @return the value or null if no cookie of that name + */ + + var getCookieCalled = function (name) { + + var i, j; + var cookies; + + cookies = document.cookie.split( ';' ); + for (i = 0; i < cookies.length; i++) { + // + // Do not use split('='), '=' is valid in Base64 encoding! + // + var cookie = cookies[i]; + var splitPoint = cookie.indexOf( '=' ); + var cookieName = cookie.substring(0, splitPoint); + + if ( name == ( cookieName.replace(/^\s+|\s+$/g, ''))) { + return cookie.substring(splitPoint+1); + } + } + return null; + } + + /** + Gets the IdP previously selected by the user. + + @return {Array} user selected IdPs identified by their entity ID + */ + var retrieveUserSelectedIdPs = function(){ + var userSelectedIdPs = []; + var j; + + var cookieValues = getCookieCalled( '_saml_idp' ); + + if ( cookieValues != null) { + cookieValues = cookieValues.replace(/^\s+|\s+$/g, ''); + cookieValues = cookieValues.replace('+','%20'); + cookieValues = cookieValues.split('%20'); + for(j=cookieValues.length; j > 0; j--){ + if (0 === cookieValues[j-1].length) { + continue; + } + var dec = base64Decode(decodeURIComponent(cookieValues[j-1])); + if (dec.length > 0) { + userSelectedIdPs.push(dec); + } + } + } + + return userSelectedIdPs; + }; + + /** + Saves the IdPs selected by the user. + + @param {Array} idps idps selected by the user + */ + var saveUserSelectedIdPs = function(idps){ + var cookieData = []; + var length = idps.length; + + if (noWriteCookie) { + return; + } + + if (length > 5) { + length = 5; + } + for(var i=length; i > 0; i--){ + if (idps[i-1].length > 0) { + cookieData.push(encodeURIComponent(base64Encode(idps[i-1]))); + } + } + + var expireDate = null; + if(samlIdPCookieTTL){ + var now = new Date(); + cookieTTL = samlIdPCookieTTL * 24 * 60 * 60 * 1000; + expireDate = new Date(now.getTime() + cookieTTL); + } + + document.cookie='_saml_idp' + '=' + cookieData.join('%20') + '; path = /' + + ((expireDate===null) ? '' : '; expires=' + expireDate.toUTCString()); + + }; + + /** + Base64 encodes the given string. + + @param {String} input string to be encoded + + @return {String} base64 encoded string + */ + var base64Encode = function(input) { + var output = '', c1, c2, c3, e1, e2, e3, e4; + + for ( var i = 0; i < input.length; ) { + c1 = input.charCodeAt(i++); + c2 = input.charCodeAt(i++); + c3 = input.charCodeAt(i++); + e1 = c1 >> 2; + e2 = ((c1 & 3) << 4) + (c2 >> 4); + e3 = ((c2 & 15) << 2) + (c3 >> 6); + e4 = c3 & 63; + if (isNaN(c2)){ + e3 = e4 = 64; + } else if (isNaN(c3)){ + e4 = 64; + } + output += base64chars.charAt(e1) + + base64chars.charAt(e2) + + base64chars.charAt(e3) + + base64chars.charAt(e4); + } + + return output; + }; + + /** + Base64 decodes the given string. + + @param {String} input string to be decoded + + @return {String} base64 decoded string + */ + var base64Decode = function(input) { + var output = '', chr1, chr2, chr3, enc1, enc2, enc3, enc4; + var i = 0; + + // Remove all characters that are not A-Z, a-z, 0-9, +, /, or = + var base64test = /[^A-Za-z0-9\+\/\=]/g; + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); + + do { + enc1 = base64chars.indexOf(input.charAt(i++)); + enc2 = base64chars.indexOf(input.charAt(i++)); + enc3 = base64chars.indexOf(input.charAt(i++)); + enc4 = base64chars.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 != 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 != 64) { + output = output + String.fromCharCode(chr3); + } + + chr1 = chr2 = chr3 = ''; + enc1 = enc2 = enc3 = enc4 = ''; + + } while (i < input.length); + + return output; + }; + + // ************************************* + // Private functions + // + // Error Handling. we'll keep it separate with a view to eventual + // exbedding into log4js + // + // ************************************* + + var fatalNoAlert = function(message) { + if (idpSelectDiv) { + var txt = document.createTextNode(message); + idpSelectDiv.appendChild(txt); + } else { + alert('FATAL (NoDiv):' + message); + } + }; + + var fatal = function(message) { + alert('FATAL - DISCO UI:' + message); + if (idpSelectDiv) { + fatalNoAlert(message); + } + }; + + var debug = function() { + // + // Nothing + }; +} + +(new IdPSelectUI()).draw(new IdPSelectUIParms()); diff --git a/conf/etc/shibboleth-ds/nonminimised/idpselect_config.js b/conf/etc/shibboleth-ds/nonminimised/idpselect_config.js new file mode 100644 index 0000000000000000000000000000000000000000..f4252195f2498d7dcada5a6ab272556f01a3f6d3 --- /dev/null +++ b/conf/etc/shibboleth-ds/nonminimised/idpselect_config.js @@ -0,0 +1,81 @@ + +/** @class IdP Selector UI */ +function IdPSelectUIParms(){ + // + // Adjust the following to fit into your local configuration + // + this.alwaysShow = true; // If true, this will show results as soon as you start typing + this.dataSource = '/Shibboleth.sso/DiscoFeed'; // Where to get the data from + this.defaultLanguage = 'en'; // Language to use if the browser local doesnt have a bundle + this.defaultLogo = 'blank.gif'; // Replace with your own logo + this.defaultLogoWidth = 1; + this.defaultLogoHeight = 1 ; + this.defaultReturn = null; // If non null, then the default place to send users who are not + // Approaching via the Discovery Protocol for example + //this.defaultReturn = "https://example.org/Shibboleth.sso/DS?SAMLDS=1&target=https://example.org/secure"; + this.defaultReturnIDParam = null; + this.returnWhiteList = [ "^https:\/\/example\.org\/Shibboleth\.sso\/Login.*$" , "^https:\/\/example\.com\/Shibboleth\.sso\/Login.*$" ]; + this.helpURL = 'https://wiki.shibboleth.net/confluence/display/SHIB2/DSRoadmap'; + this.ie6Hack = null; // An array of structures to disable when drawing the pull down (needed to + // handle the ie6 z axis problem + this.insertAtDiv = 'idpSelect'; // The div where we will insert the data + this.maxResults = 10; // How many results to show at once or the number at which to + // start showing if alwaysShow is false + this.myEntityID = null; // If non null then this string must match the string provided in the DS parms + this.preferredIdP = null; // Array of entityIds to always show + this.hiddenIdPs = null; // Array of entityIds to delete + this.ignoreKeywords = false; // Do we ignore the <mdui:Keywords/> when looking for candidates + this.showListFirst = false; // Do we start with a list of IdPs or just the dropdown + this.samlIdPCookieTTL = 730; // in days + this.setFocusTextBox = true; // Set to false to supress focus + this.testGUI = false; + + this.autoFollowCookie = null; // If you want auto-dispatch, set this to the cookie name to use + this.autoFollowCookieTTLs = [ 1, 60, 270 ]; // Cookie life (in days). Changing this requires changes to idp_select_languages + + // + // Language support. + // + // The minified source provides "en", "de", "pt-br" and "jp". + // + // Override any of these below, or provide your own language + // + //this.langBundles = { + //'en': { + // 'fatal.divMissing': '<div> specified as "insertAtDiv" could not be located in the HTML', + // 'fatal.noXMLHttpRequest': 'Browser does not support XMLHttpRequest, unable to load IdP selection data', + // 'fatal.wrongProtocol' : 'Policy supplied to DS was not "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"', + // 'fatal.wrongEntityId' : 'entityId supplied by SP did not match configuration', + // 'fatal.noData' : 'Metadata download returned no data', + // 'fatal.loadFailed': 'Failed to download metadata from ', + // 'fatal.noparms' : 'No parameters to discovery session and no defaultReturn parameter configured', + // 'fatal.noReturnURL' : "No URL return parameter provided", + // 'fatal.badProtocol' : "Return request must start with https:// or http://", + // 'idpPreferred.label': 'Use a suggested selection:', + // 'idpEntry.label': 'Or enter your organization\'s name', + // 'idpEntry.NoPreferred.label': 'Enter your organization\'s name', + // 'idpList.label': 'Or select your organization from the list below', + // 'idpList.NoPreferred.label': 'Select your organization from the list below', + // 'idpList.defaultOptionLabel': 'Please select your organization...', + // 'idpList.showList' : 'Allow me to pick from a list', + // 'idpList.showSearch' : 'Allow me to specify the site', + // 'submitButton.label': 'Continue', + // 'helpText': 'Help', + // 'defaultLogoAlt' : 'DefaultLogo' + //} + //}; + + // + // The following should not be changed without changes to the css. Consider them as mandatory defaults + // + this.maxPreferredIdPs = 3; + this.maxIdPCharsButton = 33; + this.maxIdPCharsDropDown = 58; + this.maxIdPCharsAltTxt = 60; + + this.minWidth = 20; + this.minHeight = 20; + this.maxWidth = 115; + this.maxHeight = 69; + this.bestRatio = Math.log(80 / 60); +} diff --git a/conf/etc/shibboleth-ds/nonminimised/idpselect_languages.js b/conf/etc/shibboleth-ds/nonminimised/idpselect_languages.js new file mode 100644 index 0000000000000000000000000000000000000000..ac4f2551e00d8887885de9c0e4affe6e0519a0eb --- /dev/null +++ b/conf/etc/shibboleth-ds/nonminimised/idpselect_languages.js @@ -0,0 +1,115 @@ + +/** @class IdP Selector UI */ +function IdPSelectLanguages(){ + // + // Globalization stuff + // + this.langBundles = { + 'en': { + 'fatal.divMissing': '<div> specified as "insertAtDiv" could not be located in the HTML', + 'fatal.noXMLHttpRequest': 'Browser does not support XMLHttpRequest, unable to load IdP selection data', + 'fatal.wrongProtocol' : 'Policy supplied to DS was not "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"', + 'fatal.wrongEntityId' : 'entityId supplied by SP did not match configuration', + 'fatal.noData' : 'Metadata download returned no data', + 'fatal.loadFailed': 'Failed to download metadata from ', + 'fatal.noparms' : 'No parameters to discovery session and no defaultReturn parameter configured', + 'fatal.noReturnURL' : "No URL return parameter provided", + 'fatal.badProtocol' : "Return parameter must start with https:// or http://", + 'fatal.badReturnString' : "Return parameter is not whitelisted", + 'idpPreferred.label': 'Use a suggested selection:', + 'idpEntry.label': 'Or enter your organization\'s name', + 'idpEntry.NoPreferred.label': 'Enter your organization\'s name', + 'idpList.label': 'Or select your organization from the list below', + 'idpList.NoPreferred.label': 'Select your organization from the list below', + 'idpList.defaultOptionLabel': 'Please select your organization...', + 'idpList.showList' : 'Allow me to pick from a list', + 'idpList.showSearch' : 'Allow me to specify the site', + 'submitButton.label': 'Continue', + 'helpText': 'Help', + 'defaultLogoAlt' : 'DefaultLogo', + 'autoFollow.message' : 'Always follows this selection', + 'autoFollow.never' : 'Never', + 'autoFollow.time0' : 'One day', + 'autoFollow.time1' : '3 months', + 'autoFollow.time2' : '9 months' + }, + 'de': { + 'fatal.divMissing': 'Das notwendige Div Element fehlt', + 'fatal.noXMLHttpRequest': 'Ihr Webbrowser unterst\u00fctzt keine XMLHttpRequests, IdP-Auswahl kann nicht geladen werden', + 'fatal.wrongProtocol' : 'DS bekam eine andere Policy als "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"', + 'fatal.wrongEntityId' : 'Die entityId ist nicht korrekt', + 'fatal.noData' : 'Heruntergeladene Metadata waren leer', + 'fatal.loadFailed': 'Metadaten konnten nicht heruntergeladen werden: ', + 'fatal.noparms' : 'Parameter f\u00fcr das Discovery Service oder \'defaultReturn\' fehlen', + 'fatal.noReturnURL' : "URL return Parmeter fehlt", + 'fatal.badProtocol' : "return Request muss mit https:// oder http:// beginnen", + 'fatal.badReturnString' : "Return Parameter ist nicht auf Positivliste enthalten", + 'idpPreferred.label': 'Vorherige Auswahl:', + 'idpEntry.label': 'Oder geben Sie den Namen (oder Teile davon) an:', + 'idpEntry.NoPreferred.label': 'Namen (oder Teile davon) der Institution angeben:', + 'idpList.label': 'Oder w\u00e4hlen Sie Ihre Institution aus einer Liste:', + 'idpList.NoPreferred.label': 'Institution aus folgender Liste w\u00e4hlen:', + 'idpList.defaultOptionLabel': 'W\u00e4hlen Sie Ihre Institution aus...', + 'idpList.showList' : 'Institution aus einer Liste w\u00e4hlen', + 'idpList.showSearch' : 'Institution selbst angeben', + 'submitButton.label': 'OK', + 'helpText': 'Hilfe', + 'defaultLogoAlt' : 'Standard logo', + 'autoFollow.message' : 'Auswahl merken und diesen Dialog nicht mehr anzeigen', + 'autoFollow.never' : 'Nicht merken', + 'autoFollow.time0' : 'Nur f\u00fcr Heute', + 'autoFollow.time1' : 'Drei Monate lang', + 'autoFollow.time2' : 'Neun Monate lang' + }, + 'ja': { + 'fatal.divMissing': '"insertAtDiv" の ID を持つ <div> が HTML 中に存在しません', + 'fatal.noXMLHttpRequest': 'ブラウザが XMLHttpRequest をサポートしていないので IdP 情報を取得できません', + 'fatal.wrongProtocol' : 'DSへ渡された Policy パラメータが "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single" ではありません', + 'fatal.wrongEntityId' : 'SP から渡された entityId が設定値と異なります', + 'fatal.noData' : 'メタデータが空です', + 'fatal.loadFailed': '次の URL からメタデータをダウンロードできませんでした: ', + 'fatal.noparms' : 'DSにパラメータが渡されておらず defaultReturn も設定されていません', + 'fatal.noReturnURL' : "戻り URL が指定されていません", + 'fatal.badProtocol' : "戻り URL は https:// か http:// で始まらなければなりません", + 'idpPreferred.label': '選択候補の IdP:', + 'idpEntry.label': 'もしくはあなたの所属機関名を入力してください', + 'idpEntry.NoPreferred.label': 'あなたの所属機関名を入力してください', + 'idpList.label': 'もしくはあなたの所属機関を選択してください', + 'idpList.NoPreferred.label': 'あなたの所属機関を一覧から選択してください', + 'idpList.defaultOptionLabel': '所属機関を選択してください...', + 'idpList.showList' : '一覧から選択する', + 'idpList.showSearch' : '機関名を入力する', + 'submitButton.label': '選択', + 'autoFollow.message' : '次の期間選択した機関に自動的に遷移する:', + 'autoFollow.never' : '自動遷移しない', + 'autoFollow.time0' : '1日', + 'autoFollow.time1' : '3か月', + 'autoFollow.time2' : '9か月', + 'fatal.badReturnString' : "戻り URL が不正です", + 'helpText': 'ヘルプ', + 'defaultLogoAlt' : 'ロゴ未設定' + }, + 'pt-br': { + 'fatal.divMissing': 'A tag <div> com "insertAtDiv" não foi encontrada no arquivo HTML', + 'fatal.noXMLHttpRequest': 'Seu navegador não suporta "XMLHttpRequest", impossível de carregador os dados do IdP selecionado', + 'fatal.wrongProtocol' : 'A política "Policy" fornecida para o DS não foi "urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"', + 'fatal.wrongEntityId' : 'entityId oferecido pelo SP não confere com o da configuração', + 'fatal.noData' : 'O arquivo de metadados não retornou nada;', + 'fatal.loadFailed': 'Falhou ao realizar download do metadado de ', + 'fatal.noparms' : 'Sem parâmetros para sessão de descoberta e sem parâmetro "defaultReturn" configurado', + 'fatal.noReturnURL' : "Não foi definida um endereço (URL) de retorno no parâmetro", + 'fatal.badProtocol' : "Retorno do endereço requisitado deve começar com https:// ou http://", + 'idpPreferred.label': 'Use estas Instituições sugeridas: ', + 'idpEntry.label': 'Ou informe o nome da sua Instituição', + 'idpEntry.NoPreferred.label': 'Informe o nome da sua Instituição', + 'idpList.label': 'Ou selecione sua Instituição através da lista abaixo', + 'idpList.NoPreferred.label': 'Selecione sua Instituição através da lista abaixo', + 'idpList.defaultOptionLabel': 'Por favor, selecione sua Instituição: ', + 'idpList.showList' : 'Permitir que eu escolha um IdP através de uma lista', + 'idpList.showSearch' : 'Permitir que eu especifique o IdP', + 'submitButton.label': 'Continuar ', + 'helpText': 'Ajuda', + 'defaultLogoAlt' : 'Logo padrão' + } + }; +} diff --git a/conf/etc/shibboleth-ds/nonminimised/json2.js b/conf/etc/shibboleth-ds/nonminimised/json2.js new file mode 100644 index 0000000000000000000000000000000000000000..5e61c56fc67fcce0e3cf4de8595cb77b1dc73164 --- /dev/null +++ b/conf/etc/shibboleth-ds/nonminimised/json2.js @@ -0,0 +1,481 @@ +/* + http://www.JSON.org/json2.js + 2011-02-23 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, strict: false, regexp: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +var JSON; +if (!JSON) { + JSON = {}; +} + +(function () { + "use strict"; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); + diff --git a/conf/etc/shibboleth-ds/nonminimised/typeahead.js b/conf/etc/shibboleth-ds/nonminimised/typeahead.js new file mode 100644 index 0000000000000000000000000000000000000000..329ecd82425c3385514ccf974799ac120ea30711 --- /dev/null +++ b/conf/etc/shibboleth-ds/nonminimised/typeahead.js @@ -0,0 +1,426 @@ +function TypeAheadControl(jsonObj, box, orig, submit, maxchars, getName, getEntityId, geticon, ie6hack, alwaysShow, maxResults, getKeywords) +{ + // + // Squirrel away the parameters we were given + // + this.elementList = jsonObj; + this.textBox = box; + this.origin = orig; + this.submit = submit; + this.results = 0; + this.alwaysShow = alwaysShow; + this.maxResults = maxResults; + this.ie6hack = ie6hack; + this.maxchars = maxchars; + this.getName = getName; + this.getEntityId = getEntityId; + this.geticon = geticon; + this.getKeywords = getKeywords; +} + +TypeAheadControl.prototype.draw = function(setFocus) { + + // + // Make a closure on this so that the embedded functions + // get access to it. + // + var myThis = this; + + + // + // Set up the 'dropDown' + // + this.dropDown = document.createElement('ul'); + this.dropDown.className = 'IdPSelectDropDown'; + this.dropDown.style.visibility = 'hidden'; + + this.dropDown.style.width = this.textBox.offsetWidth; + this.dropDown.current = -1; + this.textBox.setAttribute('role', 'listbox'); + document.body.appendChild(this.dropDown); + + // + // Set ARIA on the input + // + this.textBox.setAttribute('role', 'combobox'); + this.textBox.setAttribute('aria-controls', 'IdPSelectDropDown'); + this.textBox.setAttribute('aria-owns', 'IdPSelectDropDown'); + + // + // mouse listeners for the dropdown box + // + this.dropDown.onmouseover = function(event) { + if (!event) { + event = window.event; + } + var target; + if (event.target){ + target = event.target; + } + if (typeof target == 'undefined') { + target = event.srcElement; + } + myThis.select(target); + }; + + this.dropDown.onmousedown = function(event) { + if (-1 != myThis.dropDown.current) { + myThis.textBox.value = myThis.results[myThis.dropDown.current][0]; + } + }; + + // + // Add the listeners to the text box + // + this.textBox.onkeyup = function(event) { + // + // get window event if needed (because of browser oddities) + // + if (!event) { + event = window.event; + } + myThis.handleKeyUp(event); + }; + + this.textBox.onkeydown = function(event) { + if (!event) { + event = window.event; + } + + myThis.handleKeyDown(event); + }; + + this.textBox.onblur = function() { + myThis.hideDrop(); + }; + + this.textBox.onfocus = function() { + myThis.handleChange(); + }; + + if (null == setFocus || setFocus) { + this.textBox.focus(); + } +}; + +// +// Given a name return the first maxresults, or all possibles +// +TypeAheadControl.prototype.getPossible = function(name) { + var possibles = []; + var inIndex = 0; + var outIndex = 0; + var strIndex = 0; + var str; + var ostr; + + name = name.toLowerCase(); + + while (outIndex <= this.maxResults && inIndex < this.elementList.length) { + var hit = false; + var thisName = this.getName(this.elementList[inIndex]); + + // + // Check name + // + if (thisName.toLowerCase().indexOf(name) != -1) { + hit = true; + } + // + // Check entityID + // + if (!hit && this.getEntityId(this.elementList[inIndex]).toLowerCase().indexOf(name) != -1) { + hit = true; + } + + if (!hit) { + var thisKeywords = this.getKeywords(this.elementList[inIndex]); + if (null != thisKeywords && + thisKeywords.toLowerCase().indexOf(name) != -1) { + hit = true; + } + } + + if (hit) { + possibles[outIndex] = [thisName, this.getEntityId(this.elementList[inIndex]), this.geticon(this.elementList[inIndex])]; + outIndex ++; + } + + inIndex ++; + } + // + // reset the cursor to the top + // + this.dropDown.current = -1; + + return possibles; +}; + +TypeAheadControl.prototype.handleKeyUp = function(event) { + var key = event.keyCode; + + if (27 == key) { + // + // Escape - clear + // + this.textBox.value = ''; + this.handleChange(); + } else if (8 == key || 32 == key || (key >= 46 && key < 112) || key > 123) { + // + // Backspace, Space and >=Del to <F1 and > F12 + // + this.handleChange(); + } +}; + +TypeAheadControl.prototype.handleKeyDown = function(event) { + + var key = event.keyCode; + + if (38 == key) { + // + // up arrow + // + this.upSelect(); + + } else if (40 == key) { + // + // down arrow + // + this.downSelect(); + } +}; + +TypeAheadControl.prototype.hideDrop = function() { + var i = 0; + if (null !== this.ie6hack) { + while (i < this.ie6hack.length) { + this.ie6hack[i].style.visibility = 'visible'; + i++; + } + } + this.dropDown.style.visibility = 'hidden'; + this.textBox.setAttribute('aria-expanded', 'false'); + + + if (-1 == this.dropDown.current) { + this.doUnselected(); + } +}; + +TypeAheadControl.prototype.showDrop = function() { + var i = 0; + if (null !== this.ie6hack) { + while (i < this.ie6hack.length) { + this.ie6hack[i].style.visibility = 'hidden'; + i++; + } + } + this.dropDown.style.visibility = 'visible'; + this.dropDown.style.width = this.textBox.offsetWidth +"px"; + this.textBox.setAttribute('aria-expanded', 'true'); +}; + + +TypeAheadControl.prototype.doSelected = function() { + this.submit.disabled = false; +}; + +TypeAheadControl.prototype.doUnselected = function() { + this.submit.disabled = true; + this.textBox.setAttribute('aria-activedescendant', ''); +}; + +TypeAheadControl.prototype.handleChange = function() { + + var val = this.textBox.value; + var res = this.getPossible(val); + + + if (0 === val.length || + 0 === res.length || + (!this.alwaysShow && this.maxResults < res.length)) { + this.hideDrop(); + this.doUnselected(); + this.results = []; + this.dropDown.current = -1; + } else { + this.results = res; + this.populateDropDown(res); + if (1 == res.length) { + this.select(this.dropDown.childNodes[0]); + this.doSelected(); + } else { + this.doUnselected(); + } + } +}; + +// +// A lot of the stuff below comes from +// http://www.webreference.com/programming/javascript/ncz/column2 +// +// With thanks to Nicholas C Zakas +// +TypeAheadControl.prototype.populateDropDown = function(list) { + this.dropDown.innerHTML = ''; + var i = 0; + var li; + var img; + var str; + + while (i < list.length) { + li = document.createElement('li'); + li.id='IdPSelectOption' + i; + str = list[i][0]; + + if (null !== list[i][2]) { + + img = document.createElement('img'); + img.src = list[i][2]; + img.width = 16; + img.height = 16; + img.alt = ''; + li.appendChild(img); + // + // trim string back further in this case + // + if (str.length > this.maxchars - 2) { + str = str.substring(0, this.maxchars - 2); + } + str = ' ' + str; + } else { + if (str.length > this.maxchars) { + str = str.substring(0, this.maxchars); + } + } + li.appendChild(document.createTextNode(str)); + li.setAttribute('role', 'option'); + this.dropDown.appendChild(li); + i++; + } + var off = this.getXY(); + this.dropDown.style.left = off[0] + 'px'; + this.dropDown.style.top = off[1] + 'px'; + this.showDrop(); +}; + +TypeAheadControl.prototype.getXY = function() { + + var node = this.textBox; + var sumX = 0; + var sumY = node.offsetHeight; + + while(node.tagName != 'BODY') { + sumX += node.offsetLeft; + sumY += node.offsetTop; + node = node.offsetParent; + } + // + // And add in the offset for the Body + // + sumX += node.offsetLeft; + sumY += node.offsetTop; + + return [sumX, sumY]; +}; + +TypeAheadControl.prototype.select = function(selected) { + var i = 0; + var node; + this.dropDown.current = -1; + this.doUnselected(); + while (i < this.dropDown.childNodes.length) { + node = this.dropDown.childNodes[i]; + if (node == selected) { + // + // Highlight it + // + node.className = 'IdPSelectCurrent'; + node.setAttribute('aria-selected', 'true'); + this.textBox.setAttribute('aria-activedescendant', 'IdPSelectOption' + i); + + // + // turn on the button + // + this.doSelected(); + // + // setup the cursor + // + this.dropDown.current = i; + // + // and the value for the Server + // + this.origin.value = this.results[i][1]; + this.origin.textValue = this.results[i][0]; + } else { + node.setAttribute('aria-selected', 'false'); + node.className = ''; + } + i++; + } + this.textBox.focus(); +}; + +TypeAheadControl.prototype.downSelect = function() { + if (this.results.length > 0) { + + if (-1 == this.dropDown.current) { + // + // mimic a select() + // + this.dropDown.current = 0; + this.dropDown.childNodes[0].className = 'IdPSelectCurrent'; + this.dropDown.childNodes[0].setAttribute('aria-selected', 'true'); + this.textBox.setAttribute('aria-activedescendant', 'IdPSelectOption' + 0); + this.doSelected(); + this.origin.value = this.results[0][1]; + this.origin.textValue = this.results[0][0]; + + } else if (this.dropDown.current < (this.results.length-1)) { + // + // turn off highlight + // + this.dropDown.childNodes[this.dropDown.current].className = ''; + // + // move cursor + // + this.dropDown.current++; + // + // and 'select' + // + this.dropDown.childNodes[this.dropDown.current].className = 'IdPSelectCurrent'; + this.dropDown.childNodes[this.dropDown.current].setAttribute('aria-selected', 'true'); + this.textBox.setAttribute('aria-activedescendant', 'IdPSelectOption' + this.dropDown.current); + this.doSelected(); + this.origin.value = this.results[this.dropDown.current][1]; + this.origin.textValue = this.results[this.dropDown.current][0]; + + } + } +}; + + +TypeAheadControl.prototype.upSelect = function() { + if ((this.results.length > 0) && + (this.dropDown.current > 0)) { + + // + // turn off highlight + // + this.dropDown.childNodes[this.dropDown.current].className = ''; + // + // move cursor + // + this.dropDown.current--; + // + // and 'select' + // + this.dropDown.childNodes[this.dropDown.current].className = 'IdPSelectCurrent'; + this.dropDown.childNodes[this.dropDown.current].setAttribute('aria-selected', 'true'); + this.textBox.setAttribute('aria-activedescendant', 'IdPSelectOption' + this.dropDown.current); + this.doSelected(); + this.origin.value = this.results[this.dropDown.current][1]; + this.origin.textValue = this.results[this.dropDown.current][0]; + } +}; diff --git a/conf/etc/shibboleth/keygen.sh b/conf/etc/shibboleth/keygen.sh new file mode 100755 index 0000000000000000000000000000000000000000..8624b81cacd0bbbfa49e8f6cece970fa83cdce79 --- /dev/null +++ b/conf/etc/shibboleth/keygen.sh @@ -0,0 +1,91 @@ +#! /bin/sh + +while getopts n:h:u:g:o:e:y:bf c + do + case $c in + u) USER=$OPTARG;; + g) GROUP=$OPTARG;; + o) OUT=$OPTARG;; + b) BATCH=1;; + f) FORCE=1;; + h) FQDN=$OPTARG;; + e) ENTITYID=$OPTARG;; + y) YEARS=$OPTARG;; + n) PREFIX=$OPTARG;; + \?) echo "keygen [-o output directory (default .)] [-u username to own keypair] [-g owning groupname] [-h hostname for cert] [-y years to issue cert] [-e entityID to embed in cert] [-n filename prefix (default 'sp')]" + exit 1;; + esac + done + +if [ -z "$OUT" ] ; then + OUT=. +fi + +if [ -z "$PREFIX" ]; then + PREFIX="sp" +fi + +if [ -n "$FORCE" ] ; then + rm $OUT/${PREFIX}-key.pem $OUT/${PREFIX}-cert.pem +fi + +if [ -s $OUT/${PREFIX}-key.pem -o -s $OUT/${PREFIX}-cert.pem ] ; then + if [ -z "$BATCH" ] ; then + echo The files $OUT/${PREFIX}-key.pem and/or $OUT/${PREFIX}-cert.pem already exist! + echo Use -f option to force recreation of keypair. + exit 2 + fi + exit 0 +fi + +if [ -z "$FQDN" ] ; then + FQDN=`hostname` +fi + +if [ -z "$YEARS" ] ; then + YEARS=10 +fi + +DAYS=`expr $YEARS \* 365` + +if [ -z "$ENTITYID" ] ; then + ALTNAME=DNS:$FQDN +else + ALTNAME=DNS:$FQDN,URI:$ENTITYID +fi + +SSLCNF=$OUT/${PREFIX}-cert.cnf +cat >$SSLCNF <<EOF +# OpenSSL configuration file for creating keypair +[req] +prompt=no +default_bits=3072 +encrypt_key=no +default_md=sha256 +distinguished_name=dn +# PrintableStrings only +string_mask=MASK:0002 +x509_extensions=ext +[dn] +CN=$FQDN +[ext] +subjectAltName=$ALTNAME +subjectKeyIdentifier=hash +EOF + +touch $OUT/${PREFIX}-key.pem +chmod 600 $OUT/${PREFIX}-key.pem +if [ -z "$BATCH" ] ; then + openssl req -config $SSLCNF -new -x509 -days $DAYS -keyout $OUT/${PREFIX}-key.pem -out $OUT/${PREFIX}-cert.pem +else + openssl req -config $SSLCNF -new -x509 -days $DAYS -keyout $OUT/${PREFIX}-key.pem -out $OUT/${PREFIX}-cert.pem 2> /dev/null +fi +rm $SSLCNF + +if [ -s $OUT/${PREFIX}-key.pem -a -n "$USER" ] ; then + chown $USER $OUT/${PREFIX}-key.pem $OUT/${PREFIX}-cert.pem +fi + +if [ -s $OUT/${PREFIX}-key.pem -a -n "$GROUP" ] ; then + chgrp $GROUP $OUT/${PREFIX}-key.pem $OUT/${PREFIX}-cert.pem +fi diff --git a/conf/faas-registry.cnf b/conf/faas-registry.cnf index 302ad4bc89484f033b2fd9385e261fb1ef54e81f..b14cd145cdc67025ab66f81561d127620d8e2473 100644 --- a/conf/faas-registry.cnf +++ b/conf/faas-registry.cnf @@ -4,7 +4,8 @@ FAAS_REGISTRY_VERSION=1.0-SNAPSHOT FAAS_REGISTRY_PORT=9080 -FAAS_REGISTRY_BASE_URL=http://localhost:9080/rr3/ +FAAS_REGISTRY_HOSTNAME=localhost +FAAS_REGISTRY_BASE_URL=http://localhost:9080 FAAS_REGISTRY_COOKIE_SECURE=FALSE FAAS_REGISTRY_TIMEZONE=Europe/Warsaw diff --git a/docker-compose.yml b/docker-compose.yml index a83e0aa61cf2920ef84faab91f7f8c48729276f1..d5a5748b67adfbc2cdd47a8cb5a4e5285bfd9270 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,4 +48,5 @@ services: FAAS_REGISTRY_RR_SMTP_PORT: 25 FAAS_REGISTRY_RR_MAIL_USER: ${FAAS_REGISTRY_RR_MAIL_USER} FAAS_REGISTRY_RR_MAIL_PASS: ${FAAS_REGISTRY_RR_MAIL_PASS} - FAAS_REGISTRY_RR_MAIL_FROM: ${FAAS_REGISTRY_RR_MAIL_FROM} \ No newline at end of file + FAAS_REGISTRY_RR_MAIL_FROM: ${FAAS_REGISTRY_RR_MAIL_FROM} + FAAS_REGISTRY_HOSTNAME: ${FAAS_REGISTRY_HOSTNAME}