<?php

/**
 * Class for implementing authentication processing chains for IdPs.
 *
 * This class implements a system for additional steps which should be taken by an IdP before
 * submitting a response to a SP. Examples of additional steps can be additional authentication
 * checks, or attribute consent requirements.
 *
 * @author Olav Morken, UNINETT AS.
 * @package simpleSAMLphp
 * @version $Id$
 */
class SimpleSAML_Auth_ProcessingChain {


	/**
	 * The list of remaining filters which should be applied to the state.
	 */
	const FILTERS_INDEX = 'SimpleSAML_Auth_ProcessingChain.filters';


	/**
	 * The stage we use for completed requests.
	 */
	const COMPLETED_STAGE = 'SimpleSAML_Auth_ProcessingChain.completed';


	/**
	 * The request parameter we will use to pass the state identifier when we redirect after
	 * having completed processing of the state.
	 */
	const AUTHPARAM = 'AuthProcId';


	/**
	 * All authentication processing filters, in the order they should be applied.
	 */
	private $filters;


	/**
	 * Initialize an authentication processing chain for the given service provider
	 * and identity provider.
	 *
	 * @param array $idpMetadata  The metadata for the IdP.
	 * @param array $spMetadata  The metadata for the SP.
	 */
	public function __construct($idpMetadata, $spMetadata, $mode = 'idp') {
		assert('is_array($idpMetadata)');
		assert('is_array($spMetadata)');

		$this->filters = array();
		
		$config = SimpleSAML_Configuration::getInstance();
		$configauthproc = $config->getValue('authproc.' . $mode);
		
		if (!empty($configauthproc) && is_array($configauthproc)) {
			$configfilters = self::parseFilterList($configauthproc);
			self::addFilters($this->filters, $configfilters);
		}

		if (array_key_exists('authproc', $idpMetadata)) {
			$idpFilters = self::parseFilterList($idpMetadata['authproc']);
			self::addFilters($this->filters, $idpFilters);
		}

		if (array_key_exists('authproc', $spMetadata)) {
			$spFilters = self::parseFilterList($spMetadata['authproc']);
			self::addFilters($this->filters, $spFilters);
		}


		SimpleSAML_Logger::debug('Filter config for ' . $idpMetadata['entityid'] . '->' .
			$spMetadata['entityid'] . ': ' . str_replace("\n", '', var_export($this->filters, TRUE)));

	}


	/**
	 * Sort & merge filter configuration
	 *
	 * Inserts unsorted filters into sorted filter list. This sort operation is stable.
	 *
	 * @param array &$target  Target filter list. This list must be sorted.
	 * @param array $src  Source filters. May be unsorted.
	 */
	private static function addFilters(&$target, $src) {
		assert('is_array($target)');
		assert('is_array($src)');

		foreach ($src as $filter) {
			$fp = $filter->priority;

			/* Find insertion position for filter. */
			for($i = count($target)-1; $i >= 0; $i--) {
				if ($target[$i]->priority <= $fp) {
					/* The new filter should be inserted after this one. */
					break;
				}
			}
			/* $i now points to the filter which should preceede the current filter. */
			array_splice($target, $i+1, 0, array($filter));
		}

	}


	/**
	 * Parse an array of authentication processing filters.
	 *
	 * @param array $filterSrc  Array with filter configuration.
	 * @return array  Array of SimpleSAML_Auth_ProcessingFilter objects.
	 */
	private static function parseFilterList($filterSrc) {
		assert('is_array($filterSrc)');

		$parsedFilters = array();

		foreach ($filterSrc as $priority => $filter) {

			if (is_string($filter)) {
				$filter = array('class' => $filter);
			}

			if (!is_array($filter)) {
				throw new Exception('Invalid authentication processing filter configuration: ' .
					'One of the filters wasn\'t a string or an array.');
			}

			$parsedFilters[] = self::parseFilter($filter, $priority);
		}

		return $parsedFilters;
	}


	/**
	 * Parse an authentication processing filter.
	 *
	 * @param array $config  	Array with the authentication processing filter configuration.
	 * @param int $priority		The priority of the current filter, (not included in the filter 
	 *							definition.)
	 * @return SimpleSAML_Auth_ProcessingFilter  The parsed filter.
	 */
	private static function parseFilter($config, $priority) {
		assert('is_array($config)');

		if (!array_key_exists('class', $config)) 
			throw new Exception('Authentication processing filter without name given.');

		$className = SimpleSAML_Module::resolveClass($config['class'], 'Auth_Process', 'SimpleSAML_Auth_ProcessingFilter');
		$config['%priority'] = $priority;
		unset($config['class']);
		return new $className($config, NULL);
	}


