, UNINETT AS. * @package simpleSAMLphp */ class sspmod_discopower_PowerIdPDisco extends SimpleSAML_XHTML_IdPDisco { private $discoconfig; /** * The domain to use when saving common domain cookies. * This is NULL if support for common domain cookies is disabled. * * @var string|NULL */ private $cdcDomain; /** * The lifetime of the CDC cookie, in seconds. * If set to NULL, it will only be valid until the browser is closed. * * @var int|NULL */ private $cdcLifetime; /** * 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. */ public function __construct(array $metadataSets, $instance) { parent::__construct($metadataSets, $instance); $this->discoconfig = SimpleSAML_Configuration::getConfig('module_discopower.php'); $this->cdcDomain = $this->discoconfig->getString('cdc.domain', NULL); if ($this->cdcDomain !== NULL && $this->cdcDomain[0] !== '.') { /* Ensure that the CDC domain starts with a dot ('.') as required by the spec. */ $this->cdcDomain = '.' . $this->cdcDomain; } $this->cdcLifetime = $this->discoconfig->getInteger('cdc.lifetime', NULL); } /** * Log a message. * * This is an helper function for logging messages. It will prefix the messages with our * discovery service type. * * @param $message The message which should be logged. */ protected function log($message) { SimpleSAML_Logger::info('PowerIdPDisco.' . $this->instance . ': ' . $message); } /** * Compare two entities. * * This function is used to sort the entity list. It sorts based on english name, * and will always put IdP's with names configured before those with only an * entityID. * * @param array $a The metadata of the first entity. * @param array $b The metadata of the second entity. * @return int How $a compares to $b. */ public static function mcmp(array $a, array $b) { if (isset($a['name']['en']) && isset($b['name']['en'])) { return strcasecmp($a['name']['en'], $b['name']['en']); } elseif (isset($a['name']['en'])) { return -1; /* Place name before entity ID. */ } elseif (isset($b['name']['en'])) { return 1; /* Place entity ID after name. */ } else { return strcasecmp($a['entityid'], $b['entityid']); } } /* * This function will structure the idp list in a hierarchy based upon the tags. */ protected function idplistStructured($list) { # echo '
'; print_r($list); exit;
		$slist = array();
		
		$order = $this->discoconfig->getValue('taborder');
		if (is_array($order)) {
			foreach($order AS $oe) {
				$slist[$oe] = array();
			}
		}
		
		$enableTabs = $this->discoconfig->getValue('tabs', NULL);
		
		foreach($list AS $key => $val) {
			$tags = array('misc');
			if (array_key_exists('tags', $val)) {
				$tags = $val['tags'];
			}
			foreach ($tags AS $tag) {
				if (!empty($enableTabs) && !in_array($tag, $enableTabs)) continue;
				$slist[$tag][$key] = $val;
			}
		}
		
		foreach($slist AS $tab => $tbslist) {
			uasort($slist[$tab], array('sspmod_discopower_PowerIdPDisco', 'mcmp'));
		}
		
		return $slist;
	}
	
	private function processFilter($filter, $entry, $default = TRUE) {
		if (in_array($entry['entityid'], $filter['entities.include'] )) return TRUE;
		if (in_array($entry['entityid'], $filter['entities.exclude'] )) return FALSE;
		
		if (array_key_exists('tags', $entry)) {
			foreach ($filter['tags.include'] AS $fe) {
				if (in_array($fe, $entry['tags'])) return TRUE;
			}
			foreach ($filter['tags.exclude'] AS $fe) {
				if (in_array($fe, $entry['tags'])) return FALSE;
			}
		}
		return $default;
	}
	
	protected function filterList($list) {
		
		try {
			$spmd = $this->metadata->getMetaData($this->spEntityId, 'saml20-sp-remote');
		} catch(Exception $e) {
			return $list;
		}
		
		if (!isset($spmd)) return $list;
		if (!array_key_exists('discopower.filter', $spmd)) return $list;
		$filter = $spmd['discopower.filter'];
		
		if (!array_key_exists('entities.include', $filter)) $filter['entities.include'] = array();
		if (!array_key_exists('entities.exclude', $filter)) $filter['entities.exclude'] = array();
		if (!array_key_exists('tags.include', $filter)) $filter['tags.include'] = array();
		if (!array_key_exists('tags.exclude', $filter)) $filter['tags.exclude'] = array();
		
		$defaultrule = TRUE;
		if ( array_key_exists('entities.include', $spmd['discopower.filter'] ) ||
			array_key_exists('tags.include', $spmd['discopower.filter'])) {
				
				$defaultrule = FALSE;
		}
		
		$returnlist = array();
		foreach ($list AS $key => $entry) {
			if ($this->processFilter($filter, $entry, $defaultrule)) {
				$returnlist[$key] = $entry;
			}
		}
		return $returnlist;
		
	}
	

	/**
	 * 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_Utilities::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_Utilities::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp));
			}
			
			return;
		}
		
		if ($this->isPassive) {
			$this->log('Choice not made. (Redirecting the user back without answer)');
			SimpleSAML_Utilities::redirectTrustedURL($this->returnURL);
			return;
		}

		/* No choice made. Show discovery service page. */
		$idpList = $this->getIdPList();
		$idpList = $this->idplistStructured($this->filterList($idpList));
		$preferredIdP = $this->getRecommendedIdP();

		$t = new SimpleSAML_XHTML_Template($this->config, 'discopower:disco-tpl.php', '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_Utilities::selfURLNoQuery());
		$t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', FALSE);
		$t->data['rememberchecked'] = $this->config->getBoolean('idpdisco.rememberchecked', FALSE);
		$t->data['defaulttab'] = $this->discoconfig->getValue('defaulttab', 0);
		$t->data['score'] = $this->discoconfig->getValue('score', 'quicksilver');
		$t->show();
	}


	/**
	 * Get the IdP entities saved in the common domain cookie.
	 *
	 * @return array  List of IdP entities.
	 */
	private function getCDC() {

		if (!isset($_COOKIE['_saml_idp'])) {
			return array();
		}

		$ret = (string)$_COOKIE['_saml_idp'];
		$ret = explode(' ', $ret);
		foreach ($ret as &$idp) {
			$idp = base64_decode($idp);
			if ($idp === FALSE) {
				/* Not properly base64 encoded. */
				return array();
			}
		}

		return $ret;
	}


	/**
	 * Save the current IdP choice to a cookie.
	 *
	 * This function overrides the corresponding function in the parent class,
	 * to add support for common domain cookie.
	 *
	 * @param string $idp  The entityID of the IdP.
	 */
	protected function setPreviousIdP($idp) {
		assert('is_string($idp)');

		if ($this->cdcDomain === NULL) {
			parent::setPreviousIdP($idp);
			return;
		}

		$list = $this->getCDC();

		$prevIndex = array_search($idp, $list, TRUE);
		if ($prevIndex !== FALSE) {
			unset($list[$prevIndex]);
		}
		$list[] = $idp;

		foreach ($list as &$value) {
			$value = base64_encode($value);
		}
		$newCookie = implode(' ', $list);

		while (strlen($newCookie) > 4000) {
			/* The cookie is too long. Remove the oldest elements until it is short enough. */
			$tmp = explode(' ', $newCookie, 2);
			if (count($tmp) === 1) {
				/*
				 * We are left with a single entityID whose base64
				 * representation is too long to fit in a cookie.
				 */
				break;
			}
			$newCookie = $tmp[1];
		}

		$params = array(
			'lifetime' => $this->cdcLifetime,
			'domain' => $this->cdcDomain,
			'secure' => TRUE,
			'httponly' => FALSE,
		);
		SimpleSAML_Utilities::setCookie('_saml_idp', $newCookie, $params, FALSE);
	}


	/**
	 * Retrieve the previous IdP the user used.
	 *
	 * This function overrides the corresponding function in the parent class,
	 * to add support for common domain cookie.
	 *
	 * @return string|NULL  The entity id of the previous IdP the user used, or NULL if this is the first time.
	 */
	protected function getPreviousIdP() {

		if ($this->cdcDomain === NULL) {
			return parent::getPreviousIdP();
		}

		$prevIdPs = $this->getCDC();
		while (count($prevIdPs) > 0) {
			$idp = array_pop($prevIdPs);
			$idp = $this->validateIdP($idp);
			if ($idp !== NULL) {
				return $idp;
			}
		}

		return NULL;
	}

}