diff --git a/lib/SimpleSAML/XHTML/IdPDisco.php b/lib/SimpleSAML/XHTML/IdPDisco.php
index 96b76267b5c8ea522665c26a50899be246b2108e..d84fcec45cc14961678a17e71353750ed31838f6 100644
--- a/lib/SimpleSAML/XHTML/IdPDisco.php
+++ b/lib/SimpleSAML/XHTML/IdPDisco.php
@@ -1,5 +1,6 @@
 <?php
 
+
 /**
  * This class implements a generic IdP discovery service, for use in various IdP
  * discovery service pages. This should reduce code duplication.
@@ -12,560 +13,580 @@
  * @author Andreas Ă…kre Solberg <andreas@uninett.no>, UNINETT AS.
  * @package SimpleSAMLphp
  */
-class SimpleSAML_XHTML_IdPDisco {
-
-	/**
-	 * An instance of the configuration class.
-	 *
-	 * @var SimpleSAML_Configuration
-	 */
-	protected $config;
-
-	/**
-	 * The identifier of this discovery service.
-	 *
-	 * @var string
-	 */
-	protected $instance;
-
-
-	/**
-	 * An instance of the metadata handler, which will allow us to fetch metadata about IdPs.
-	 *
-	 * @var SimpleSAML_Metadata_MetaDataStorageHandler
-	 */
-	protected $metadata;
-
-
-	/**
-	 * The users session.
-	 *
-	 * @var SimpleSAML_Session
-	 */
-	protected $session;
-
-
-	/**
-	 * The metadata sets we find allowed entities in, in prioritized order.
-	 *
-	 * @var array
-	 */
-	protected $metadataSets;
-
-
-	/**
-	 * The entity id of the SP which accesses this IdP discovery service.
-	 *
-	 * @var string
-	 */
-	protected $spEntityId;
-	
-	/**
-	 * HTTP parameter from the request, indicating whether the discovery service
-	 * can interact with the user or not.
-	 *
-	 * @var boolean
-	 */
-	protected $isPassive;
-	
-	/**
-	 * The SP request to set the IdPentityID...
-	 *
-	 * @var string|null
-	 */
-	protected $setIdPentityID = NULL;
-
-
-	/**
-	 * The name of the query parameter which should contain the users choice of IdP.
-	 * This option default to 'entityID' for Shibboleth compatibility.
-	 *
-	 * @var string
-	 */
-	protected $returnIdParam;
-
-	/**
-	 * The list of scoped idp's. The intersection between the metadata idpList
-	 * and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to
-	 * the user. If the intersection is empty the metadata idpList is used.
-	 *
-	 * @var array
-	 */
-	protected $scopedIDPList = array();
-	
-	/**
-	 * The URL the user should be redirected to after choosing an IdP.
-	 *
-	 * @var string
-	 */
-	protected $returnURL;
-
-
-	/**
-	 * Initializes this discovery service.
-	 *
-	 * The constructor does the parsing of the request. If this is an invalid request, it will throw an exception.
-	 *
-	 * @param array $metadataSets Array with metadata sets we find remote entities in.
-	 * @param string $instance The name of this instance of the discovery service.
-	 *
-	 * @throws Exception If the request is invalid.
-	 */
-	public function __construct(array $metadataSets, $instance) {
-		assert('is_string($instance)');
-
-		/* Initialize standard classes. */
-		$this->config = SimpleSAML_Configuration::getInstance();
-		$this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
-		$this->session = SimpleSAML_Session::getSessionFromRequest();
-		$this->instance = $instance;
-		$this->metadataSets = $metadataSets;
-
-		$this->log('Accessing discovery service.');
-
-
-		/* Standard discovery service parameters. */
-
-		if(!array_key_exists('entityID', $_GET)) {
-			throw new Exception('Missing parameter: entityID');
-		} else {
-			$this->spEntityId = $_GET['entityID'];
-		}
-
-		if(!array_key_exists('returnIDParam', $_GET)) {
-			$this->returnIdParam = 'entityID';
-		} else {
-			$this->returnIdParam = $_GET['returnIDParam'];
-		}
-		
-		$this->log('returnIdParam initially set to [' . $this->returnIdParam . ']');
-
-		if(!array_key_exists('return', $_GET)) {
-			throw new Exception('Missing parameter: return');
-		} else {
-			$this->returnURL = \SimpleSAML\Utils\HTTP::checkURLAllowed($_GET['return']);
-		}
-		
-		$this->isPassive = FALSE;
-		if (array_key_exists('isPassive', $_GET)) {
-			if ($_GET['isPassive'] === 'true') $this->isPassive = TRUE;
-		}
-		$this->log('isPassive initially set to [' . ($this->isPassive ? 'TRUE' : 'FALSE' ) . ']');
-		
-		if (array_key_exists('IdPentityID', $_GET)) {
-			$this->setIdPentityID = $_GET['IdPentityID'];
-		} else {
-			$this->setIdPentityID = NULL;
-		}
-
-		if (array_key_exists('IDPList', $_REQUEST)) {
-			$this->scopedIDPList = $_REQUEST['IDPList'];
-		}
-
-	}
-
-
-	/**
-	 * Log a message.
-	 *
-	 * This is an helper function for logging messages. It will prefix the messages with our
-	 * discovery service type.
-	 *
-	 * @param string $message The message which should be logged.
-	 */
-	protected function log($message) {
-		SimpleSAML_Logger::info('idpDisco.' . $this->instance . ': ' . $message);
-	}
-
-
-	/**
-	 * Retrieve cookie with the given name.
-	 *
-	 * This function will retrieve a cookie with the given name for the current discovery
-	 * service type.
-	 *
-	 * @param string $name The name of the cookie.
-	 * @return string The value of the cookie with the given name, or NULL if no cookie with that name exists.
-	 */
-	protected function getCookie($name) {
-		$prefixedName = 'idpdisco_' . $this->instance . '_' . $name;
-		if(array_key_exists($prefixedName, $_COOKIE)) {
-			return $_COOKIE[$prefixedName];
-		} else {
-			return NULL;
-		}
-	}
-
-
-	/**
-	 * Save cookie with the given name and value.
-	 *
-	 * This function will save a cookie with the given name and value for the current discovery
-	 * service type.
-	 *
-	 * @param string $name The name of the cookie.
-	 * @param string $value The value of the cookie.
-	 */
-	protected function setCookie($name, $value) {
-		$prefixedName = 'idpdisco_' . $this->instance . '_' . $name;
-
-		$params = array(
-			/* We save the cookies for 90 days. */
-			'lifetime' => (60*60*24*90),
-			/* The base path for cookies. This should be the installation directory for simpleSAMLphp. */
-			'path' => ('/' . $this->config->getBaseUrl()),
-			'httponly' => FALSE,
-		);
-
-        \SimpleSAML\Utils\HTTP::setCookie($prefixedName, $value, $params, FALSE);
-	}
-
-
-	/**
-	 * Validates the given IdP entity id.
-	 *
-	 * Takes a string with the IdP entity id, and returns the entity id if it is valid, or
-	 * NULL if not.
-	 *
-	 * @param string|null $idp The entity id we want to validate. This can be NULL, in which case we will return NULL.
-	 * @return string|null The entity id if it is valid, NULL if not.
-	 */
-	protected function validateIdP($idp) {
-		if($idp === NULL) {
-			return NULL;
-		}
-
-		if(!$this->config->getBoolean('idpdisco.validate', TRUE)) {
-			return $idp;
-		}
-
-		foreach ($this->metadataSets AS $metadataSet) {
-			try {
-				$this->metadata->getMetaData($idp, $metadataSet);
-				return $idp;
-			} catch(Exception $e) { }
-		}
-
-		$this->log('Unable to validate IdP entity id [' . $idp . '].');
-		/* The entity id wasn't valid. */
-		return NULL;
-	}
-
-
-	/**
-	 * Retrieve the users choice of IdP.
-	 *
-	 * This function finds out which IdP the user has manually chosen, if any.
-	 *
-	 * @return string The entity id of the IdP the user has chosen, or NULL if the user has made no choice.
-	 */
-	protected function getSelectedIdP() {
-
-
-		/* Parameter set from the Extended IdP Metadata Discovery Service Protocol,
-		 * indicating that the user prefers this IdP.
-		 */
-		if ($this->setIdPentityID) {
-			return $this->validateIdP($this->setIdPentityID);
-		}
-
-		/* User has clicked on a link, or selected the IdP from a dropdown list. */
-		if(array_key_exists('idpentityid', $_GET)) {
-			return $this->validateIdP($_GET['idpentityid']);
-		}
-
-		/* Search for the IdP selection from the form used by the links view.
-		 * This form uses a name which equals idp_<entityid>, so we search for that.
-		 *
-		 * Unfortunately, php replaces periods in the name with underscores, and there
-		 * is no reliable way to get them back. Therefore we do some quick and dirty
-		 * parsing of the query string.
-		 */
-		$qstr = $_SERVER['QUERY_STRING'];
-		$matches = array();
-		if(preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) {
-			return $this->validateIdP(urldecode($matches[1]));
-		}
-
-		/* No IdP chosen. */
-		return NULL;
-	}
-
-
-	/**
-	 * Retrieve the users saved choice of IdP.
-	 *
-	 * @return string The entity id of the IdP the user has saved, or NULL if the user hasn't saved any choice.
-	 */
-	protected function getSavedIdP() {
-		if(!$this->config->getBoolean('idpdisco.enableremember', FALSE)) {
-			/* Saving of IdP choices is disabled. */
-			return NULL;
-		}
-
-		if($this->getCookie('remember') === '1') {
-			$this->log('Return previously saved IdP because of remember cookie set to 1');
-			return $this->getPreviousIdP();
-		}
-		
-		if( $this->isPassive) {
-			$this->log('Return previously saved IdP because of isPassive');
-			return $this->getPreviousIdP();
-		}
-		
-		return NULL;
-	}
-
-
-	/**
-	 * Retrieve the previous IdP the user used.
-	 *
-	 * @return string The entity id of the previous IdP the user used, or NULL if this is the first time.
-	 */
-	protected function getPreviousIdP() {
-		return $this->validateIdP($this->getCookie('lastidp'));
-	}
-
-
-	/**
-	 * Retrieve a recommended IdP based on the IP address of the client.
-	 *
-	 * @return string|NULL  The entity ID of the IdP if one is found, or NULL if not.
-	 */
-	protected function getFromCIDRhint() {
-
-		foreach ($this->metadataSets as $metadataSet) {
-			$idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']);
-			if (!empty($idp)) {
-				return $idp;
-			}
-		}
-
-		return NULL;
-	}
-
-
-	/**
-	 * Try to determine which IdP the user should most likely use.
-	 *
-	 * This function will first look at the previous IdP the user has chosen. If the user
-	 * hasn't chosen an IdP before, it will look at the IP address.
-	 *
-	 * @return string The entity id of the IdP the user should most likely use.
-	 */
-	protected function getRecommendedIdP() {
-
-		$idp = $this->getPreviousIdP();
-		if($idp !== NULL) {
-			$this->log('Preferred IdP from previous use [' . $idp . '].');
-			return $idp;
-		}
-
-		$idp = $this->getFromCIDRhint();
-
-		if(!empty($idp)) {
-			$this->log('Preferred IdP from CIDR hint [' . $idp . '].');
-			return $idp;
-		}
-
-		return NULL;
-	}
-
-
-	/**
-	 * Save the current IdP choice to a cookie.
-	 *
-	 * @param string $idp The entityID of the IdP.
-	 */
-	protected function setPreviousIdP($idp) {
-		assert('is_string($idp)');
-
-		$this->log('Choice made [' . $idp . '] Setting cookie.');
-		$this->setCookie('lastidp', $idp);
-	}
-
-
-	/**
-	 * Determine whether the choice of IdP should be saved.
-	 *
-	 * @return boolean True if the choice should be saved, false otherwise.
-	 */
-	protected function saveIdP() {
-		if(!$this->config->getBoolean('idpdisco.enableremember', FALSE)) {
-			/* Saving of IdP choices is disabled. */
-			return FALSE;
-		}
-
-		if(array_key_exists('remember', $_GET)) {
-			return TRUE;
-		}
-	}
-
-
-	/**
-	 * Determine which IdP the user should go to, if any.
-	 *
-	 * @return string The entity id of the IdP the user should be sent to, or NULL if the user should choose.
-	 */
-	protected function getTargetIdP() {
-
-		/* First, check if the user has chosen an IdP. */
-		$idp = $this->getSelectedIdP();
-		if($idp !== NULL) {
-			/* The user selected this IdP. Save the choice in a cookie. */
-			$this->setPreviousIdP($idp);
-
-			if($this->saveIdP()) {
-				$this->setCookie('remember', '1');
-			} else {
-				$this->setCookie('remember', '0');
-			}
-
-			return $idp;
-		}
-
-		$this->log('getSelectedIdP() returned NULL');
-
-		/* Check if the user has saved an choice earlier. */
-		$idp = $this->getSavedIdP();
-		if($idp !== NULL) {
-			$this->log('Using saved choice [' . $idp . '].');
-			return $idp;
-		}
-
-		/* The user has made no choice. */
-		return NULL;
-	}
-
-
-	/**
-	 * Retrieve the list of IdPs which are stored in the metadata.
-	 *
-	 * @return array An array with entityid => metadata mappings.
-	 */
-	protected function getIdPList() {
-
-		$idpList = array();
-		foreach ($this->metadataSets AS $metadataSet) {
-			$newList = $this->metadata->getList($metadataSet);
-			/*
-			 * Note that we merge the entities in reverse order. This ensuers
-			 * that it is the entity in the first metadata set that "wins" if
-			 * two metadata sets have the same entity.
-			 */
-			$idpList = array_merge($newList, $idpList);
-		}
-
-		return $idpList;
-	}
-
-	/**
-	 * Return the list of scoped idp
-	 *
-	 * @return array An array of IdP entities
-	 */
-	protected function getScopedIDPList() {
-		return $this->scopedIDPList;
-	}
-
-
-	/**
-	 * Filter the list of IdPs.
-	 *
-	 * This method returns the IdPs that comply with the following conditions:
-	 *   - The IdP does not have the 'hide.from.discovery' configuration option.
-	 *
-	 * @param array $list An associative array containing metadata for the IdPs to apply the filtering to.
-	 *
-	 * @return array An associative array containing metadata for the IdPs that were not filtered out.
-	 */
-	protected function filter($list)
-	{
-		foreach ($list as $entity => $metadata) {
-			if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) {
-				unset($list[$entity]);
-			}
-		}
-		return $list;
-	}
-
-
-	/**
-	 * Handles a request to this discovery service.
-	 *
-	 * The IdP disco parameters should be set before calling this function.
-	 */
-	public function handleRequest() {
-
-		$idp = $this->getTargetIdp();
-		if($idp !== NULL) {
-		
-			$extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', NULL);
-			if ($extDiscoveryStorage !== NULL) {
-				$this->log('Choice made [' . $idp . '] (Forwarding to external discovery storage)');
-				\SimpleSAML\Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, array(
-					'entityID' => $this->spEntityId,
-					'IdPentityID' => $idp,
-					'returnIDParam' => $this->returnIdParam,
-					'isPassive' => 'true',
-					'return' => $this->returnURL
-				));
-				
-			} else {
-				$this->log('Choice made [' . $idp . '] (Redirecting the user back. returnIDParam=' . $this->returnIdParam . ')');
-				\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp));
-			}
-			
-			return;
-		}
-		
-		if ($this->isPassive) {
-			$this->log('Choice not made. (Redirecting the user back without answer)');
-			\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL);
-			return;
-		}
-
-		/* No choice made. Show discovery service page. */
-
-		$idpList = $this->getIdPList();
-		$idpList = $this->filter($idpList);
-		$preferredIdP = $this->getRecommendedIdP();
-
-		$idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList());
-		if (sizeof($idpintersection) > 0) {
-			$idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, NULL));
-		}
-
-		$idpintersection = array_values($idpintersection);
-
-		if(sizeof($idpintersection)  == 1) {
-			$this->log('Choice made [' . $idpintersection[0] . '] (Redirecting the user back. returnIDParam=' . $this->returnIdParam . ')');
-			\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idpintersection[0]));
-		}
-
-		/*
-		 * Make use of an XHTML template to present the select IdP choice to the user.
-		 * Currently the supported options is either a drop down menu or a list view.
-		 */
-		switch($this->config->getString('idpdisco.layout', 'links')) {
-		case 'dropdown':
-			$templateFile = 'selectidp-dropdown.php';
-			break;
-		case 'links':
-			$templateFile = 'selectidp-links.php';
-			break;
-		default:
-			throw new Exception('Invalid value for the \'idpdisco.layout\' option.');
-		}
-
-		$t = new SimpleSAML_XHTML_Template($this->config, $templateFile, 'disco');
-		$t->data['idplist'] = $idpList;
-		$t->data['preferredidp'] = $preferredIdP;
-		$t->data['return'] = $this->returnURL;
-		$t->data['returnIDParam'] = $this->returnIdParam;
-		$t->data['entityID'] = $this->spEntityId;
-		$t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery());
-		$t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', FALSE);
-		$t->show();
-	}
+class SimpleSAML_XHTML_IdPDisco
+{
+
+    /**
+     * An instance of the configuration class.
+     *
+     * @var SimpleSAML_Configuration
+     */
+    protected $config;
+
+    /**
+     * The identifier of this discovery service.
+     *
+     * @var string
+     */
+    protected $instance;
+
+
+    /**
+     * An instance of the metadata handler, which will allow us to fetch metadata about IdPs.
+     *
+     * @var SimpleSAML_Metadata_MetaDataStorageHandler
+     */
+    protected $metadata;
+
+
+    /**
+     * The users session.
+     *
+     * @var SimpleSAML_Session
+     */
+    protected $session;
+
+
+    /**
+     * The metadata sets we find allowed entities in, in prioritized order.
+     *
+     * @var array
+     */
+    protected $metadataSets;
+
+
+    /**
+     * The entity id of the SP which accesses this IdP discovery service.
+     *
+     * @var string
+     */
+    protected $spEntityId;
+
+    /**
+     * HTTP parameter from the request, indicating whether the discovery service
+     * can interact with the user or not.
+     *
+     * @var boolean
+     */
+    protected $isPassive;
+
+    /**
+     * The SP request to set the IdPentityID...
+     *
+     * @var string|null
+     */
+    protected $setIdPentityID = null;
+
+
+    /**
+     * The name of the query parameter which should contain the users choice of IdP.
+     * This option default to 'entityID' for Shibboleth compatibility.
+     *
+     * @var string
+     */
+    protected $returnIdParam;
+
+    /**
+     * The list of scoped idp's. The intersection between the metadata idpList
+     * and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to
+     * the user. If the intersection is empty the metadata idpList is used.
+     *
+     * @var array
+     */
+    protected $scopedIDPList = array();
+
+    /**
+     * The URL the user should be redirected to after choosing an IdP.
+     *
+     * @var string
+     */
+    protected $returnURL;
+
+
+    /**
+     * Initializes this discovery service.
+     *
+     * The constructor does the parsing of the request. If this is an invalid request, it will throw an exception.
+     *
+     * @param array  $metadataSets Array with metadata sets we find remote entities in.
+     * @param string $instance The name of this instance of the discovery service.
+     *
+     * @throws Exception If the request is invalid.
+     */
+    public function __construct(array $metadataSets, $instance)
+    {
+        assert('is_string($instance)');
+
+        // initialize standard classes
+        $this->config = SimpleSAML_Configuration::getInstance();
+        $this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
+        $this->session = SimpleSAML_Session::getSessionFromRequest();
+        $this->instance = $instance;
+        $this->metadataSets = $metadataSets;
+
+        $this->log('Accessing discovery service.');
+
+        // standard discovery service parameters
+        if (!array_key_exists('entityID', $_GET)) {
+            throw new Exception('Missing parameter: entityID');
+        } else {
+            $this->spEntityId = $_GET['entityID'];
+        }
+
+        if (!array_key_exists('returnIDParam', $_GET)) {
+            $this->returnIdParam = 'entityID';
+        } else {
+            $this->returnIdParam = $_GET['returnIDParam'];
+        }
+
+        $this->log('returnIdParam initially set to ['.$this->returnIdParam.']');
+
+        if (!array_key_exists('return', $_GET)) {
+            throw new Exception('Missing parameter: return');
+        } else {
+            $this->returnURL = \SimpleSAML\Utils\HTTP::checkURLAllowed($_GET['return']);
+        }
+
+        $this->isPassive = false;
+        if (array_key_exists('isPassive', $_GET)) {
+            if ($_GET['isPassive'] === 'true') {
+                $this->isPassive = true;
+            }
+        }
+        $this->log('isPassive initially set to ['.($this->isPassive ? 'TRUE' : 'FALSE').']');
+
+        if (array_key_exists('IdPentityID', $_GET)) {
+            $this->setIdPentityID = $_GET['IdPentityID'];
+        } else {
+            $this->setIdPentityID = null;
+        }
+
+        if (array_key_exists('IDPList', $_REQUEST)) {
+            $this->scopedIDPList = $_REQUEST['IDPList'];
+        }
+    }
+
+
+    /**
+     * Log a message.
+     *
+     * This is an helper function for logging messages. It will prefix the messages with our
+     * discovery service type.
+     *
+     * @param string $message The message which should be logged.
+     */
+    protected function log($message)
+    {
+        SimpleSAML_Logger::info('idpDisco.'.$this->instance.': '.$message);
+    }
+
+
+    /**
+     * Retrieve cookie with the given name.
+     *
+     * This function will retrieve a cookie with the given name for the current discovery
+     * service type.
+     *
+     * @param string $name The name of the cookie.
+     *
+     * @return string The value of the cookie with the given name, or null if no cookie with that name exists.
+     */
+    protected function getCookie($name)
+    {
+        $prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
+        if (array_key_exists($prefixedName, $_COOKIE)) {
+            return $_COOKIE[$prefixedName];
+        } else {
+            return null;
+        }
+    }
+
+
+    /**
+     * Save cookie with the given name and value.
+     *
+     * This function will save a cookie with the given name and value for the current discovery
+     * service type.
+     *
+     * @param string $name The name of the cookie.
+     * @param string $value The value of the cookie.
+     */
+    protected function setCookie($name, $value)
+    {
+        $prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
+
+        $params = array(
+            // we save the cookies for 90 days
+            'lifetime' => (60 * 60 * 24 * 90),
+            // the base path for cookies. This should be the installation directory for SimpleSAMLphp
+            'path'     => ('/'.$this->config->getBaseUrl()),
+            'httponly' => false,
+        );
+
+        \SimpleSAML\Utils\HTTP::setCookie($prefixedName, $value, $params, false);
+    }
+
+
+    /**
+     * Validates the given IdP entity id.
+     *
+     * Takes a string with the IdP entity id, and returns the entity id if it is valid, or
+     * null if not.
+     *
+     * @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null.
+     *
+     * @return string|null The entity id if it is valid, null if not.
+     */
+    protected function validateIdP($idp)
+    {
+        if ($idp === null) {
+            return null;
+        }
+
+        if (!$this->config->getBoolean('idpdisco.validate', true)) {
+            return $idp;
+        }
+
+        foreach ($this->metadataSets as $metadataSet) {
+            try {
+                $this->metadata->getMetaData($idp, $metadataSet);
+                return $idp;
+            } catch (Exception $e) {
+            }
+        }
+
+        $this->log('Unable to validate IdP entity id ['.$idp.'].');
+
+        // the entity id wasn't valid
+        return null;
+    }
+
+
+    /**
+     * Retrieve the users choice of IdP.
+     *
+     * This function finds out which IdP the user has manually chosen, if any.
+     *
+     * @return string The entity id of the IdP the user has chosen, or null if the user has made no choice.
+     */
+    protected function getSelectedIdP()
+    {
+        /* Parameter set from the Extended IdP Metadata Discovery Service Protocol, indicating that the user prefers
+         * this IdP.
+         */
+        if ($this->setIdPentityID) {
+            return $this->validateIdP($this->setIdPentityID);
+        }
+
+        // user has clicked on a link, or selected the IdP from a drop-down list
+        if (array_key_exists('idpentityid', $_GET)) {
+            return $this->validateIdP($_GET['idpentityid']);
+        }
+
+        /* Search for the IdP selection from the form used by the links view. This form uses a name which equals
+         * idp_<entityid>, so we search for that.
+         *
+         * Unfortunately, php replaces periods in the name with underscores, and there is no reliable way to get them
+         * back. Therefore we do some quick and dirty parsing of the query string.
+         */
+        $qstr = $_SERVER['QUERY_STRING'];
+        $matches = array();
+        if (preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) {
+            return $this->validateIdP(urldecode($matches[1]));
+        }
+
+        // no IdP chosen
+        return null;
+    }
+
+
+    /**
+     * Retrieve the users saved choice of IdP.
+     *
+     * @return string The entity id of the IdP the user has saved, or null if the user hasn't saved any choice.
+     */
+    protected function getSavedIdP()
+    {
+        if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
+            // saving of IdP choices is disabled
+            return null;
+        }
+
+        if ($this->getCookie('remember') === '1') {
+            $this->log('Return previously saved IdP because of remember cookie set to 1');
+            return $this->getPreviousIdP();
+        }
+
+        if ($this->isPassive) {
+            $this->log('Return previously saved IdP because of isPassive');
+            return $this->getPreviousIdP();
+        }
+
+        return null;
+    }
+
+
+    /**
+     * Retrieve the previous IdP the user used.
+     *
+     * @return string The entity id of the previous IdP the user used, or null if this is the first time.
+     */
+    protected function getPreviousIdP()
+    {
+        return $this->validateIdP($this->getCookie('lastidp'));
+    }
+
+
+    /**
+     * Retrieve a recommended IdP based on the IP address of the client.
+     *
+     * @return string|null  The entity ID of the IdP if one is found, or null if not.
+     */
+    protected function getFromCIDRhint()
+    {
+        foreach ($this->metadataSets as $metadataSet) {
+            $idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']);
+            if (!empty($idp)) {
+                return $idp;
+            }
+        }
+
+        return null;
+    }
+
+
+    /**
+     * Try to determine which IdP the user should most likely use.
+     *
+     * This function will first look at the previous IdP the user has chosen. If the user
+     * hasn't chosen an IdP before, it will look at the IP address.
+     *
+     * @return string The entity id of the IdP the user should most likely use.
+     */
+    protected function getRecommendedIdP()
+    {
+        $idp = $this->getPreviousIdP();
+        if ($idp !== null) {
+            $this->log('Preferred IdP from previous use ['.$idp.'].');
+            return $idp;
+        }
+
+        $idp = $this->getFromCIDRhint();
+
+        if (!empty($idp)) {
+            $this->log('Preferred IdP from CIDR hint ['.$idp.'].');
+            return $idp;
+        }
+
+        return null;
+    }
+
+
+    /**
+     * Save the current IdP choice to a cookie.
+     *
+     * @param string $idp The entityID of the IdP.
+     */
+    protected function setPreviousIdP($idp)
+    {
+        assert('is_string($idp)');
+
+        $this->log('Choice made ['.$idp.'] Setting cookie.');
+        $this->setCookie('lastidp', $idp);
+    }
+
+
+    /**
+     * Determine whether the choice of IdP should be saved.
+     *
+     * @return boolean True if the choice should be saved, false otherwise.
+     */
+    protected function saveIdP()
+    {
+        if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
+            // saving of IdP choices is disabled
+            return false;
+        }
+
+        if (array_key_exists('remember', $_GET)) {
+            return true;
+        }
+
+        return false;
+    }
+
+
+    /**
+     * Determine which IdP the user should go to, if any.
+     *
+     * @return string The entity id of the IdP the user should be sent to, or null if the user should choose.
+     */
+    protected function getTargetIdP()
+    {
+        // first, check if the user has chosen an IdP
+        $idp = $this->getSelectedIdP();
+        if ($idp !== null) {
+            // the user selected this IdP. Save the choice in a cookie
+            $this->setPreviousIdP($idp);
+
+            if ($this->saveIdP()) {
+                $this->setCookie('remember', '1');
+            } else {
+                $this->setCookie('remember', '0');
+            }
+
+            return $idp;
+        }
+
+        $this->log('getSelectedIdP() returned null');
+
+        // check if the user has saved an choice earlier
+        $idp = $this->getSavedIdP();
+        if ($idp !== null) {
+            $this->log('Using saved choice ['.$idp.'].');
+            return $idp;
+        }
+
+        // the user has made no choice
+        return null;
+    }
+
+
+    /**
+     * Retrieve the list of IdPs which are stored in the metadata.
+     *
+     * @return array An array with entityid => metadata mappings.
+     */
+    protected function getIdPList()
+    {
+        $idpList = array();
+        foreach ($this->metadataSets as $metadataSet) {
+            $newList = $this->metadata->getList($metadataSet);
+            /*
+             * Note that we merge the entities in reverse order. This ensures that it is the entity in the first
+             * metadata set that "wins" if two metadata sets have the same entity.
+             */
+            $idpList = array_merge($newList, $idpList);
+        }
+
+        return $idpList;
+    }
+
+
+    /**
+     * Return the list of scoped idp
+     *
+     * @return array An array of IdP entities
+     */
+    protected function getScopedIDPList()
+    {
+        return $this->scopedIDPList;
+    }
+
+
+    /**
+     * Filter the list of IdPs.
+     *
+     * This method returns the IdPs that comply with the following conditions:
+     *   - The IdP does not have the 'hide.from.discovery' configuration option.
+     *
+     * @param array $list An associative array containing metadata for the IdPs to apply the filtering to.
+     *
+     * @return array An associative array containing metadata for the IdPs that were not filtered out.
+     */
+    protected function filter($list)
+    {
+        foreach ($list as $entity => $metadata) {
+            if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) {
+                unset($list[$entity]);
+            }
+        }
+        return $list;
+    }
+
+
+    /**
+     * Handles a request to this discovery service.
+     *
+     * The IdP disco parameters should be set before calling this function.
+     */
+    public function handleRequest()
+    {
+        $idp = $this->getTargetIdp();
+        if ($idp !== null) {
+
+            $extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', null);
+            if ($extDiscoveryStorage !== null) {
+                $this->log('Choice made ['.$idp.'] (Forwarding to external discovery storage)');
+                \SimpleSAML\Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, array(
+                    'entityID'      => $this->spEntityId,
+                    'IdPentityID'   => $idp,
+                    'returnIDParam' => $this->returnIdParam,
+                    'isPassive'     => 'true',
+                    'return'        => $this->returnURL
+                ));
+            } else {
+                $this->log(
+                    'Choice made ['.$idp.'] (Redirecting the user back. returnIDParam='.$this->returnIdParam.')'
+                );
+                \SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp));
+            }
+
+            return;
+        }
+
+        if ($this->isPassive) {
+            $this->log('Choice not made. (Redirecting the user back without answer)');
+            \SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL);
+            return;
+        }
+
+        // no choice made. Show discovery service page
+        $idpList = $this->getIdPList();
+        $idpList = $this->filter($idpList);
+        $preferredIdP = $this->getRecommendedIdP();
+
+        $idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList());
+        if (sizeof($idpintersection) > 0) {
+            $idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, null));
+        }
+
+        $idpintersection = array_values($idpintersection);
+
+        if (sizeof($idpintersection) == 1) {
+            $this->log(
+                'Choice made ['.$idpintersection[0].'] (Redirecting the user back. returnIDParam='.
+                $this->returnIdParam.')'
+            );
+            \SimpleSAML\Utils\HTTP::redirectTrustedURL(
+                $this->returnURL,
+                array($this->returnIdParam => $idpintersection[0])
+            );
+        }
+
+        /*
+         * Make use of an XHTML template to present the select IdP choice to the user. Currently the supported options
+         * is either a drop down menu or a list view.
+         */
+        switch ($this->config->getString('idpdisco.layout', 'links')) {
+            case 'dropdown':
+                $templateFile = 'selectidp-dropdown.php';
+                break;
+            case 'links':
+                $templateFile = 'selectidp-links.php';
+                break;
+            default:
+                throw new Exception('Invalid value for the \'idpdisco.layout\' option.');
+        }
+
+        $t = new SimpleSAML_XHTML_Template($this->config, $templateFile, 'disco');
+        $t->data['idplist'] = $idpList;
+        $t->data['preferredidp'] = $preferredIdP;
+        $t->data['return'] = $this->returnURL;
+        $t->data['returnIDParam'] = $this->returnIdParam;
+        $t->data['entityID'] = $this->spEntityId;
+        $t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery());
+        $t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', false);
+        $t->show();
+    }
 }