From 79c9cdf47e3c8e2ab3a3b09ca69e7e6ed3b667d1 Mon Sep 17 00:00:00 2001
From: GraceDB <gracedb@gracedb-dev1.ligo.uwm.edu>
Date: Fri, 14 Dec 2018 14:29:42 -0600
Subject: [PATCH] Add shibboleth discovery service

---
 docker/shibboleth-ds/LICENSE.txt              |  174 ++
 docker/shibboleth-ds/Makefile                 |   31 +
 docker/shibboleth-ds/README.txt               |   52 +
 docker/shibboleth-ds/RELEASE-NOTES.txt        |    1 +
 docker/shibboleth-ds/blank.gif                |  Bin 0 -> 43 bytes
 docker/shibboleth-ds/idpselect.css            |  213 +++
 docker/shibboleth-ds/idpselect.js             |    1 +
 docker/shibboleth-ds/idpselect_config.js      |   81 +
 docker/shibboleth-ds/index.html               |   38 +
 .../shibboleth-ds/nonminimised/idpselect.js   | 1582 +++++++++++++++++
 .../nonminimised/idpselect_config.js          |   80 +
 .../nonminimised/idpselect_languages.js       |  106 ++
 docker/shibboleth-ds/nonminimised/json2.js    |  481 +++++
 .../shibboleth-ds/nonminimised/typeahead.js   |  426 +++++
 docker/shibboleth-ds/shibboleth-ds.conf       |   17 +
 .../shibboleth-ds/shibboleth-embedded-ds.spec |  106 ++
 16 files changed, 3389 insertions(+)
 create mode 100644 docker/shibboleth-ds/LICENSE.txt
 create mode 100644 docker/shibboleth-ds/Makefile
 create mode 100644 docker/shibboleth-ds/README.txt
 create mode 100644 docker/shibboleth-ds/RELEASE-NOTES.txt
 create mode 100644 docker/shibboleth-ds/blank.gif
 create mode 100644 docker/shibboleth-ds/idpselect.css
 create mode 100644 docker/shibboleth-ds/idpselect.js
 create mode 100644 docker/shibboleth-ds/idpselect_config.js
 create mode 100644 docker/shibboleth-ds/index.html
 create mode 100644 docker/shibboleth-ds/nonminimised/idpselect.js
 create mode 100644 docker/shibboleth-ds/nonminimised/idpselect_config.js
 create mode 100644 docker/shibboleth-ds/nonminimised/idpselect_languages.js
 create mode 100644 docker/shibboleth-ds/nonminimised/json2.js
 create mode 100644 docker/shibboleth-ds/nonminimised/typeahead.js
 create mode 100644 docker/shibboleth-ds/shibboleth-ds.conf
 create mode 100644 docker/shibboleth-ds/shibboleth-embedded-ds.spec