	/**
	 * Process the given state.
	 *
	 * This function will only return if processing completes. If processing requires showing
	 * a page to the user, we will redirect to the URL set in $state['ReturnURL'] after processing is
	 * completed.
	 *
	 * @param array &$state  The state we are processing.
	 */
	public function processState(&$state) {
		assert('is_array($state)');
		assert('array_key_exists("ReturnURL", $state)');

		$state[self::FILTERS_INDEX] = $this->filters;

		if (!array_key_exists('UserID', $state)) {
			/* No unique user ID present. Attempt to add one. */
			self::addUserID($state);
		}

		while (count($state[self::FILTERS_INDEX]) > 0) {
			$filter = array_shift($state[self::FILTERS_INDEX]);
			$filter->process($state);
		}

		/* Completed. */
	}


	/**
	 * Continues processing of the state.
	 *
	 * This function is used to resume processing by filters which for example needed to show
	 * a page to the user.
	 *
	 * This function will never return. In the case of an exception, exception handling should
	 * be left to the main simpleSAMLphp exception handler.
	 *
	 * @param array $state  The state we are processing.
	 */
	public static function resumeProcessing($state) {
		assert('is_array($state)');

		while (count($state[self::FILTERS_INDEX]) > 0) {
			$filter = array_shift($state[self::FILTERS_INDEX]);
			$filter->process($state);
		}

		assert('array_key_exists("ReturnURL", $state)');

		/* Completed. Save state information, and redirect to the URL specified
		 * in $state['ReturnURL'].
		 */
		$id = SimpleSAML_Auth_State::saveState($state, self::COMPLETED_STAGE);
		SimpleSAML_Utilities::redirect($state['ReturnURL'], array(self::AUTHPARAM => $id));
	}

	/**
	 * Process the given state passivly.
	 *
	 * Modules with user interaction are expected to throw an SimpleSAML_Error_NoPassive exception
	 * which are silently ignored. Exceptions of other types are passed further up the call stack.
	 *
	 * This function will only return if processing completes.
	 *
	 * @param array &$state  The state we are processing.
	 */
	public function processStatePassive(&$state) {
		assert('is_array($state)');
		// Should not be set when calling this method
		assert('!array_key_exists("ReturnURL", $state)');

		// Notify filters about passive request
		$state['isPassive'] = TRUE;

		$state[self::FILTERS_INDEX] = $this->filters;

		if (!array_key_exists('UserID', $state)) {
			/* No unique user ID present. Attempt to add one. */
			self::addUserID($state);
		}

		while (count($state[self::FILTERS_INDEX]) > 0) {
			$filter = array_shift($state[self::FILTERS_INDEX]);
			try {
				$filter->process($state);

			// Ignore SimpleSAML_Error_NoPassive exceptions
			} catch (SimpleSAML_Error_NoPassive $e) { }
		}
	}

	/**
	 * Retrieve a state which has finished processing.
	 *
	 * @param string $id  The identifier of the state. This can be found in the request parameter
	 *                    with index from SimpleSAML_Auth_ProcessingChain::AUTHPARAM.
	 */
	public static function fetchProcessedState($id) {
		assert('is_string($id)');

		return SimpleSAML_Auth_State::loadState($id, self::COMPLETED_STAGE);
	}


	/**
	 * Add unique user ID.
	 *
	 * This function attempts to add an unique user ID to the state.
	 *
	 * @param array &$state  The state we should update.
	 */
	private static function addUserID(&$state) {
		assert('is_array($state)');
		assert('array_key_exists("Attributes", $state)');

		if (isset($state['Destination']['userid.attribute'])) {
			$attributeName = $state['Destination']['userid.attribute'];
		} elseif (isset($state['Source']['userid.attribute'])) {
			$attributeName = $state['Source']['userid.attribute'];
		} else {
			/* Default attribute. */
			$attributeName = 'eduPersonPrincipalName';
		}

		if (!array_key_exists($attributeName, $state['Attributes'])) {
			return;
		}

		$uid = $state['Attributes'][$attributeName];
		if (count($uid) === 0) {
			SimpleSAML_Logger::warning('Empty user id attribute [' . $attributeName . '].');
			return;
		}

		if (count($uid) > 1) {
			SimpleSAML_Logger::warning('Multiple attribute values for user id attribute [' . $attributeName . '].');
		}

		$uid = $uid[0];
		$state['UserID'] = $uid;
	}

}

?>