diff --git a/docker/shibboleth-ds/LICENSE.txt b/docker/shibboleth-ds/LICENSE.txt
new file mode 100644
index 000000000..dd5b3a58a
--- /dev/null
+++ b/docker/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/docker/shibboleth-ds/Makefile b/docker/shibboleth-ds/Makefile
new file mode 100644
index 000000000..24c88c808
--- /dev/null
+++ b/docker/shibboleth-ds/Makefile
@@ -0,0 +1,31 @@
+#
+# Note: Changes made here should be reflected in the windows .bat file at src/build.bat
+#
+INSTALL=/usr/bin/install
+JAVA=java
+TAR=tar
+ZIP=zip
+TARGET=shibboleth-embedded-ds-1.2.0
+prefix=
+sysconfdir=${prefix}/etc
+
+all:
+
+install:	index.html
+	${INSTALL} -d $(DESTDIR)${sysconfdir}/shibboleth-ds
+	${INSTALL} -m 644 *.txt *.html *.css *.gif *.js *.conf $(DESTDIR)${sysconfdir}/shibboleth-ds
+
+clean:
+	rm -rf ${TARGET}
+
+kit:	clean
+	mkdir ${TARGET}
+	mkdir ${TARGET}/nonminimised
+	cat src/javascript/idpselect_languages.js src/javascript/typeahead.js src/javascript/idpselect.js | ${JAVA} -jar build/yuicompressor-2.4.8.jar -o ${TARGET}/idpselect.js --type js
+	cp Makefile shibboleth-embedded-ds.spec LICENSE.txt doc/*.txt src/resources/index.html src/resources/idpselect.css src/resources/blank.gif src/javascript/idpselect_config.js src/apache/*.conf ${TARGET}
+	cp src/javascript/*.js ${TARGET}/nonminimised
+
+dist:	kit
+	${TAR} czf ${TARGET}.tar.gz ${TARGET}/*
+	${ZIP} ${TARGET}.zip ${TARGET}/*
+	rm -rf ${TARGET}
diff --git a/docker/shibboleth-ds/README.txt b/docker/shibboleth-ds/README.txt
new file mode 100644
index 000000000..d765a1630
--- /dev/null
+++ b/docker/shibboleth-ds/README.txt
@@ -0,0 +1,52 @@
+Welcome to the Shibboleth Embedded Discovery Service.
+
+Shibboleth is a federated web authentication and attribute exchange
+system based on SAML. The Embedded Discovery Service allows anyone
+running a suitable SP to quickly and easily deploy IdP discovery.
+
+For full instructions on installation and deployment please read:
+
+https://wiki.shibboleth.net/confluence/display/EDS10
+
+INSTALLED FILES.
+================
+
+The following files will be installed, please consult the documentation
+for more details.
+
+index.html    - An example file showing how to embed the EDS into a 
+              standard webpage.
+
+idpselect.js  - The minified sources for the EDS.  DO NOT EDIT THIS FILE 
+              since it will be updated by future releases.
+
+idpselect_config.js - Configuration.  Edit this file according to the
+              documentation (cited above).
+
+idpselect.css - CSS for the EDS.  You may wish to edit this file.
+
+README.TXT    - This file
+
+RELEASE-NOTES.TXT - The list of bugs fixed in this and previous releases.
+
+FURTHER DETAILS.
+================
+
+Shibboleth is licensed under the Apache 2.0 license which is provided in the
+LICENSE.txt file.
+
+Shibboleth Project Site:
+http://shibboleth.internet2.edu/
+
+Shibboleth Documentation Site:
+https://wiki.shibboleth.net/confluence/display/SHIB2/Home
+
+Source and binary distributions
+http://www.shibboleth.net/downloads/
+
+Bug Tracker:
+https://issues.shibboleth.net/
+
+Other sources:
+This tools embeds the JSON parsing tool from https://github.com/douglascrockford/JSON-js
+The build process uses the yui compressor (http://developer.yahoo.com/yui/compressor/)
diff --git a/docker/shibboleth-ds/RELEASE-NOTES.txt b/docker/shibboleth-ds/RELEASE-NOTES.txt
new file mode 100644
index 000000000..9eca8df76
--- /dev/null
+++ b/docker/shibboleth-ds/RELEASE-NOTES.txt
@@ -0,0 +1 @@
+See https://issues.shibboleth.net/jira/projects/EDS
diff --git a/docker/shibboleth-ds/blank.gif b/docker/shibboleth-ds/blank.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2799b45c6591f1db05c8c00bd1fd0c5c01f57614
GIT binary patch
literal 43
scmZ?wbhEHbWMp7uXkcLY|NlP&1B2pE79h#MpaUX6G7L;iE{qJ;0LYaF_y7O^

literal 0
HcmV?d00001

diff --git a/docker/shibboleth-ds/idpselect.css b/docker/shibboleth-ds/idpselect.css
new file mode 100644
index 000000000..a0accceb9
--- /dev/null
+++ b/docker/shibboleth-ds/idpselect.css
@@ -0,0 +1,213 @@
+/* Top level is idpSelectIdPSelector */
+#idpSelectIdPSelector
+{
+    width: 512px;
+    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/docker/shibboleth-ds/idpselect.js b/docker/shibboleth-ds/idpselect.js
new file mode 100644
index 000000000..db075f996
--- /dev/null
+++ b/docker/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 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","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.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","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"},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か月",helpText:"Help",defaultLogoAlt:"DefaultLogo"},"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 q;var V="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var aH;var R;var az;var al;var Z;var d;var E;var m;var x;var j;var au;var f;var s;var aa;var af;var C;var ae;var Q;var g;var J;var ar;var M;var S;var aj;var av;var ao;var c;var ad;var F;var P;var Y;var O;var T;var i;var ax;var U;var aJ="idpSelect";var ah="IdPSelect";var ap;var A="";var X="";var ay=[];var aE="entityID";this.draw=function(aN){if(!l(aN)){return}aH=document.getElementById(aN.insertAtDiv);if(!aH){N(z("fatal.divMissing"));return}if((null!=P)&&(null!=h(P))){var aK=b();if(aK.length!=0){var aM=aE+"="+encodeURIComponent(aK[0]);if(A.indexOf("?")==-1){aM="?"+aM}else{aM="&"+aM}p(aH,A+aM);return}}if(!e(aN.dataSource)){return}aI();t(aN.hiddenIdPs);q.sort(function(aP,aO){return aC(aP).localeCompare(aC(aO))});var aL=ab();aH.appendChild(aL);ap.draw(aN.setFocusTextBox)};var l=function(aR){var aL;C=aR.preferredIdP;ae=aR.maxPreferredIdPs;Q=aR.helpURL;g=aR.ie6Hack;J=aR.samlIdPCookieTTL;aj=aR.alwaysShow;av=aR.maxResults;ao=aR.ignoreKeywords;if(aR.showListFirst){c=aR.showListFirst}else{c=false}if(aR.noWriteCookie){ad=aR.noWriteCookie}else{ad=false}if(aR.ignoreURLParams){F=aR.ignoreURLParams}else{F=false}E=aR.defaultLogo;m=aR.defaultLogoWidth;x=aR.defaultLogoHeight;j=aR.minWidth;au=aR.minHeight;f=aR.maxWidth;s=aR.maxHeight;aa=aR.bestRatio;if(null==aR.doNotCollapse){af=true}else{af=aR.doNotCollapse}M=aR.maxIdPCharsButton;ar=aR.maxIdPCharsDropDown;S=aR.maxIdPCharsAltTxt;P=aR.autoFollowCookie;Y=aR.autoFollowCookieTTLs;var a1;if(typeof navigator=="undefined"){a1=aR.defaultLanguage}else{a1=navigator.language||navigator.userLanguage||aR.defaultLanguage}a1=a1.toLowerCase();if(a1.indexOf("-")>0){az=a1.substring(0,a1.indexOf("-"))}var aV=new IdPSelectLanguages();al=aR.defaultLanguage;if(typeof aR.langBundles!="undefined"&&typeof aR.langBundles[a1]!="undefined"){Z=aR.langBundles[a1]}else{if(typeof aV.langBundles[a1]!="undefined"){Z=aV.langBundles[a1]}else{if(typeof az!="undefined"){if(typeof aR.langBundles!="undefined"&&typeof aR.langBundles[az]!="undefined"){Z=aR.langBundles[az]}else{if(typeof aV.langBundles[az]!="undefined"){Z=aV.langBundles[az]}}}}}if(typeof aR.langBundles!="undefined"&&typeof aR.langBundles[aR.defaultLanguage]!="undefined"){d=aR.langBundles[aR.defaultLanguage]}else{d=aV.langBundles[aR.defaultLanguage]}if(!d){N("No languages work");return false}if(!Z){r("No language support for "+a1);Z=d}if(aR.testGUI){return true}var aW="urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single";var aZ;var a0=false;var aP;var aS;var aO=window;while(null!==aO.parent&&aO!==aO.parent){aO=aO.parent}var aU=aO.location;var aQ=aU.search;if(F||null==aQ||0==aQ.length||aQ.charAt(0)!="?"){if((null==aR.defaultReturn)&&!F){N(z("fatal.noparms"));return false}aL=aR.myEntityID;A=aR.defaultReturn;if(null!=aR.defaultReturnIDParam){aE=aR.defaultReturnIDParam}}else{aQ=aQ.substring(1);aP=aQ.split("&");if(aP.length===0){N(z("fatal.noparms"));return false}for(aZ=0;aZ<aP.length;aZ++){aS=aP[aZ].split("=");if(aS.length!=2){continue}if(aS[0]=="entityID"){aL=decodeURIComponent(aS[1])}else{if(aS[0]=="return"){A=decodeURIComponent(aS[1])}else{if(aS[0]=="returnIDParam"){aE=decodeURIComponent(aS[1])}else{if(aS[0]=="policy"){aW=decodeURIComponent(aS[1])}else{if(aS[0]=="isPassive"){a0=(aS[1].toUpperCase()=="TRUE")}}}}}}}var aK;if(null==aR.allowableProtocols){aK=["urn:oasis:names:tc:SAML:profiles:SSO:idpdiscovery-protocol:single"]}else{aK=aR.allowableProtocols}var aY=false;for(var aZ=0;aZ<aK.length;aZ++){var aX=aK[aZ];if(aW==aX){aY=true;break}}if(!aY){N(z("fatal.wrongProtocol"));return false}if(aR.myEntityID!==null&&aR.myEntityID!=aL){N(z("fatal.wrongEntityId")+'"'+aL+'" != "'+aR.myEntityID+'"');return false}if(null===A||A.length===0){N(z("fatal.noReturnURL"));return false}if(!am(A)){N(z("fatal.badProtocol"));return false}if(a0){var aN=b();var aT=document.getElementById(parmsSupplied.insertAtDiv);if(aN.length==0){p(aT,A);return false}else{var aM=aE+"="+encodeURIComponent(aN[0]);if(A.indexOf("?")==-1){aM="?"+aM}else{aM="&"+aM}p(aT,A+aM);return false}}aZ=A.indexOf("?");if(aZ<0){X=A;return true}X=A.substring(0,aZ);aQ=A.substring(aZ+1);aP=aQ.split("&");for(aZ=0;aZ<aP.length;aZ++){aS=aP[aZ].split("=");if(aS.length!=2){continue}aS[1]=decodeURIComponent(aS[1]);ay.push(aS)}return true};var aI=function(){var aM=[];var aL;for(aL=0;aL<q.length;){var aK=y(q[aL]);if(null==aM[aK]){aM[aK]=aK;aL=aL+1}else{q.splice(aL,1)}}};var t=function(aM){if(null==aM||0==aM.length){return}var aL;var aK;for(aL=0;aL<aM.length;aL++){for(aK=0;aK<q.length;aK++){if(y(q[aK])==aM[aL]){q.splice(aK,1);break}}}};var am=function(aL){if(null===aL){return false}var aK="://";var aM=aL.indexOf(aK);if(aM<0){return false}aL=aL.substring(0,aM);if(aL=="http"||aL=="https"){return true}return false};var aG=function(){if(null==navigator){return false}var aK=navigator.appName;if(null==aK){return false}return(aK=="Microsoft Internet Explorer")};var p=function(aL,aM){var aK=document.createElement("a");aK.href=aM;aL.appendChild(aK);aK.click()};var e=function(aN){var aM=null;try{aM=new XMLHttpRequest()}catch(aL){}if(null==aM){try{aM=new ActiveXObject("Microsoft.XMLHTTP")}catch(aL){}}if(null==aM){try{aM=new ActiveXObject("MSXML2.XMLHTTP.3.0")}catch(aL){}}if(null==aM){N(z("fatal.noXMLHttpRequest"));return false}if(aG()){aN+="?random="+(Math.random()*1000000)}aM.open("GET",aN,false);if(typeof aM.overrideMimeType=="function"){aM.overrideMimeType("application/json")}aM.send(null);if(aM.status==200){var aK=aM.responseText;if(aK===null){N(z("fatal.noData"));return false}q=JSON.parse(aK)}else{N(z("fatal.loadFailed")+aN);return false}return true};var ac=function(aK){for(var aL=0;aL<q.length;aL++){if(y(q[aL])==aK){return q[aL]}}return null};var G=function(aR,aL){var aQ=function(aU){var aS=null;var aT;if(null==aR.Logos){return null}for(aT in aR.Logos){if(aR.Logos[aT].lang==aU&&aR.Logos[aT].width!=null&&aR.Logos[aT].width>=j&&aR.Logos[aT].height!=null&&aR.Logos[aT].height>=au){if(aS===null){aS=aR.Logos[aT]}else{me=Math.abs(aa-Math.log(aR.Logos[aT].width/aR.Logos[aT].height));him=Math.abs(aa-Math.log(aS.width/aS.height));if(him>me){aS=aR.Logos[aT]}}}}return aS};var aN=null;var aM=document.createElement("img");ak(aM,"IdPImg");aN=aQ(R);if(null===aN&&typeof az!="undefined"){aN=aQ(az)}if(null===aN){aN=aQ(null)}if(null===aN){aN=aQ(al)}if(null===aN){if(!aL){return null}aM.src=E;aM.width=m;aM.height=x;aM.alt=z("defaultLogoAlt");return aM}aM.src=aN.value;var aO=aC(aR);if(aO.length>S){aO=aO.substring(0,S)+"..."}aM.alt=aO;var aK=aN.width;var aP=aN.height;if(aK>f){aP=(f/aK)*aP;aK=f}if(aP>s){aK=(s/aP)*aK;aP=s}aM.setAttribute("width",aK);aM.setAttribute("height",aP);return aM};var ab=function(){var aL=an("IdPSelector");var aK;aK=aA(aL);n(aL,aK);W(aL,aK);if(null!=P){B(aL)}return aL};var L=function(aM,aT,aL){var aK=an(undefined,"PreferredIdPButton");var aS=document.createElement("a");var aR=aE+"="+encodeURIComponent(y(aM));var aN=A;var aP=G(aM,aL);if(aN.indexOf("?")==-1){aR="?"+aR}else{aR="&"+aR}aS.href=aN+aR;aS.onclick=function(){aF(y(aM))};if(null!=aP){var aU=an(undefined,"PreferredIdPImg");aU.appendChild(aP);aS.appendChild(aU)}var aQ=an(undefined,"TextDiv");var aO=aC(aM);if(aO.length>M){aO=aO.substring(0,M)+"..."}aK.title=aO;aQ.appendChild(document.createTextNode(aO));aS.appendChild(aQ);aK.appendChild(aS);return aK};var aD=function(aK,aN){var aM=an(undefined,"TextDiv");var aL=document.createTextNode(z(aN));aM.appendChild(aL);aK.appendChild(aM)};var a=function(aK,aM){if(null===aM||0===aM.length||"-"==aM.value){return}var aL=0;while(aL<aK.options.length){if(aK.options[aL].value==aM){aK.options[aL].selected=true;break}aL++}};var aA=function(aP){var aO=K();if(0===aO.length){return false}var aK=af;for(var aM=0;aM<ae&&aM<aO.length;aM++){if(aO[aM]&&G(aO[aM],false)){aK=true}}var aN;if(aK){aN=an("PreferredIdPTile")}else{aN=an("PreferredIdPTileNoImg")}aD(aN,"idpPreferred.label");for(var aM=0;aM<ae&&aM<aO.length;aM++){if(aO[aM]){var aL=L(aO[aM],aM,aK);aN.appendChild(aL)}}aP.appendChild(aN);return true};var ag=function(){var aL=document.createElement("form");T.appendChild(aL);aL.action=X;aL.method="GET";aL.setAttribute("autocomplete","OFF");var aK=0;for(aK=0;aK<ay.length;aK++){var aM=document.createElement("input");aM.setAttribute("type","hidden");aM.name=ay[aK][0];aM.value=ay[aK][1];aL.appendChild(aM)}return aL};var n=function(aR,aL){T=an("IdPEntryTile");if(c){T.style.display="none"}var aM=document.createElement("label");aM.setAttribute("for",aJ+"Input");if(aL){aD(aM,"idpEntry.label")}else{aD(aM,"idpEntry.NoPreferred.label")}var aP=ag();aP.appendChild(aM);var aO=document.createElement("input");aP.appendChild(aO);aO.type="text";k(aO,"Input");var aQ=document.createElement("input");aQ.setAttribute("type","hidden");aP.appendChild(aQ);aQ.name=aE;aQ.value="-";var aN=u("Select");aN.disabled=true;aP.appendChild(aN);aP.onsubmit=function(){if(null===aQ.value||0===aQ.value.length||"-"==aQ.value){return false}aO.value=aQ.textValue;aF(aQ.value);return true};ap=new TypeAheadControl(q,aO,aQ,aN,ar,aC,y,ai,g,aj,av,H);var aK=document.createElement("a");aK.appendChild(document.createTextNode(z("idpList.showList")));aK.href="#";ak(aK,"DropDownToggle");aK.onclick=function(){T.style.display="none";a(ax,aQ.value);i.style.display="";U.focus();return false};T.appendChild(aK);w(T);aR.appendChild(T)};var W=function(aK,aN){i=an("IdPListTile");if(!c){i.style.display="none"}var aR=document.createElement("label");aR.setAttribute("for",aJ+"Selector");if(aN){aD(aR,"idpList.label")}else{aD(aR,"idpList.NoPreferred.label")}ax=document.createElement("select");k(ax,"Selector");ax.name=aE;i.appendChild(ax);var aS=o("-",z("idpList.defaultOptionLabel"));aS.selected=true;ax.appendChild(aS);var aM;for(var aO=0;aO<q.length;aO++){aM=q[aO];aS=o(y(aM),aC(aM));ax.appendChild(aS)}var aL=ag();aL.appendChild(aR);aL.appendChild(ax);aL.onsubmit=function(){if(ax.selectedIndex<1){return false}aF(ax.options[ax.selectedIndex].value);return true};var aP=u("List");U=aP;aL.appendChild(aP);i.appendChild(aL);var aQ=document.createElement("a");aQ.appendChild(document.createTextNode(z("idpList.showSearch")));aQ.href="#";ak(aQ,"DropDownToggle");aQ.onclick=function(){T.style.display="";i.style.display="none";return false};i.appendChild(aQ);w(i);aK.appendChild(i)};var B=function(aN){var aL="IdPSelectAutoDisp";autoDispatchTile=an(undefined,"autoDispatchArea");autoDispatchTile.appendChild(document.createTextNode(z("autoFollow.message")));var aK=document.createElement("input");aK.setAttribute("type","radio");aK.setAttribute("checked","checked");aK.setAttribute("name",aL);aK.onclick=function(){D(0)};div=an(undefined,"autoDispatchTile");div.appendChild(aK);div.appendChild(document.createTextNode(z("autoFollow.never")));autoDispatchTile.appendChild(div);var aM;for(aM=0;aM<Y.length;aM++){aK=document.createElement("input");aK.setAttribute("type","radio");aK.setAttribute("name",aL);aK.life=Y[aM];aK.onclick=function(){var aO=this.life;D(aO)};div=an(undefined,"autoDispatchTile");div.appendChild(aK);div.appendChild(document.createTextNode(z("autoFollow.time"+aM)));autoDispatchTile.appendChild(div)}aN.appendChild(autoDispatchTile)};var u=function(aL){var aK=document.createElement("input");aK.setAttribute("type","submit");aK.value=z("submitButton.label");k(aK,aL+"Button");return aK};var w=function(aL){var aK=document.createElement("a");aK.href=Q;aK.appendChild(document.createTextNode(z("helpText")));ak(aK,"HelpButton");aL.appendChild(aK)};var an=function(aM,aK){var aL=document.createElement("div");if(undefined!==aM){k(aL,aM)}if(undefined!==aK){ak(aL,aK)}return aL};var o=function(aL,aM){var aK=document.createElement("option");aK.value=aL;if(aM.length>ar){aM=aM.substring(0,ar)}aK.appendChild(document.createTextNode(aM));return aK};var k=function(aL,aK){aL.id=aJ+aK};var ak=function(aL,aK){aL.setAttribute("class",ah+aK)};var aB=function(aK){return document.getElementById(aJ+aK)};var aF=function(aK){I(aK);aq(O)};var z=function(aK){var aL=Z[aK];if(!aL){aL=d[aK]}if(!aL){aL="Missing message for "+aK}return aL};var y=function(aK){return aK.entityID};var ai=function(aM){var aK;if(null==aM.Logos){return null}for(aK=0;aK<aM.Logos.length;aK++){var aL=aM.Logos[aK];if(aL.height=="16"&&aL.width=="16"){if(null==aL.lang||R==aL.lang||(typeof az!="undefined"&&az==aL.lang)||al==aL.lang){return aL.value}}}return null};var aC=function(aL){var aK=aw(aL.DisplayNames);if(null!==aK){return aK}r("No Name entry in any language for "+y(aL));return y(aL)};var H=function(aL){if(ao||null==aL.Keywords){return null}var aK=aw(aL.Keywords);return aK};var aw=function(aK){var aL;for(aL in aK){if(aK[aL].lang==R){return aK[aL].value}}if(typeof az!="undefined"){for(aL in aK){if(aK[aL].lang==az){return aK[aL].value}}}for(aL in aK){if(aK[aL].lang==null){return aK[aL].value}}for(aL in aK){if(aK[aL].lang==al){return aK[aL].value}}return null};var K=function(){var aO=[];var aN=0;var aM;var aL;if(null!=C){for(aM=0;aM<C.length&&aM<ae;aM++){aO[aM]=ac(C[aM]);aN++}}O=b();for(aM=aN,aL=0;aL<O.length&&aM<ae;aL++){var aK=ac(O[aL]);if(typeof aO.indexOf==="undefined"){aO.push(aK);aM++}else{if(aO.indexOf(aK)===-1){aO.push(aK);aM++}}}return aO};var I=function(aK){var aL=[];while(0!==O.length){var aM=O.pop();if(aM!=aK){aL.unshift(aM)}}aL.unshift(aK);O=aL;return};var D=function(aM){var aK;if(aM>0){var aL=new Date();cookieTTL=aM*24*60*60*1000;aK=new Date(aL.getTime()+cookieTTL)}else{aK=new Date(0)}document.cookie=P+"=1;path=/;expires="+aK.toUTCString()};var h=function(aM){var aO,aL;var aP;aP=document.cookie.split(";");for(aO=0;aO<aP.length;aO++){var aN=aP[aO];var aK=aN.indexOf("=");var aQ=aN.substring(0,aK);if(aM==(aQ.replace(/^\s+|\s+$/g,""))){return aN.substring(aK+1)}}return null};var b=function(){var aK=[];var aL;var aM=h("_saml_idp");if(aM!=null){aM=aM.replace(/^\s+|\s+$/g,"");aM=aM.replace("+","%20");aM=aM.split("%20");for(aL=aM.length;aL>0;aL--){if(0===aM[aL-1].length){continue}var aN=at(decodeURIComponent(aM[aL-1]));if(aN.length>0){aK.push(aN)}}}return aK};var aq=function(aP){var aM=[];var aO=aP.length;if(ad){return}if(aO>5){aO=5}for(var aN=aO;aN>0;aN--){if(aP[aN-1].length>0){aM.push(encodeURIComponent(v(aP[aN-1])))}}var aK=null;if(J){var aL=new Date();cookieTTL=J*24*60*60*1000;aK=new Date(aL.getTime()+cookieTTL)}document.cookie="_saml_idp="+aM.join("%20")+"; path = /"+((aK===null)?"":"; expires="+aK.toUTCString())};var v=function(aT){var aK="",aO,aM,aL,aS,aR,aQ,aP;for(var aN=0;aN<aT.length;){aO=aT.charCodeAt(aN++);aM=aT.charCodeAt(aN++);aL=aT.charCodeAt(aN++);aS=aO>>2;aR=((aO&3)<<4)+(aM>>4);aQ=((aM&15)<<2)+(aL>>6);aP=aL&63;if(isNaN(aM)){aQ=aP=64}else{if(isNaN(aL)){aP=64}}aK+=V.charAt(aS)+V.charAt(aR)+V.charAt(aQ)+V.charAt(aP)}return aK};var at=function(aN){var aL="",aU,aS,aQ,aT,aR,aP,aO;var aM=0;var aK=/[^A-Za-z0-9\+\/\=]/g;aN=aN.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{aT=V.indexOf(aN.charAt(aM++));aR=V.indexOf(aN.charAt(aM++));aP=V.indexOf(aN.charAt(aM++));aO=V.indexOf(aN.charAt(aM++));aU=(aT<<2)|(aR>>4);aS=((aR&15)<<4)|(aP>>2);aQ=((aP&3)<<6)|aO;aL=aL+String.fromCharCode(aU);if(aP!=64){aL=aL+String.fromCharCode(aS)}if(aO!=64){aL=aL+String.fromCharCode(aQ)}aU=aS=aQ="";aT=aR=aP=aO=""}while(aM<aN.length);return aL};var N=function(aL){alert("FATAL - DISCO UI:"+aL);var aK=document.createTextNode(aL);aH.appendChild(aK)};var r=function(){}}(new IdPSelectUI()).draw(new IdPSelectUIParms());
\ No newline at end of file
diff --git a/docker/shibboleth-ds/idpselect_config.js b/docker/shibboleth-ds/idpselect_config.js
new file mode 100644
index 000000000..be2cb6668
--- /dev/null
+++ b/docker/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.helpURL = 'https://wiki.shibboleth.net/confluence/display/SHIB2/DiscoveryService'
+    //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 = ['https://login.ligo.org/idp/shibboleth', 'https://login2.ligo.org/idp/shibboleth', 'https://google.cirrusidentity.com/gateway'];
+    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 = 4;
+    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/docker/shibboleth-ds/index.html b/docker/shibboleth-ds/index.html
new file mode 100644
index 000000000..a4f85bc90
--- /dev/null
+++ b/docker/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/docker/shibboleth-ds/nonminimised/idpselect.js b/docker/shibboleth-ds/nonminimised/idpselect.js
new file mode 100644
index 000000000..6f355a44f
--- /dev/null
+++ b/docker/shibboleth-ds/nonminimised/idpselect.js
@@ -0,0 +1,1582 @@
+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){
+
+        if (!setupLocals(parms)) {
+            return;
+        }
+
+        idpSelectDiv = document.getElementById(parms.insertAtDiv);
+        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;
+        }
+
+        //
+        // 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;
+    };
+
+    /**
+     * 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 fatal = function(message) {
+        alert('FATAL - DISCO UI:' + message);
+        var txt = document.createTextNode(message); 
+        idpSelectDiv.appendChild(txt);
+    };
+
+    var debug = function() {
+        //
+        // Nothing
+    };
+}
+
+(new IdPSelectUI()).draw(new IdPSelectUIParms());
diff --git a/docker/shibboleth-ds/nonminimised/idpselect_config.js b/docker/shibboleth-ds/nonminimised/idpselect_config.js
new file mode 100644
index 000000000..90ac048c5
--- /dev/null
+++ b/docker/shibboleth-ds/nonminimised/idpselect_config.js
@@ -0,0 +1,80 @@
+ 
+/** @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.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/docker/shibboleth-ds/nonminimised/idpselect_languages.js b/docker/shibboleth-ds/nonminimised/idpselect_languages.js
new file mode 100644
index 000000000..6041f062c
--- /dev/null
+++ b/docker/shibboleth-ds/nonminimised/idpselect_languages.js
@@ -0,0 +1,106 @@
+ 
+/** @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 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',
+        '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.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",
+        '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'
+        },
+    '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か月',
+        'helpText': 'Help',
+        'defaultLogoAlt' : 'DefaultLogo'
+    },
+    '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/docker/shibboleth-ds/nonminimised/json2.js b/docker/shibboleth-ds/nonminimised/json2.js
new file mode 100644
index 000000000..5e61c56fc
--- /dev/null
+++ b/docker/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 '&nbsp;'),
+                        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/docker/shibboleth-ds/nonminimised/typeahead.js b/docker/shibboleth-ds/nonminimised/typeahead.js
new file mode 100644
index 000000000..329ecd824
--- /dev/null
+++ b/docker/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/docker/shibboleth-ds/shibboleth-ds.conf b/docker/shibboleth-ds/shibboleth-ds.conf
new file mode 100644
index 000000000..fd46068ce
--- /dev/null
+++ b/docker/shibboleth-ds/shibboleth-ds.conf
@@ -0,0 +1,17 @@
+# Basic Apache configuration
+
+<IfModule mod_alias.c>
+  <Location /shibboleth-ds>
+    Allow from all
+    <IfModule mod_shib.c>
+      AuthType shibboleth
+      ShibRequestSetting requireSession false
+      require shibboleth
+    </IfModule>
+  </Location>
+  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
+</IfModule>
diff --git a/docker/shibboleth-ds/shibboleth-embedded-ds.spec b/docker/shibboleth-ds/shibboleth-embedded-ds.spec
new file mode 100644
index 000000000..d6d50ab95
--- /dev/null
+++ b/docker/shibboleth-ds/shibboleth-embedded-ds.spec
@@ -0,0 +1,106 @@
+Name:		shibboleth-embedded-ds
+Version:	1.2.0
+Release:	1
+Summary:	Client-side federation discovery service for SAML-based SSO
+Group:		Productivity/Networking/Security
+Vendor:		Shibboleth Consortium
+License:	Apache-2.0
+URL:		http://shibboleth.net/
+Source:		%{name}-%{version}.tar.gz
+BuildArch:	noarch
+BuildRoot:	%{_tmppath}/%{name}-%{version}-root
+%if "%{_vendor}" == "redhat"
+BuildRequires: redhat-rpm-config
+%{!?_without_builtinapache:BuildRequires: httpd}
+%endif
+%if "%{_vendor}" == "suse"
+%{!?_without_builtinapache:BuildRequires: apache2}
+%endif
+
+%description
+The Embedded Discovery Service is a JS/CSS/HTML-based tool for
+identity provider selection in conjunction with SAML-based web
+single sign-on implementations such as Shibboleth.
+
+%prep
+%setup -q
+
+%build
+
+
+%install
+%{__make} install DESTDIR=$RPM_BUILD_ROOT
+
+# Plug the DS into the built-in Apache on a recognized system.
+touch rpm.filelist
+APACHE_CONFIG="shibboleth-ds.conf"
+%{?_without_builtinapache:APACHE_CONFIG="no"}
+if [ "$APACHE_CONFIG" != "no" ] ; then
+    APACHE_CONFD="no"
+    if [ -d %{_sysconfdir}/httpd/conf.d ] ; then
+            APACHE_CONFD="%{_sysconfdir}/httpd/conf.d"
+    fi
+    if [ -d %{_sysconfdir}/apache2/conf.d ] ; then
+            APACHE_CONFD="%{_sysconfdir}/apache2/conf.d"
+    fi
+    if [ "$APACHE_CONFD" != "no" ] ; then
+        %{__mkdir} -p $RPM_BUILD_ROOT$APACHE_CONFD
+        %{__cp} -p $RPM_BUILD_ROOT%{_sysconfdir}/shibboleth-ds/$APACHE_CONFIG $RPM_BUILD_ROOT$APACHE_CONFD/$APACHE_CONFIG 
+        echo "%config(noreplace) $APACHE_CONFD/$APACHE_CONFIG" > rpm.filelist
+    fi
+fi
+
+%clean
+[ "$RPM_BUILD_ROOT" != "/" ] && %{__rm} -rf $RPM_BUILD_ROOT
+
+%post
+%if "%{_vendor}" == "redhat"
+	# On upgrade, restart components if they're already running.
+    if [ "$1" -gt "1" ] ; then
+        %{!?_without_builtinapache:/sbin/service httpd status 1>/dev/null && /sbin/service httpd restart 1>/dev/null}
+        exit 0
+    fi
+%endif
+
+%preun
+%if "%{_vendor}" == "redhat"
+	if [ "$1" = 0 ] ; then
+        %{!?_without_builtinapache:/sbin/service httpd status 1>/dev/null && /sbin/service httpd restart 1>/dev/null}
+	fi
+%endif
+%if "%{_vendor}" == "suse"
+    if [ "$1" = 0 ] ; then
+        %{!?_without_builtinapache:/sbin/service apache2 status 1>/dev/null && /sbin/service apache2 restart 1>/dev/null}
+    fi
+%endif
+exit 0
+
+%postun
+%if "%{_vendor}" == "suse"
+cd /
+%{!?_without_builtinapache:%restart_on_update apache2}
+%endif
+
+%files -f rpm.filelist
+%defattr(-,root,root,-)
+%dir %{_sysconfdir}/shibboleth-ds
+%{_sysconfdir}/shibboleth-ds/*.txt
+%{_sysconfdir}/shibboleth-ds/*.gif
+%config(noreplace) %{_sysconfdir}/shibboleth-ds/index.html
+%config(noreplace) %{_sysconfdir}/shibboleth-ds/idpselect.css
+%config(noreplace) %{_sysconfdir}/shibboleth-ds/idpselect_config.js
+%config %{_sysconfdir}/shibboleth-ds/idpselect.js
+%config %{_sysconfdir}/shibboleth-ds/shibboleth-ds.conf
+
+%changelog
+* Mon Jun 6 2016 Scott Cantor <cantor.2@osu.edu> - 1.2.0-1
+- Update version
+- Fix license name
+
+* Wed Apr 29 2015  Scott Cantor  <cantor.2@osu.edu>  - 1.1.0-1
+- Update version
+- Stop marking text files as configs
+- Add gif to package
+
+* Mon Apr 11 2011  Scott Cantor  <cantor.2@osu.edu>  - 1.0-1
+- First version.
-- 
GitLab