<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown;

/**
 * Markdown parser for github flavored markdown.
 *
 * @author Carsten Brandt <mail@cebe.cc>
 */
class GithubMarkdown extends Markdown
{
	// include block element parsing using traits
	use block\TableTrait;
	use block\FencedCodeTrait;

	// include inline element parsing using traits
	use inline\StrikeoutTrait;
	use inline\UrlLinkTrait;

	/**
	 * @var boolean whether to interpret newlines as `<br />`-tags.
	 * This feature is useful for comments where newlines are often meant to be real new lines.
	 */
	public $enableNewlines = false;

	/**
	 * @inheritDoc
	 */
	protected $escapeCharacters = [
		// from Markdown
		'\\', // backslash
		'`', // backtick
		'*', // asterisk
		'_', // underscore
		'{', '}', // curly braces
		'[', ']', // square brackets
		'(', ')', // parentheses
		'#', // hash mark
		'+', // plus sign
		'-', // minus sign (hyphen)
		'.', // dot
		'!', // exclamation mark
		'<', '>',
		// added by GithubMarkdown
		':', // colon
		'|', // pipe
	];

	/**
	 * Consume lines for a paragraph
	 *
	 * Allow headlines, lists and code to break paragraphs
	 */
	protected function consumeParagraph($lines, $current)
	{
		// consume until newline
		$content = [];
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
			$line = $lines[$i];
			if ($line === ''
				|| ltrim($line) === ''
				|| !ctype_alpha($line[0]) && (
					$this->identifyQuote($line, $lines, $i) ||
					$this->identifyFencedCode($line, $lines, $i) ||
					$this->identifyUl($line, $lines, $i) ||
					$this->identifyOl($line, $lines, $i) ||
					$this->identifyHr($line, $lines, $i)
				)
				|| $this->identifyHeadline($line, $lines, $i))
			{
				break;
			} elseif ($this->identifyCode($line, $lines, $i)) {
				// possible beginning of a code block
				// but check for continued inline HTML
				// e.g.
				if (preg_match('~<\w+([^>]+)$~s', implode("\n", $content))) {
					$content[] = $line;
				} else {
					break;
				}
			} else {
				$content[] = $line;
			}
		}
		$block = [
			'paragraph',
			'content' => $this->parseInline(implode("\n", $content)),
		];
		return [$block, --$i];
	}

	/**
	 * @inheritdocs
	 *
	 * Parses a newline indicated by two spaces on the end of a markdown line.
	 */
	protected function renderText($text)
	{
		if ($this->enableNewlines) {
			$br = $this->html5 ? "
\n" : "
\n";
			return strtr($text[1], ["  \n" => $br, "\n" => $br]);
		} else {
			return parent::renderText($text);
		}
	}
}


<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown;

/**
 * Markdown parser for the [initial markdown spec](http://daringfireball.net/projects/markdown/syntax).
 *
 * @author Carsten Brandt <mail@cebe.cc>
 */
class Markdown extends Parser
{
	// include block element parsing using traits
	use block\CodeTrait;
	use block\HeadlineTrait;
	use block\HtmlTrait {
		parseInlineHtml as private;
	}
	use block\ListTrait {
		// Check Ul List before headline
		identifyUl as protected identifyBUl;
		consumeUl as protected consumeBUl;
	}
	use block\QuoteTrait;
	use block\RuleTrait {
		// Check Hr before checking lists
		identifyHr as protected identifyAHr;
		consumeHr as protected consumeAHr;
	}

	// include inline element parsing using traits
	use inline\CodeTrait;
	use inline\EmphStrongTrait;
	use inline\LinkTrait;

	/**
	 * @var boolean whether to format markup according to HTML5 spec.
	 * Defaults to `false` which means that markup is formatted as HTML4.
	 */
	public $html5 = false;

	/**
	 * @var array these are "escapeable" characters. When using one of these prefixed with a
	 * backslash, the character will be outputted without the backslash and is not interpreted
	 * as markdown.
	 */
	protected $escapeCharacters = [
		'\\', // backslash
		'`', // backtick
		'*', // asterisk
		'_', // underscore
		'{', '}', // curly braces
		'[', ']', // square brackets
		'(', ')', // parentheses
		'#', // hash mark
		'+', // plus sign
		'-', // minus sign (hyphen)
		'.', // dot
		'!', // exclamation mark
		'<', '>',
	];

	/**
	 * @inheritDoc
	 */
	protected function prepare()
	{
		// reset references
		$this->references = [];
	}

	/**
	 * Consume lines for a paragraph
	 *
	 * Allow headlines and code to break paragraphs
	 */
	protected function consumeParagraph($lines, $current)
	{
		// consume until newline
		$content = [];
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
			$line = $lines[$i];

			// a list may break a paragraph when it is inside of a list
			if (isset($this->context[1]) && $this->context[1] === 'list' && !ctype_alpha($line[0]) && (
				$this->identifyUl($line, $lines, $i) || $this->identifyOl($line, $lines, $i))) {
				break;
			}

			if ($line === '' || ltrim($line) === '' || $this->identifyHeadline($line, $lines, $i)) {
				break;
			} elseif ($line[0] === "\t" || $line[0] === " " && strncmp($line, '    ', 4) === 0) {
				// possible beginning of a code block
				// but check for continued inline HTML
				// e.g.
				if (preg_match('~<\w+([^>]+)$~s', implode("\n", $content))) {
					$content[] = $line;
				} else {
					break;
				}
			} else {
				$content[] = $line;
			}
		}
		$block = [
			'paragraph',
			'content' => $this->parseInline(implode("\n", $content)),
		];
		return [$block, --$i];
	}

	/**
	 * @inheritdocs
	 *
	 * Parses a newline indicated by two spaces on the end of a markdown line.
	 */
	protected function renderText($text)
	{
		return str_replace("  \n", $this->html5 ? "
\n" : "
\n", $text[1]);
	}
}


<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown\block;

/**
 * Adds the 4 space indented code blocks
 */
trait CodeTrait
{
	/**
	 * identify a line as the beginning of a code block.
	 */
	protected function identifyCode($line)
	{
		// indentation >= 4 or one tab is code
		return ($l = $line[0]) === ' ' && $line[1] === ' ' && $line[2] === ' ' && $line[3] === ' ' || $l === "\t";
	}

	/**
	 * Consume lines for a code block element
	 */
	protected function consumeCode($lines, $current)
	{
		// consume until newline

		$content = [];
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
			$line = $lines[$i];

			// a line is considered to belong to this code block as long as it is intended by 4 spaces or a tab
			if (isset($line[0]) && ($line[0] === "\t" || strncmp($line, '    ', 4) === 0)) {
				$line = $line[0] === "\t" ? substr($line, 1) : substr($line, 4);
				$content[] = $line;
			// but also if it is empty and the next line is intended by 4 spaces or a tab
			} elseif (($line === '' || rtrim($line) === '') && isset($lines[$i + 1][0]) &&
				      ($lines[$i + 1][0] === "\t" || strncmp($lines[$i + 1], '    ', 4) === 0)) {
				if ($line !== '') {
					$line = $line[0] === "\t" ? substr($line, 1) : substr($line, 4);
				}
				$content[] = $line;
			} else {
				break;
			}
		}

		$block = [
			'code',
			'content' => implode("\n", $content),
		];
		return [$block, --$i];
	}

	/**
	 * Renders a code block
	 */
	protected function renderCode($block)
	{
		$class = isset($block['language']) ? ' class="language-' . $block['language'] . '"' : '';
		return "
" . htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8') . "
\n";
	}
}


<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown\block;

/**
 * Adds the list blocks
 */
trait ListTrait
{
	/**
	 * @var bool enable support `start` attribute of ordered lists. This means that lists
	 * will start with the number you actually type in markdown and not the HTML generated one.
	 * Defaults to `false` which means that numeration of all ordered lists(
) starts with 1.
	 */
	public $keepListStartNumber = false;

	/**
	 * identify a line as the beginning of an ordered list.
	 */
	protected function identifyOl($line)
	{
		return (($l = $line[0]) > '0' && $l <= '9' || $l === ' ') && preg_match('/^ {0,3}\d+\.[ \t]/', $line);
	}

	/**
	 * identify a line as the beginning of an unordered list.
	 */
	protected function identifyUl($line)
	{
		$l = $line[0];
		return ($l === '-' || $l === '+' || $l === '*') && (isset($line[1]) && (($l1 = $line[1]) === ' ' || $l1 === "\t")) ||
		       ($l === ' ' && preg_match('/^ {0,3}[\-\+\*][ \t]/', $line));
	}

	/**
	 * Consume lines for an ordered list
	 */
	protected function consumeOl($lines, $current)
	{
		// consume until newline

		$block = [
			'list',
			'list' => 'ol',
			'attr' => [],
			'items' => [],
		];
		return $this->consumeList($lines, $current, $block, 'ol');
	}

	/**
	 * Consume lines for an unordered list
	 */
	protected function consumeUl($lines, $current)
	{
		// consume until newline

		$block = [
			'list',
			'list' => 'ul',
			'items' => [],
		];
		return $this->consumeList($lines, $current, $block, 'ul');
	}

	private function consumeList($lines, $current, $block, $type)
	{
		$item = 0;
		$indent = '';
		$len = 0;
		$lastLineEmpty = false;
		// track the indentation of list markers, if indented more than previous element
		// a list marker is considered to be long to a lower level
		$leadSpace = 3;
		$marker = $type === 'ul' ? ltrim($lines[$current])[0] : '';
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
			$line = $lines[$i];
			// match list marker on the beginning of the line
			$pattern = ($type === 'ol') ? '/^( {0,'.$leadSpace.'})(\d+)\.[ \t]+/' : '/^( {0,'.$leadSpace.'})\\'.$marker.'[ \t]+/';
			if (preg_match($pattern, $line, $matches)) {
				if (($len = substr_count($matches[0], "\t")) > 0) {
					$indent = str_repeat("\t", $len);
					$line = substr($line, strlen($matches[0]));
				} else {
					$len = strlen($matches[0]);
					$indent = str_repeat(' ', $len);
					$line = substr($line, $len);
				}
				if ($i === $current) {
					$leadSpace = strlen($matches[1]) + 1;
				}

				if ($type === 'ol' && $this->keepListStartNumber) {
					// attr `start` for ol
					if (!isset($block['attr']['start']) && isset($matches[2])) {
						$block['attr']['start'] = $matches[2];
					}
				}

				$block['items'][++$item][] = $line;
				$block['lazyItems'][$item] = $lastLineEmpty;
				$lastLineEmpty = false;
			} elseif (ltrim($line) === '') {
				// line is empty, may be a lazy list
				$lastLineEmpty = true;

				// two empty lines will end the list
				if (!isset($lines[$i + 1][0])) {
					break;

				// next item is the continuation of this list -> lazy list
				} elseif (preg_match($pattern, $lines[$i + 1])) {
					$block['items'][$item][] = $line;
					$block['lazyItems'][$item] = true;

				// next item is indented as much as this list -> lazy list if it is not a reference
				} elseif (strncmp($lines[$i + 1], $indent, $len) === 0 || !empty($lines[$i + 1]) && $lines[$i + 1][0] == "\t") {
					$block['items'][$item][] = $line;
					$nextLine = $lines[$i + 1][0] === "\t" ? substr($lines[$i + 1], 1) : substr($lines[$i + 1], $len);
					$block['lazyItems'][$item] = empty($nextLine) || !method_exists($this, 'identifyReference') || !$this->identifyReference($nextLine);

				// everything else ends the list
				} else {
					break;
				}
			} else {
				if ($line[0] === "\t") {
					$line = substr($line, 1);
				} elseif (strncmp($line, $indent, $len) === 0) {
					$line = substr($line, $len);
				}
				$block['items'][$item][] = $line;
				$lastLineEmpty = false;
			}

			// if next line is
, end the list
			if (!empty($lines[$i + 1]) && method_exists($this, 'identifyHr') && $this->identifyHr($lines[$i + 1])) {
				break;
			}
		}

		foreach($block['items'] as $itemId => $itemLines) {
			$content = [];
			if (!$block['lazyItems'][$itemId]) {
				$firstPar = [];
				while (!empty($itemLines) && rtrim($itemLines[0]) !== '' && $this->detectLineType($itemLines, 0) === 'paragraph') {
					$firstPar[] = array_shift($itemLines);
				}
				$content = $this->parseInline(implode("\n", $firstPar));
			}
			if (!empty($itemLines)) {
				$content = array_merge($content, $this->parseBlocks($itemLines));
			}
			$block['items'][$itemId] = $content;
		}

		return [$block, $i];
	}

	/**
	 * Renders a list
	 */
	protected function renderList($block)
	{
		$type = $block['list'];

		if (!empty($block['attr'])) {
			$output = "<$type " . $this->generateHtmlAttributes($block['attr']) . ">\n";
		} else {
			$output = "<$type>\n";
		}

		foreach ($block['items'] as $item => $itemLines) {
			$output .= '
' . $this->renderAbsy($itemLines). "
\n";
		}
		return $output . "</$type>\n";
	}

	/**
	 * Return html attributes string from [attrName => attrValue] list
	 * @param array $attributes the attribute name-value pairs.
	 * @return string
	 */
	private function generateHtmlAttributes($attributes)
	{
		foreach ($attributes as $name => $value) {
			$attributes[$name] = "$name=\"$value\"";
		}
		return implode(' ', $attributes);
	}

	abstract protected function parseBlocks($lines);
	abstract protected function parseInline($text);
	abstract protected function renderAbsy($absy);
	abstract protected function detectLineType($lines, $current);
}


<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown\block;

/**
 * Adds the block quote elements
 */
trait QuoteTrait
{
	/**
	 * identify a line as the beginning of a block quote.
	 */
	protected function identifyQuote($line)
	{
		return $line[0] === '>' && (!isset($line[1]) || ($l1 = $line[1]) === ' ' || $l1 === "\t");
	}

	/**
	 * Consume lines for a blockquote element
	 */
	protected function consumeQuote($lines, $current)
	{
		// consume until newline
		$content = [];
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
			$line = $lines[$i];
			if (ltrim($line) !== '') {
				if ($line[0] == '>' && !isset($line[1])) {
					$line = '';
				} elseif (strncmp($line, '> ', 2) === 0) {
					$line = substr($line, 2);
				}
				$content[] = $line;
			} else {
				break;
			}
		}

		$block = [
			'quote',
			'content' => $this->parseBlocks($content),
			'simple' => true,
		];
		return [$block, $i];
	}

	/**
	 * Renders a blockquote
	 */
	protected function renderQuote($block)
	{
		return '<blockquote>' . $this->renderAbsy($block['content']) . "</blockquote>\n";
	}

	abstract protected function parseBlocks($lines);
	abstract protected function renderAbsy($absy);
}


<?php

namespace cebe\markdown;

use cebe\markdown\block\TableTrait;

// work around https://github.com/facebook/hhvm/issues/1120
defined('ENT_HTML401') || define('ENT_HTML401', 0);

/**
 * Markdown parser for the [markdown extra](http://michelf.ca/projects/php-markdown/extra/) flavor.
 *
 * @author Carsten Brandt <mail@cebe.cc>
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */
class MarkdownExtra extends Markdown
{
	// include block element parsing using traits
	use block\TableTrait;
	use block\FencedCodeTrait;

	// include inline element parsing using traits
	// TODO

	/**
	 * @var bool whether special attributes on code blocks should be applied on the `<pre>` element.
	 * The default behavior is to put them on the `<code>` element.
	 */
	public $codeAttributesOnPre = false;

	/**
	 * @inheritDoc
	 */
	protected $escapeCharacters = [
		// from Markdown
		'\\', // backslash
		'`', // backtick
		'*', // asterisk
		'_', // underscore
		'{', '}', // curly braces
		'[', ']', // square brackets
		'(', ')', // parentheses
		'#', // hash mark
		'+', // plus sign
		'-', // minus sign (hyphen)
		'.', // dot
		'!', // exclamation mark
		'<', '>',
		// added by MarkdownExtra
		':', // colon
		'|', // pipe
	];

	private $_specialAttributesRegex = '\{(([#\.][A-z0-9-_]+\s*)+)\}';

	// TODO allow HTML intended 3 spaces

	// TODO add markdown inside HTML blocks

	// TODO implement definition lists

	// TODO implement footnotes

	// TODO implement Abbreviations

	// block parsing

	protected function identifyReference($line)
	{
		return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[[^\[](.*?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $line);
	}

	/**
	 * Consume link references
	 */
	protected function consumeReference($lines, $current)
	{
		while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $lines[$current], $matches)) {
			$label = strtolower($matches[1]);

			$this->references[$label] = [
				'url' => $this->replaceEscape($matches[2]),
			];
			if (isset($matches[3])) {
				$this->references[$label]['title'] = $matches[3];
			} else {
				// title may be on the next line
				if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
					$this->references[$label]['title'] = $matches[1];
					$current++;
				}
			}
			if (isset($matches[5])) {
				$this->references[$label]['attributes'] = $matches[5];
			}
			$current++;
		}
		return [false, --$current];
	}

	/**
	 * Consume lines for a fenced code block
	 */
	protected function consumeFencedCode($lines, $current)
	{
		// consume until ```
		$block = [
			'code',
		];
		$line = trim($lines[$current]);
		if (($pos = strrpos($line, '`')) === false) {
			$pos = strrpos($line, '~');
		}
		$fence = substr($line, 0, $pos + 1);
		$block['attributes'] = substr($line, $pos);
		$content = [];
		for($i = $current + 1, $count = count($lines); $i < $count; $i++) {
			if (($pos = strpos($line = $lines[$i], $fence)) === false || $pos > 3) {
				$content[] = $line;
			} else {
				break;
			}
		}
		$block['content'] = implode("\n", $content);
		return [$block, $i];
	}

	protected function renderCode($block)
	{
		$attributes = $this->renderAttributes($block);
		return ($this->codeAttributesOnPre ? "
" : "
")
			. htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8')
			. "
\n";
	}

	/**
	 * Renders a headline
	 */
	protected function renderHeadline($block)
	{
		foreach($block['content'] as $i => $element) {
			if ($element[0] === 'specialAttributes') {
				unset($block['content'][$i]);
				$block['attributes'] = $element[1];
			}
		}
		$tag = 'h' . $block['level'];
		$attributes = $this->renderAttributes($block);
		return "<$tag$attributes>" . rtrim($this->renderAbsy($block['content']), "# \t") . "</$tag>\n";
	}

	protected function renderAttributes($block)
	{
		$html = [];
		if (isset($block['attributes'])) {
			$attributes = preg_split('/\s+/', $block['attributes'], -1, PREG_SPLIT_NO_EMPTY);
			foreach($attributes as $attribute) {
				if ($attribute[0] === '#') {
					$html['id'] = substr($attribute, 1);
				} else {
					$html['class'][] = substr($attribute, 1);
				}
			}
		}
		$result = '';
		foreach($html as $attr => $value) {
			if (is_array($value)) {
				$value = trim(implode(' ', $value));
			}
			if (!empty($value)) {
				$result .= " $attr=\"$value\"";
			}
		}
		return $result;
	}

	// inline parsing

	/**
	 * @marker {
	 */
	protected function parseSpecialAttributes($text)
	{
		if (preg_match("~$this->_specialAttributesRegex~", $text, $matches)) {
			return [['specialAttributes', $matches[1]], strlen($matches[0])];
		}
		return [['text', '{'], 1];
	}

	protected function renderSpecialAttributes($block)
	{
		return '{' . $block[1] . '}';
	}

	protected function parseInline($text)
	{
		$elements = parent::parseInline($text);
		// merge special attribute elements to links and images as they are not part of the final absy later
		$relatedElement = null;
		foreach($elements as $i => $element) {
			if ($element[0] === 'link' || $element[0] === 'image') {
				$relatedElement = $i;
			} elseif ($element[0] === 'specialAttributes') {
				if ($relatedElement !== null) {
					$elements[$relatedElement]['attributes'] = $element[1];
					unset($elements[$i]);
				}
				$relatedElement = null;
			} else {
				$relatedElement = null;
			}
		}
		return $elements;
	}

	protected function renderLink($block)
	{
		if (isset($block['refkey'])) {
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
				$block = array_merge($block, $ref);
			} else {
				if (strncmp($block['orig'], '[', 1) === 0) {
					return '[' . $this->renderAbsy($this->parseInline(substr($block['orig'], 1)));
				}
				return $block['orig'];
			}
		}
		$attributes = $this->renderAttributes($block);
		return '' . $this->renderAbsy($block['text']) . '';
	}

	protected function renderImage($block)
	{
		if (isset($block['refkey'])) {
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
				$block = array_merge($block, $ref);
			} else {
				if (strncmp($block['orig'], '![', 2) === 0) {
					return '![' . $this->renderAbsy($this->parseInline(substr($block['orig'], 2)));
				}
				return $block['orig'];
			}
		}
		$attributes = $this->renderAttributes($block);
		return 'html5 ? '>' : ' />');
	}
}


<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown\inline;

/**
 * Adds strikeout inline elements
 */
trait StrikeoutTrait
{
	/**
	 * Parses the strikethrough feature.
	 * @marker ~~
	 */
	protected function parseStrike($markdown)
	{
		if (preg_match('/^~~(.+?)~~/', $markdown, $matches)) {
			return [
				[
					'strike',
					$this->parseInline($matches[1])
				],
				strlen($matches[0])
			];
		}
		return [['text', $markdown[0] . $markdown[1]], 2];
	}

	protected function renderStrike($block)
	{
		return '<del>' . $this->renderAbsy($block[1]) . '</del>';
	}

    abstract protected function parseInline($text);
    abstract protected function renderAbsy($blocks);
}


<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown\block;

/**
 * Adds the table blocks
 */
trait TableTrait
{
	/**
	 * identify a line as the beginning of a table block.
	 */
	protected function identifyTable($line, $lines, $current)
	{
		return strpos($line, '|') !== false && isset($lines[$current + 1])
			&& preg_match('~^\\s*\\|?(\\s*:?-[\\-\\s]*:?\\s*\\|?)*\\s*$~', $lines[$current + 1])
			&& strpos($lines[$current + 1], '|') !== false
			&& isset($lines[$current + 2]) && trim($lines[$current + 1]) !== '';
	}

	/**
	 * Consume lines for a table
	 */
	protected function consumeTable($lines, $current)
	{
		// consume until newline

		$block = [
			'table',
			'cols' => [],
			'rows' => [],
		];
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
			$line = trim($lines[$i]);

			// extract alignment from second line
			if ($i == $current+1) {
				$cols = explode('|', trim($line, ' |'));
				foreach($cols as $col) {
					$col = trim($col);
					if (empty($col)) {
						$block['cols'][] = '';
						continue;
					}
					$l = ($col[0] === ':');
					$r = (substr($col, -1, 1) === ':');
					if ($l && $r) {
						$block['cols'][] = 'center';
					} elseif ($l) {
						$block['cols'][] = 'left';
					} elseif ($r) {
						$block['cols'][] = 'right';
					} else {
						$block['cols'][] = '';
					}
				}

				continue;
			}
			if ($line === '' || substr($lines[$i], 0, 4) === '    ') {
				break;
			}
			if ($line[0] === '|') {
				$line = substr($line, 1);
			}
			if (substr($line, -1, 1) === '|' && (substr($line, -2, 2) !== '\\|' || substr($line, -3, 3) === '\\\\|')) {
				$line = substr($line, 0, -1);
			}

			array_unshift($this->context, 'table');
			$row = $this->parseInline($line);
			array_shift($this->context);

			$r = count($block['rows']);
			$c = 0;
			$block['rows'][] = [];
			foreach ($row as $absy) {
				if (!isset($block['rows'][$r][$c])) {
					$block['rows'][$r][] = [];
				}
				if ($absy[0] === 'tableBoundary') {
					$c++;
				} else {
					$block['rows'][$r][$c][] = $absy;
				}
			}
		}

		return [$block, --$i];
	}

	/**
	 * render a table block
	 */
	protected function renderTable($block)
	{
		$head = '';
		$body = '';
		$cols = $block['cols'];
		$first = true;
		foreach($block['rows'] as $row) {
			$cellTag = $first ? 'th' : 'td';
			$tds = '';
			foreach ($row as $c => $cell) {
				$align = empty($cols[$c]) ? '' : ' align="' . $cols[$c] . '"';
				$tds .= "<$cellTag$align>" . trim($this->renderAbsy($cell)) . "</$cellTag>";
			}
			if ($first) {
				$head .= "
$tds
\n";
			} else {
				$body .= "
$tds
\n";
			}
			$first = false;
		}
		return $this->composeTable($head, $body);
	}

	/**
	 * This method composes a table from parsed body and head HTML.
	 *
	 * You may override this method to customize the table rendering, for example by
	 * adding a `class` to the table tag:
	 *
	 * ```php
	 * return "<table class="table table-striped">\n<thead>\n$head</thead>\n<tbody>\n$body</tbody>\n</table>\n"
	 * ```
	 *
	 * @param string $head table head HTML.
	 * @param string $body table body HTML.
	 * @return string the complete table HTML.
	 * @since 1.2.0
	 */
	protected function composeTable($head, $body)
	{
		return "
\n
\n$head
\n
\n$body
\n
\n";
	}

	/**
	 * @marker |
	 */
	protected function parseTd($markdown)
	{
		if (isset($this->context[1]) && $this->context[1] === 'table') {
			return [['tableBoundary'], isset($markdown[1]) && $markdown[1] === ' ' ? 2 : 1];
		}
		return [['text', $markdown[0]], 1];
	}

	abstract protected function parseInline($text);
	abstract protected function renderAbsy($absy);
}


<?php
/**
 * @copyright Copyright (c) 2014 Carsten Brandt
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
 * @link https://github.com/cebe/markdown#readme
 */

namespace cebe\markdown\inline;

// work around https://github.com/facebook/hhvm/issues/1120
defined('ENT_HTML401') || define('ENT_HTML401', 0);

/**
 * Adds auto linking for URLs
 */
trait UrlLinkTrait
{
	/**
	 * Parses urls and adds auto linking feature.
	 * @marker http
	 * @marker ftp
	 */
	protected function parseUrl($markdown)
	{
		$pattern = <<<REGEXP
			/(?(R) # in case of recursion match parentheses
				 \(((?>[^\s()]+)|(?R))*\)
			|      # else match a link with title
				^(https?|ftp):\/\/(([^\s<>()]+)|(?R))+(?<![\.,:;\'"!\?\s])
			)/x
REGEXP;

		if (!in_array('parseLink', $this->context) && preg_match($pattern, $markdown, $matches)) {
			return [
				['autoUrl', $matches[0]],
				strlen($matches[0])
			];
		}
		return [['text', substr($markdown, 0, 4)], 4];
	}

	protected function renderAutoUrl($block)
	{
		$href = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
		$decodedUrl = urldecode($block[1]);
		$secureUrlText = preg_match('//u', $decodedUrl) ? $decodedUrl : $block[1];
		$text = htmlspecialchars($secureUrlText, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
		return "$text";
	}
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Action\Profile\Outbound\Message;

use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use Psr\Log\LoggerInterface;

/**
 * Sets the Version of the outbound message to the given value.
 */
class MessageVersionAction extends AbstractProfileAction
{
    /** @var string */
    private $version;

    /**
     * @param LoggerInterface $logger
     * @param string          $version
     */
    public function __construct(LoggerInterface $logger, $version)
    {
        parent::__construct($logger);

        $this->version = $version;
    }

    /**
     * @param ProfileContext $context
     *
     * @return void
     */
    protected function doExecute(ProfileContext $context)
    {
        MessageContextHelper::asSamlMessage($context->getOutboundContext())
            ->setVersion($this->version);

        $this->logger->debug(
            sprintf('Message Version set to "%s"', $this->version),
            LogHelper::getActionContext($context, $this)
        );
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Builder\Profile\WebBrowserSso\Sp;

use LightSaml\Build\Container\BuildContainerInterface;
use LightSaml\Builder\Action\Profile\SingleSignOn\Sp\SsoSpSendAuthnRequestActionBuilder;
use LightSaml\Builder\Profile\AbstractProfileBuilder;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Meta\TrustOptions\TrustOptions;
use LightSaml\Model\Metadata\EntityDescriptor;
use LightSaml\Profile\Profiles;

class SsoSpSendAuthnRequestProfileBuilder extends AbstractProfileBuilder
{
    protected $idpEntityId;

    /**
     * @param BuildContainerInterface $buildContainer
     * @param string                  $idpEntityId
     */
    public function __construct(BuildContainerInterface $buildContainer, $idpEntityId)
    {
        parent::__construct($buildContainer);

        $this->idpEntityId = $idpEntityId;
    }

    public function buildContext()
    {
        $result = parent::buildContext();

        $idpEd = $this->container->getPartyContainer()->getIdpEntityDescriptorStore()->get($this->idpEntityId);
        if (false == $idpEd) {
            throw new \RuntimeException(sprintf('Unknown IDP "%s"', $this->idpEntityId));
        }

        $trustOptions = $this->getTrustOptions($idpEd);

        $result->getPartyEntityContext()
            ->setEntityDescriptor($idpEd)
            ->setTrustOptions($trustOptions)
        ;

        return $result;
    }

    /**
     * @return string
     */
    protected function getProfileId()
    {
        return Profiles::SSO_SP_SEND_AUTHN_REQUEST;
    }

    /**
     * @return string
     */
    protected function getProfileRole()
    {
        return ProfileContext::ROLE_SP;
    }

    /**
     * @return \LightSaml\Builder\Action\ActionBuilderInterface
     */
    protected function getActionBuilder()
    {
        return new SsoSpSendAuthnRequestActionBuilder($this->container);
    }

    /**
     * @param EntityDescriptor $idpEd
     *
     * @return TrustOptions
     */
    private function getTrustOptions(EntityDescriptor $idpEd)
    {
        $trustOptions = $this->container->getPartyContainer()->getTrustOptionsStore()->get($this->idpEntityId) ?: new TrustOptions();

        $wantAuthnRequestsSigned = $idpEd->getFirstIdpSsoDescriptor()->getWantAuthnRequestsSigned();

        if (null !== $wantAuthnRequestsSigned) {
            $trustOptions->setSignAuthnRequest($wantAuthnRequestsSigned);
        }

        return $trustOptions;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Action\Profile;

use LightSaml\Action\ActionInterface;
use LightSaml\Context\ContextInterface;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use Psr\Log\LoggerInterface;

abstract class AbstractProfileAction implements ActionInterface
{
    /** @var LoggerInterface */
    protected $logger;

    /**
     * @param LoggerInterface $logger
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param ContextInterface $context
     *
     * @return void
     */
    public function execute(ContextInterface $context)
    {
        if ($context instanceof ProfileContext) {
            $this->doExecute($context);
        } else {
            $message = sprintf('Expected ProfileContext but got %s', get_class($context));
            $this->logger->emergency($message, array('context' => $context));
            throw new LightSamlContextException($context, $message);
        }
    }

    abstract protected function doExecute(ProfileContext $context);
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Resolver\Endpoint;

use LightSaml\Model\Metadata\EndpointReference;
use LightSaml\Resolver\Endpoint\Criteria\LocationCriteria;
use LightSaml\Criteria\CriteriaSet;

class LocationEndpointResolver implements EndpointResolverInterface
{
    /**
     * @param CriteriaSet         $criteriaSet
     * @param EndpointReference[] $candidates
     *
     * @return EndpointReference[]
     */
    public function resolve(CriteriaSet $criteriaSet, array $candidates)
    {
        if (false === $criteriaSet->has(LocationCriteria::class)) {
            return $candidates;
        }

        $result = array();
        /** @var LocationCriteria $locationCriteria */
        foreach ($criteriaSet->get(LocationCriteria::class) as $locationCriteria) {
            foreach ($candidates as $endpointReference) {
                if ($endpointReference->getEndpoint()->getLocation() == $locationCriteria->getLocation()) {
                    $result[] = $endpointReference;
                }
            }
        }

        return $result;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Model\Metadata;

use LightSaml\Error\LightSamlXmlException;
use LightSaml\Model\Context\DeserializationContext;
use LightSaml\Model\Context\SerializationContext;
use LightSaml\Model\AbstractSamlModel;
use LightSaml\SamlConstants;
use LightSaml\Credential\X509Certificate;

class KeyDescriptor extends AbstractSamlModel
{
    const USE_SIGNING = 'signing';
    const USE_ENCRYPTION = 'encryption';

    /** @var string */
    protected $use;

    /** @var X509Certificate */
    private $certificate;

    /**
     * @param string|null          $use
     * @param X509Certificate|null $certificate
     */
    public function __construct($use = null, X509Certificate $certificate = null)
    {
        $this->use = $use;
        $this->certificate = $certificate;
    }

    /**
     * @param string $use
     *
     * @return KeyDescriptor
     *
     * @throws \InvalidArgumentException
     */
    public function setUse($use)
    {
        $use = trim($use);
        if (false != $use && self::USE_ENCRYPTION != $use && self::USE_SIGNING != $use) {
            throw new \InvalidArgumentException(sprintf("Invalid use value '%s'", $use));
        }
        $this->use = $use;

        return $this;
    }

    /**
     * @return string
     */
    public function getUse()
    {
        return $this->use;
    }

    /**
     * @param X509Certificate $certificate
     *
     * @return KeyDescriptor
     */
    public function setCertificate(X509Certificate $certificate)
    {
        $this->certificate = $certificate;

        return $this;
    }

    /**
     * @return X509Certificate
     */
    public function getCertificate()
    {
        return $this->certificate;
    }

    /**
     * @param \DOMNode             $parent
     * @param SerializationContext $context
     *
     * @return void
     */
    public function serialize(\DOMNode $parent, SerializationContext $context)
    {
        $result = $this->createElement('KeyDescriptor', SamlConstants::NS_METADATA, $parent, $context);

        $this->attributesToXml(array('use'), $result);

        $keyInfo = $this->createElement('ds:KeyInfo', SamlConstants::NS_XMLDSIG, $result, $context);
        $xData = $this->createElement('ds:X509Data', SamlConstants::NS_XMLDSIG, $keyInfo, $context);
        $xCert = $this->createElement('ds:X509Certificate', SamlConstants::NS_XMLDSIG, $xData, $context);

        $xCert->nodeValue = $this->getCertificate()->getData();
    }

    /**
     * @param \DOMNode               $node
     * @param DeserializationContext $context
     */
    public function deserialize(\DOMNode $node, DeserializationContext $context)
    {
        $this->checkXmlNodeName($node, 'KeyDescriptor', SamlConstants::NS_METADATA);

        $this->attributesFromXml($node, array('use'));

        $list = $context->getXpath()->query('./ds:KeyInfo/ds:X509Data/ds:X509Certificate', $node);
        if (1 != $list->length) {
            throw new LightSamlXmlException('Missing X509Certificate node');
        }

        /** @var $x509CertificateNode \DOMElement */
        $x509CertificateNode = $list->item(0);
        $certificateData = trim($x509CertificateNode->textContent);
        if (false == $certificateData) {
            throw new LightSamlXmlException('Missing certificate data');
        }

        $this->certificate = new X509Certificate();
        $this->certificate->setData($certificateData);
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Action;

use LightSaml\Context\ContextInterface;
use LightSaml\Context\Profile\ExceptionContext;
use LightSaml\Context\Profile\ProfileContexts;

class CatchableErrorAction implements ActionInterface
{
    /** @var ActionInterface */
    protected $mainAction;

    /** @var ActionInterface */
    protected $errorAction;

    /**
     * @param ActionInterface $mainAction
     * @param ActionInterface $errorAction
     */
    public function __construct(ActionInterface $mainAction, ActionInterface $errorAction)
    {
        $this->mainAction = $mainAction;
        $this->errorAction = $errorAction;
    }

    /**
     * @param ContextInterface $context
     *
     * @return void
     */
    public function execute(ContextInterface $context)
    {
        try {
            $this->mainAction->execute($context);
        } catch (\Exception $ex) {
            /** @var ExceptionContext $exceptionContext */
            $exceptionContext = $context->getSubContext(ProfileContexts::EXCEPTION, ExceptionContext::class);
            $exceptionContext->addException($ex);

            $this->errorAction->execute($context);
        }
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Model\Assertion;

use LightSaml\Credential\CredentialInterface;
use LightSaml\Model\Context\DeserializationContext;
use LightSaml\Model\Context\SerializationContext;
use LightSaml\Error\LightSamlSecurityException;
use LightSaml\Error\LightSamlXmlException;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use RobRichards\XMLSecLibs\XMLSecEnc;

class EncryptedElementReader extends EncryptedElement
{
    /** @var XMLSecEnc */
    protected $xmlEnc;

    /** @var XMLSecurityKey */
    protected $symmetricKey;

    /** @var XMLSecurityKey */
    protected $symmetricKeyInfo;

    /**
     * @return XMLSecurityKey
     */
    public function getSymmetricKey()
    {
        return $this->symmetricKey;
    }

    /**
     * @return XMLSecurityKey
     */
    public function getSymmetricKeyInfo()
    {
        return $this->symmetricKeyInfo;
    }

    /**
     * @param \DOMNode             $parent
     * @param SerializationContext $context
     *
     * @throws \LogicException
     *
     * @return void
     */
    public function serialize(\DOMNode $parent, SerializationContext $context)
    {
        throw new \LogicException('EncryptedElementReader can not be used for serialization');
    }

    /**
     * @param \DOMNode               $node
     * @param DeserializationContext $context
     */
    public function deserialize(\DOMNode $node, DeserializationContext $context)
    {
        $list = $context->getXpath()->query('xenc:EncryptedData', $node);
        if (0 == $list->length) {
            throw new LightSamlXmlException('Missing encrypted data in <saml:EncryptedAssertion>');
        }
        if (1 != $list->length) {
            throw new LightSamlXmlException('More than one encrypted data element in <saml:EncryptedAssertion>');
        }

        /** @var \DOMElement $encryptedData */
        $encryptedData = $list->item(0);
        $this->xmlEnc = new XMLSecEnc();
        $this->xmlEnc->setNode($encryptedData);
        $this->xmlEnc->type = $encryptedData->getAttribute('Type');

        $this->symmetricKey = $this->loadSymmetricKey();

        $this->symmetricKeyInfo = $this->loadSymmetricKeyInfo($this->symmetricKey);
    }

    /**
     * @param XMLSecurityKey[] $inputKeys
     *
     * @throws \LogicException
     * @throws \LightSaml\Error\LightSamlXmlException
     * @throws \LightSaml\Error\LightSamlSecurityException
     *
     * @return \DOMElement
     */
    public function decryptMulti(array $inputKeys)
    {
        /** @var \LogicException $lastException */
        $lastException = null;

        foreach ($inputKeys as $key) {
            if ($key instanceof CredentialInterface) {
                $key = $key->getPrivateKey();
            }
            if (false == $key instanceof XMLSecurityKey) {
                throw new \InvalidArgumentException('Expected XMLSecurityKey');
            }

            try {
                return $this->decrypt($key);
            } catch (\Exception $ex) {
                $lastException = $ex;
            }
        }

        if ($lastException) {
            throw $lastException;
        }

        throw new LightSamlSecurityException('No key provided for decryption');
    }

    /**
     * @param XMLSecurityKey $inputKey
     *
     * @throws \LogicException
     * @throws \LightSaml\Error\LightSamlXmlException
     * @throws \LightSaml\Error\LightSamlSecurityException
     *
     * @return \DOMElement
     */
    public function decrypt(XMLSecurityKey $inputKey)
    {
        $this->symmetricKey = $this->loadSymmetricKey();
        $this->symmetricKeyInfo = $this->loadSymmetricKeyInfo($this->symmetricKey);

        if ($this->symmetricKeyInfo->isEncrypted) {
            $this->decryptSymmetricKey($inputKey);
        } else {
            $this->symmetricKey = $inputKey;
        }

        $decrypted = $this->decryptCipher();

        $result = $this->buildXmlElement($decrypted);

        return $result;
    }

    /**
     * @param string $decrypted
     *
     * @return \DOMElement
     */
    protected function buildXmlElement($decrypted)
    {
        /*
         * This is a workaround for the case where only a subset of the XML
         * tree was serialized for encryption. In that case, we may miss the
         * namespaces needed to parse the XML.
         */
        $xml = sprintf(
            '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">%s</root>',
            $decrypted
        );
        $newDoc = new \DOMDocument();
        if (false == @$newDoc->loadXML($xml)) {
            throw new LightSamlXmlException('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?');
        }
        $decryptedElement = $newDoc->firstChild->firstChild;
        if (null == $decryptedElement) {
            throw new LightSamlSecurityException('Missing encrypted element.');
        }

        if (false == $decryptedElement instanceof \DOMElement) {
            throw new LightSamlXmlException('Decrypted element was not actually a DOMElement.');
        }

        return $decryptedElement;
    }

    /**
     * @return string
     *
     * @throws \Exception
     */
    protected function decryptCipher()
    {
        $decrypted = $this->xmlEnc->decryptNode($this->symmetricKey, false);
        if (false == is_string($decrypted)) {
            throw new \LogicException('Expected decrypted string');
        }

        return $decrypted;
    }

    /**
     * @param XMLSecurityKey $inputKey
     *
     * @throws \Exception
     */
    protected function decryptSymmetricKey(XMLSecurityKey $inputKey)
    {
        /** @var XMLSecEnc $encKey */
        $encKey = $this->symmetricKeyInfo->encryptedCtx;
        $this->symmetricKeyInfo->key = $inputKey->key;

        $keySize = $this->symmetricKey->getSymmetricKeySize();
        if (null === $keySize) {
            // To protect against "key oracle" attacks, we need to be able to create a
            // symmetric key, and for that we need to know the key size.
            throw new LightSamlSecurityException(sprintf(
                "Unknown key size for encryption algorithm: '%s'",
                $this->symmetricKey->type
            ));
        }

        /** @var string $key */
        $key = $encKey->decryptKey($this->symmetricKeyInfo);
        if (false == is_string($key)) {
            throw new \LogicException('Expected string');
        }
        if (strlen($key) != $keySize) {
            throw new LightSamlSecurityException(sprintf(
                "Unexpected key size of '%s' bits for encryption algorithm '%s', expected '%s' bits size",
                strlen($key) * 8,
                $this->symmetricKey->type,
                $keySize
            ));
        }

        $this->symmetricKey->loadkey($key);
    }

    /**
     * @return XMLSecurityKey
     *
     * @throws \LightSaml\Error\LightSamlXmlException
     */
    protected function loadSymmetricKey()
    {
        $symmetricKey = $this->xmlEnc->locateKey();
        if (false == $symmetricKey) {
            throw new LightSamlXmlException('Could not locate key algorithm in encrypted data');
        }

        return $symmetricKey;
    }

    /**
     * @param XMLSecurityKey $symmetricKey
     *
     * @throws \LightSaml\Error\LightSamlXmlException
     *
     * @return XMLSecurityKey
     */
    protected function loadSymmetricKeyInfo(XMLSecurityKey $symmetricKey)
    {
        $symmetricKeyInfo = $this->xmlEnc->locateKeyInfo($symmetricKey);
        if (false == $symmetricKeyInfo) {
            throw new LightSamlXmlException('Could not locate <dsig:KeyInfo> for the encrypted key');
        }

        return $symmetricKeyInfo;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Action\Profile\Inbound\Message;

use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\Helper\MessageContextHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Error\LightSamlValidationException;
use LightSaml\Validator\Model\NameId\NameIdValidatorInterface;
use Psr\Log\LoggerInterface;

class IssuerValidatorAction extends AbstractProfileAction
{
    /** @var NameIdValidatorInterface */
    protected $nameIdValidator;

    /** @var string */
    protected $allowedFormat;

    /**
     * @param LoggerInterface          $logger
     * @param NameIdValidatorInterface $nameIdValidator
     * @param string                   $allowedFormat
     */
    public function __construct(LoggerInterface $logger, NameIdValidatorInterface $nameIdValidator, $allowedFormat)
    {
        parent::__construct($logger);

        $this->nameIdValidator = $nameIdValidator;
        $this->allowedFormat = $allowedFormat;
    }

    /**
     * @param ProfileContext $context
     *
     * @return void
     */
    protected function doExecute(ProfileContext $context)
    {
        $message = MessageContextHelper::asSamlMessage($context->getInboundContext());

        if (false == $message->getIssuer()) {
            $message = 'Inbound message must have Issuer element';
            $this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
            throw new LightSamlContextException($context, $message);
        }

        if ($this->allowedFormat &&
            $message->getIssuer()->getValue() &&
            $message->getIssuer()->getFormat() &&
            $message->getIssuer()->getFormat() != $this->allowedFormat
        ) {
            $message = sprintf(
                "Response Issuer Format if set must have value '%s' but it was '%s'",
                $this->allowedFormat,
                $message->getIssuer()->getFormat()
            );
            $this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
            throw new LightSamlContextException($context, $message);
        }

        try {
            $this->nameIdValidator->validateNameId($message->getIssuer());
        } catch (LightSamlValidationException $ex) {
            throw new LightSamlContextException($context, $ex->getMessage(), 0, $ex);
        }
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Validator\Model\Subject;

use LightSaml\Error\LightSamlValidationException;
use LightSaml\Helper;
use LightSaml\Model\Assertion\Subject;
use LightSaml\Model\Assertion\SubjectConfirmation;
use LightSaml\Model\Assertion\SubjectConfirmationData;
use LightSaml\Validator\Model\NameId\NameIdValidatorInterface;

class SubjectValidator implements SubjectValidatorInterface
{
    /** @var NameIdValidatorInterface */
    protected $nameIdValidator;

    /**
     * @param NameIdValidatorInterface $nameIdValidator
     */
    public function __construct(NameIdValidatorInterface $nameIdValidator)
    {
        $this->nameIdValidator = $nameIdValidator;
    }

    /**
     * @param Subject $subject
     *
     * @throws LightSamlValidationException
     *
     * @return void
     */
    public function validateSubject(Subject $subject)
    {
        if (false == $subject->getNameID() &&
            false == $subject->getAllSubjectConfirmations()
        ) {
            throw new LightSamlValidationException('Subject MUST contain either an identifier or a subject confirmation');
        }

        if ($subject->getNameID()) {
            $this->nameIdValidator->validateNameId($subject->getNameID());
        }

        foreach ($subject->getAllSubjectConfirmations() as $subjectConfirmation) {
            $this->validateSubjectConfirmation($subjectConfirmation);
        }
    }

    /**
     * @param SubjectConfirmation $subjectConfirmation
     *
     * @throws \LightSaml\Error\LightSamlValidationException
     */
    protected function validateSubjectConfirmation(SubjectConfirmation $subjectConfirmation)
    {
        if (false == Helper::validateRequiredString($subjectConfirmation->getMethod())) {
            throw new LightSamlValidationException('Method attribute of SubjectConfirmation MUST contain at least one non-whitespace character');
        }
        if (false == Helper::validateWellFormedUriString($subjectConfirmation->getMethod())) {
            throw new LightSamlValidationException('SubjectConfirmation element has Method attribute which is not a wellformed absolute uri.');
        }
        if ($subjectConfirmation->getNameID()) {
            $this->nameIdValidator->validateNameId($subjectConfirmation->getNameID());
        }
        if ($subjectConfirmation->getSubjectConfirmationData()) {
            $this->validateSubjectConfirmationData($subjectConfirmation->getSubjectConfirmationData());
        }
    }

    protected function validateSubjectConfirmationData(SubjectConfirmationData $subjectConfirmationData)
    {
        if ($subjectConfirmationData->getRecipient()) {
            if (false == Helper::validateWellFormedUriString($subjectConfirmationData->getRecipient())) {
                throw new LightSamlValidationException('Recipient of SubjectConfirmationData must be a wellformed absolute URI.');
            }
        }
        if ($subjectConfirmationData->getNotBeforeTimestamp() &&
            $subjectConfirmationData->getNotOnOrAfterTimestamp() &&
            $subjectConfirmationData->getNotBeforeTimestamp() >= $subjectConfirmationData->getNotOnOrAfterTimestamp()
        ) {
            throw new LightSamlValidationException('SubjectConfirmationData NotBefore MUST be less than NotOnOrAfter');
        }
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Model\XmlDSig;

use LightSaml\Error\LightSamlSecurityException;
use LightSaml\Error\LightSamlXmlException;
use LightSaml\Model\Context\DeserializationContext;
use LightSaml\Model\Context\SerializationContext;
use LightSaml\SamlConstants;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecEnc;

class SignatureXmlReader extends AbstractSignatureReader
{
    /** @var XMLSecurityDSig */
    protected $signature;

    /** @var string[] */
    protected $certificates = array();

    /**
     * @param string $certificate
     */
    public function addCertificate($certificate)
    {
        $this->certificates[] = (string) $certificate;
    }

    /**
     * @return \string[]
     */
    public function getAllCertificates()
    {
        return $this->certificates;
    }

    /**
     * @param XMLSecurityDSig $signature
     */
    public function setSignature(XMLSecurityDSig $signature)
    {
        $this->signature = $signature;
    }

    /**
     * @return XMLSecurityDSig
     */
    public function getSignature()
    {
        return $this->signature;
    }

    /**
     * @param XMLSecurityKey $key
     *
     * @return bool
     *
     * @throws LightSamlSecurityException
     */
    public function validate(XMLSecurityKey $key)
    {
        if (null == $this->signature) {
            return false;
        }

        if (false == $this->signature->validateReference()) {
            throw new LightSamlSecurityException('Digest validation failed');
        }

        $key = $this->castKeyIfNecessary($key);

        if (false == $this->signature->verify($key)) {
            throw new LightSamlSecurityException('Unable to verify Signature');
        }

        return true;
    }

    /**
     * @return string
     *
     * @throws \LightSaml\Error\LightSamlXmlException
     */
    public function getAlgorithm()
    {
        $xpath = new \DOMXPath(
            $this->signature->sigNode instanceof \DOMDocument
            ? $this->signature->sigNode
            : $this->signature->sigNode->ownerDocument
        );
        $xpath->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS);

        $list = $xpath->query('./ds:SignedInfo/ds:SignatureMethod', $this->signature->sigNode);
        if (!$list || 0 == $list->length) {
            throw new LightSamlXmlException('Missing SignatureMethod element');
        }
        /** @var $sigMethod \DOMElement */
        $sigMethod = $list->item(0);
        if (!$sigMethod->hasAttribute('Algorithm')) {
            throw new LightSamlXmlException('Missing Algorithm-attribute on SignatureMethod element.');
        }
        $algorithm = $sigMethod->getAttribute('Algorithm');

        return $algorithm;
    }

    /**
     * @param \DOMNode             $parent
     * @param SerializationContext $context
     *
     * @throws \LogicException
     */
    public function serialize(\DOMNode $parent, SerializationContext $context)
    {
        throw new \LogicException('SignatureXmlReader can not be serialized');
    }

    /**
     * @param \DOMNode               $node
     * @param DeserializationContext $context
     *
     * @throws \Exception
     */
    public function deserialize(\DOMNode $node, DeserializationContext $context)
    {
        $this->checkXmlNodeName($node, 'Signature', SamlConstants::NS_XMLDSIG);

        $this->signature = new XMLSecurityDSig();
        $this->signature->idKeys[] = $this->getIDName();
        $this->signature->sigNode = $node;
        $this->signature->canonicalizeSignedInfo();

        $this->key = null;
        $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public'));
        XMLSecEnc::staticLocateKeyInfo($key, $node);
        if ($key->name || $key->key) {
            $this->key = $key;
        }

        $this->certificates = array();
        $list = $context->getXpath()->query('./ds:KeyInfo/ds:X509Data/ds:X509Certificate', $node);
        foreach ($list as $certNode) {
            $certData = trim($certNode->textContent);
            $certData = str_replace(array("\r", "\n", "\t", ' '), '', $certData);
            $this->certificates[] = $certData;
        }
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Model\XmlDSig;

use LightSaml\Credential\CredentialInterface;
use LightSaml\Credential\KeyHelper;
use LightSaml\Error\LightSamlSecurityException;
use RobRichards\XMLSecLibs\XMLSecurityKey;

abstract class AbstractSignatureReader extends Signature
{
    /** @var XMLSecurityKey|null */
    protected $key;

    /**
     * @param XMLSecurityKey $key
     *
     * @return bool True if validated, False if validation was not performed
     *
     * @throws \LightSaml\Error\LightSamlSecurityException If validation fails
     */
    abstract public function validate(XMLSecurityKey $key);

    /**
     * @return XMLSecurityKey|null
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * @param CredentialInterface[] $credentialCandidates
     *
     * @throws \InvalidArgumentException                   If element of $credentialCandidates array is not CredentialInterface
     * @throws \LightSaml\Error\LightSamlSecurityException If validation fails
     *
     * @return CredentialInterface|null Returns credential that validated the signature or null if validation was not performed
     */
    public function validateMulti(array $credentialCandidates)
    {
        $lastException = null;

        foreach ($credentialCandidates as $credential) {
            if (false == $credential instanceof CredentialInterface) {
                throw new \InvalidArgumentException('Expected CredentialInterface');
            }
            if (null == $credential->getPublicKey()) {
                continue;
            }

            try {
                $result = $this->validate($credential->getPublicKey());

                if (false === $result) {
                    return null;
                }

                return $credential;
            } catch (LightSamlSecurityException $ex) {
                $lastException = $ex;
            }
        }

        if ($lastException) {
            throw $lastException;
        } else {
            throw new LightSamlSecurityException('No public key available for signature verification');
        }
    }

    /**
     * @return string
     */
    abstract public function getAlgorithm();

    /**
     * @param XMLSecurityKey $key
     *
     * @return XMLSecurityKey
     */
    protected function castKeyIfNecessary(XMLSecurityKey $key)
    {
        $algorithm = $this->getAlgorithm();

        if (!in_array($algorithm, [
            XMLSecurityKey::RSA_SHA1,
            XMLSecurityKey::RSA_SHA256,
            XMLSecurityKey::RSA_SHA384,
            XMLSecurityKey::RSA_SHA512,
        ])) {
            throw new LightSamlSecurityException(sprintf('Unsupported signing algorithm: "%s"', $algorithm));
        }

        if ($algorithm != $key->type) {
            $key = KeyHelper::castKey($key, $algorithm);
        }

        return $key;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Resolver\Credential;

use LightSaml\Credential\CredentialInterface;
use LightSaml\Credential\Criteria\CredentialNameCriteria;
use LightSaml\Criteria\CriteriaSet;

class CredentialNameFilterResolver extends AbstractQueryableResolver
{
    /**
     * @param CriteriaSet           $criteriaSet
     * @param CredentialInterface[] $arrCredentials
     *
     * @return CredentialInterface[]
     */
    public function resolve(CriteriaSet $criteriaSet, array $arrCredentials = array())
    {
        if (false == $criteriaSet->has(CredentialNameCriteria::class)) {
            return $arrCredentials;
        }

        $result = array();
        foreach ($criteriaSet->get(CredentialNameCriteria::class) as $criteria) {
            /* @var CredentialNameCriteria $criteria */
            foreach ($arrCredentials as $credential) {
                $arrCredentialNames = $credential->getKeyNames();
                $criteriaName = $criteria->getName();
                foreach ($arrCredentialNames as $credentialName) {
                    if ($credentialName == $criteriaName) {
                        $result[] = $credential;
                        break;
                    }
                }
            }
        }

        return $result;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Resolver\Credential;

use LightSaml\Credential\CredentialInterface;
use LightSaml\Store\Credential\CredentialStoreInterface;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Credential\Criteria\EntityIdCriteria;

class EntityIdResolver extends AbstractQueryableResolver
{
    /** @var CredentialStoreInterface */
    protected $credentialStore;

    /**
     * @param CredentialStoreInterface $credentialStore
     */
    public function __construct(CredentialStoreInterface $credentialStore)
    {
        $this->credentialStore = $credentialStore;
    }

    /**
     * @param CriteriaSet                 $criteriaSet
     * @param array|CredentialInterface[] $arrCredentials
     *
     * @return array|CredentialInterface[]
     */
    public function resolve(CriteriaSet $criteriaSet, array $arrCredentials = array())
    {
        $result = array();
        foreach ($criteriaSet->get(EntityIdCriteria::class) as $criteria) {
            /* @var EntityIdCriteria $criteria */
            $result = array_merge($result, $this->credentialStore->getByEntityId($criteria->getEntityId()));
        }

        return $result;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Resolver\Endpoint;

use LightSaml\Criteria\CriteriaSet;
use LightSaml\Model\Metadata\EndpointReference;
use LightSaml\Resolver\Endpoint\Criteria\ServiceTypeCriteria;

/**
 * Filters out those endpoint candidates which are not an instance of the type
 * specified in the ServiceTypeCriteria. If criteria set does not have
 * ServiceTypeCriteria it will return all endpoint candidates.
 */
class ServiceTypeEndpointResolver implements EndpointResolverInterface
{
    /**
     * @param CriteriaSet         $criteriaSet
     * @param EndpointReference[] $candidates
     *
     * @return EndpointReference[]
     */
    public function resolve(CriteriaSet $criteriaSet, array $candidates)
    {
        if (false === $criteriaSet->has(ServiceTypeCriteria::class)) {
            return $candidates;
        }

        $result = array();
        /** @var ServiceTypeCriteria $serviceTypeCriteria */
        foreach ($criteriaSet->get(ServiceTypeCriteria::class) as $serviceTypeCriteria) {
            foreach ($candidates as $endpointReference) {
                $type = $serviceTypeCriteria->getServiceType();
                if ($endpointReference->getEndpoint() instanceof $type) {
                    $result[] = $endpointReference;
                }
            }
        }

        return $result;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Context\Profile\Helper;

use LightSaml\Context\Profile\MessageContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Protocol\AbstractRequest;
use LightSaml\Model\Protocol\AuthnRequest;
use LightSaml\Model\Protocol\LogoutRequest;
use LightSaml\Model\Protocol\LogoutResponse;
use LightSaml\Model\Protocol\Response;
use LightSaml\Model\Protocol\StatusResponse;

abstract class MessageContextHelper
{
    /**
     * @param MessageContext $context
     *
     * @return \LightSaml\Model\Protocol\SamlMessage
     */
    public static function asSamlMessage(MessageContext $context)
    {
        $message = $context->getMessage();
        if ($message) {
            return $message;
        }

        throw new LightSamlContextException($context, 'Missing SamlMessage');
    }

    /**
     * @param MessageContext $context
     *
     * @return \LightSaml\Model\Protocol\AuthnRequest
     */
    public static function asAuthnRequest(MessageContext $context)
    {
        $message = $context->getMessage();
        if ($message instanceof AuthnRequest) {
            return $message;
        }

        throw new LightSamlContextException($context, 'Expected AuthnRequest');
    }

    /**
     * @param MessageContext $context
     *
     * @return \LightSaml\Model\Protocol\AbstractRequest
     */
    public static function asAbstractRequest(MessageContext $context)
    {
        $message = $context->getMessage();
        if ($message instanceof AbstractRequest) {
            return $message;
        }

        throw new LightSamlContextException($context, 'Expected AbstractRequest');
    }

    /**
     * @param MessageContext $context
     *
     * @return \LightSaml\Model\Protocol\Response
     */
    public static function asResponse(MessageContext $context)
    {
        $message = $context->getMessage();
        if ($message instanceof Response) {
            return $message;
        }

        throw new LightSamlContextException($context, 'Expected Response');
    }

    /**
     * @param MessageContext $context
     *
     * @return \LightSaml\Model\Protocol\StatusResponse
     */
    public static function asStatusResponse(MessageContext $context)
    {
        $message = $context->getMessage();
        if ($message instanceof StatusResponse) {
            return $message;
        }

        throw new LightSamlContextException($context, 'Expected StatusResponse');
    }

    /**
     * @param MessageContext $context
     *
     * @return \LightSaml\Model\Protocol\LogoutRequest
     */
    public static function asLogoutRequest(MessageContext $context)
    {
        $message = $context->getMessage();
        if ($message instanceof LogoutRequest) {
            return $message;
        }

        throw new LightSamlContextException($context, 'Expected LogoutRequest');
    }

    /**
     * @param MessageContext $context
     *
     * @return \LightSaml\Model\Protocol\LogoutResponse
     */
    public static function asLogoutResponse(MessageContext $context)
    {
        $message = $context->getMessage();
        if ($message instanceof LogoutResponse) {
            return $message;
        }

        throw new LightSamlContextException($context, 'Expected LogoutResponse');
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Action\Profile;

use LightSaml\Context\ContextInterface;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContexts;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Context\Profile\RequestStateContext;
use LightSaml\Store\Request\RequestStateStoreInterface;
use Psr\Log\LoggerInterface;

class FlushRequestStatesAction extends AbstractProfileAction
{
    /** @var RequestStateStoreInterface */
    protected $requestStore;

    /**
     * @param LoggerInterface            $logger
     * @param RequestStateStoreInterface $requestStore
     */
    public function __construct(LoggerInterface $logger, RequestStateStoreInterface $requestStore)
    {
        parent::__construct($logger);

        $this->requestStore = $requestStore;
    }

    /**
     * @param ProfileContext $context
     *
     * @return void
     */
    protected function doExecute(ProfileContext $context)
    {
        $this->flush($context->getInboundContext()->getSubContext(ProfileContexts::REQUEST_STATE, null));
        foreach ($context as $child) {
            if ($child instanceof AssertionContext) {
                $this->flush($child->getSubContext(ProfileContexts::REQUEST_STATE, null));
            }
        }
    }

    /**
     * @param ContextInterface|null $requestStateContext
     */
    protected function flush($requestStateContext = null)
    {
        if ($requestStateContext instanceof RequestStateContext &&
            $requestStateContext->getRequestState() &&
            $requestStateContext->getRequestState()->getId()
        ) {
            $existed = $this->requestStore->remove($requestStateContext->getRequestState()->getId());

            if ($existed) {
                $this->logger->debug(
                    sprintf('Removed request state "%s"', $requestStateContext->getRequestState()->getId()),
                    LogHelper::getActionContext($requestStateContext, $this)
                );
            } else {
                $this->logger->warning(
                    sprintf('Request state "%s" does not exist', $requestStateContext->getRequestState()->getId()),
                    LogHelper::getActionContext($requestStateContext, $this)
                );
            }
        }
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Action\Profile\Inbound\Message;

use LightSaml\Action\Profile\AbstractProfileAction;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Context\Profile\ProfileContext;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Meta\TrustOptions\TrustOptions;
use LightSaml\Store\EntityDescriptor\EntityDescriptorStoreInterface;
use LightSaml\Store\TrustOptions\TrustOptionsStoreInterface;
use Psr\Log\LoggerInterface;

/**
 * Looks up inbound message Issuer in entity descriptor providers and sets it to the party context.
 */
class ResolvePartyEntityIdAction extends AbstractProfileAction
{
    /** @var EntityDescriptorStoreInterface */
    private $spEntityDescriptorProvider;

    /** @var EntityDescriptorStoreInterface */
    private $idpEntityDescriptorProvider;

    /** @var TrustOptionsStoreInterface */
    protected $trustOptionsProvider;

    /**
     * @param LoggerInterface                $logger
     * @param EntityDescriptorStoreInterface $spEntityDescriptorProvider
     * @param EntityDescriptorStoreInterface $idpEntityDescriptorProvider
     * @param TrustOptionsStoreInterface     $trustOptionsProvider
     */
    public function __construct(
        LoggerInterface $logger,
        EntityDescriptorStoreInterface $spEntityDescriptorProvider,
        EntityDescriptorStoreInterface $idpEntityDescriptorProvider,
        TrustOptionsStoreInterface $trustOptionsProvider
    ) {
        parent::__construct($logger);

        $this->spEntityDescriptorProvider = $spEntityDescriptorProvider;
        $this->idpEntityDescriptorProvider = $idpEntityDescriptorProvider;
        $this->trustOptionsProvider = $trustOptionsProvider;
    }

    /**
     * @param ProfileContext $context
     */
    protected function doExecute(ProfileContext $context)
    {
        $partyContext = $context->getPartyEntityContext();

        if ($partyContext->getEntityDescriptor() && $partyContext->getTrustOptions()) {
            $this->logger->debug(
                sprintf('Party EntityDescriptor and TrustOptions already set for "%s"', $partyContext->getEntityDescriptor()->getEntityID()),
                LogHelper::getActionContext($context, $this, array(
                    'partyEntityId' => $partyContext->getEntityDescriptor()->getEntityID(),
                ))
            );

            return;
        }

        $entityId = $partyContext->getEntityDescriptor() ? $partyContext->getEntityDescriptor()->getEntityID() : null;
        $entityId = $entityId ? $entityId : $partyContext->getEntityId();
        if (null == $entityId) {
            $message = 'EntityID is not set in the party context';
            $this->logger->critical($message, LogHelper::getActionErrorContext($context, $this));
            throw new LightSamlContextException($context, $message);
        }

        if (null == $partyContext->getEntityDescriptor()) {
            $partyEntityDescriptor = $this->getPartyEntityDescriptor(
                $context,
                ProfileContext::ROLE_IDP === $context->getOwnRole()
                ? $this->spEntityDescriptorProvider
                : $this->idpEntityDescriptorProvider,
                $context->getPartyEntityContext()->getEntityId()
            );
            $partyContext->setEntityDescriptor($partyEntityDescriptor);
            $this->logger->debug(
                sprintf('Known issuer resolved: "%s"', $partyEntityDescriptor->getEntityID()),
                LogHelper::getActionContext($context, $this, array(
                    'partyEntityId' => $partyEntityDescriptor->getEntityID(),
                ))
            );
        }

        if (null == $partyContext->getTrustOptions()) {
            $trustOptions = $this->trustOptionsProvider->get($partyContext->getEntityDescriptor()->getEntityID());
            if (null === $trustOptions) {
                $trustOptions = new TrustOptions();
            }
            $partyContext->setTrustOptions($trustOptions);
        }
    }

    /**
     * @param ProfileContext                 $context
     * @param EntityDescriptorStoreInterface $entityDescriptorProvider
     * @param string                         $entityId
     *
     * @return \LightSaml\Model\Metadata\EntityDescriptor
     */
    protected function getPartyEntityDescriptor(
        ProfileContext $context,
        EntityDescriptorStoreInterface $entityDescriptorProvider,
        $entityId
    ) {
        $partyEntityDescriptor = $entityDescriptorProvider->get($entityId);
        if (null === $partyEntityDescriptor) {
            $message = sprintf("Unknown issuer '%s'", $entityId);
            $this->logger->emergency($message, LogHelper::getActionErrorContext($context, $this));
            throw new LightSamlContextException($context, $message);
        }

        return $partyEntityDescriptor;
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\State\Sso;

use LightSaml\Error\LightSamlException;
use LightSaml\Meta\ParameterBag;

class SsoSessionState implements \Serializable
{
    /** @var string */
    protected $idpEntityId;

    /** @var string */
    protected $spEntityId;

    /** @var string */
    protected $nameId;

    /** @var string */
    protected $nameIdFormat;

    /** @var string */
    protected $sessionIndex;

    /** @var \DateTime */
    protected $sessionInstant;

    /** @var \DateTime */
    protected $firstAuthOn;

    /** @var \DateTime */
    protected $lastAuthOn;

    /** @var ParameterBag */
    protected $parameters;

    public function __construct()
    {
        $this->parameters = new ParameterBag();
    }

    /**
     * @return string
     */
    public function getIdpEntityId()
    {
        return $this->idpEntityId;
    }

    /**
     * @param string $idpEntityId
     *
     * @return SsoSessionState
     */
    public function setIdpEntityId($idpEntityId)
    {
        $this->idpEntityId = $idpEntityId;

        return $this;
    }

    /**
     * @return string
     */
    public function getSpEntityId()
    {
        return $this->spEntityId;
    }

    /**
     * @param string $spEntityId
     *
     * @return SsoSessionState
     */
    public function setSpEntityId($spEntityId)
    {
        $this->spEntityId = $spEntityId;

        return $this;
    }

    /**
     * @return string
     */
    public function getNameId()
    {
        return $this->nameId;
    }

    /**
     * @param string $nameId
     *
     * @return SsoSessionState
     */
    public function setNameId($nameId)
    {
        $this->nameId = $nameId;

        return $this;
    }

    /**
     * @return string
     */
    public function getNameIdFormat()
    {
        return $this->nameIdFormat;
    }

    /**
     * @param string $nameIdFormat
     *
     * @return SsoSessionState
     */
    public function setNameIdFormat($nameIdFormat)
    {
        $this->nameIdFormat = $nameIdFormat;

        return $this;
    }

    /**
     * @return string
     */
    public function getSessionIndex()
    {
        return $this->sessionIndex;
    }

    /**
     * @param string $sessionIndex
     *
     * @return SsoSessionState
     */
    public function setSessionIndex($sessionIndex)
    {
        $this->sessionIndex = $sessionIndex;

        return $this;
    }

    /**
     * @return \DateTime
     */
    public function getFirstAuthOn()
    {
        return $this->firstAuthOn;
    }

    /**
     * @param \DateTime $firstAuthOn
     *
     * @return SsoSessionState
     */
    public function setFirstAuthOn($firstAuthOn)
    {
        $this->firstAuthOn = $firstAuthOn;

        return $this;
    }

    /**
     * @return \DateTime
     */
    public function getLastAuthOn()
    {
        return $this->lastAuthOn;
    }

    /**
     * @param \DateTime $lastAuthOn
     *
     * @return SsoSessionState
     */
    public function setLastAuthOn($lastAuthOn)
    {
        $this->lastAuthOn = $lastAuthOn;

        return $this;
    }

    /**
     * @return \DateTime
     */
    public function getSessionInstant()
    {
        return $this->sessionInstant;
    }

    /**
     * @param \DateTime $sessionInstant
     *
     * @return SsoSessionState
     */
    public function setSessionInstant($sessionInstant)
    {
        $this->sessionInstant = $sessionInstant;

        return $this;
    }

    /**
     * @return ParameterBag
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * @deprecated Since 1.2, will be removed in 2.0. Use getParameters() instead
     *
     * @return array
     */
    public function getOptions()
    {
        return $this->parameters->all();
    }

    /**
     * @deprecated Since 1.2, will be removed in 2.0. Use getParameters() instead
     *
     * @param string $name
     * @param mixed  $value
     *
     * @return SsoSessionState
     */
    public function addOption($name, $value)
    {
        $this->parameters->set($name, $value);

        return $this;
    }

    /**
     * @deprecated Since 1.2, will be removed in 2.0. Use getParameters() instead
     *
     * @param string $name
     *
     * @return SsoSessionState
     */
    public function removeOption($name)
    {
        $this->parameters->remove($name);

        return $this;
    }

    /**
     * @deprecated Since 1.2, will be removed in 2.0. Use getParameters() instead
     *
     * @param string $name
     *
     * @return bool
     */
    public function hasOption($name)
    {
        return $this->parameters->has($name);
    }

    /**
     * @param string $partyId
     *
     * @return string Other party id
     *
     * @throws \LightSaml\Error\LightSamlException If $partyId does not match sp or idp entity id
     */
    public function getOtherPartyId($partyId)
    {
        if ($partyId == $this->idpEntityId) {
            return $this->spEntityId;
        } elseif ($partyId == $this->spEntityId) {
            return $this->idpEntityId;
        }

        throw new LightSamlException(sprintf(
            'Party "%s" is not included in sso session between "%s" and "%s"',
            $partyId,
            $this->idpEntityId,
            $this->spEntityId
        ));
    }

    /**
     * @return string the string representation of the object or null
     */
    public function serialize()
    {
        return serialize(array(
            $this->idpEntityId,
            $this->spEntityId,
            $this->nameId,
            $this->nameIdFormat,
            $this->sessionIndex,
            $this->sessionInstant,
            $this->firstAuthOn,
            $this->lastAuthOn,
            [],
            $this->parameters,
        ));
    }

    /**
     * @param string $serialized
     *
     * @return void
     */
    public function unserialize($serialized)
    {
        $data = unserialize($serialized);

        // add a few extra elements in the array to ensure that we have enough keys when unserializing
        // older data which does not include all properties.
        $data = array_merge($data, array_fill(0, 5, null));

        list(
            $this->idpEntityId,
            $this->spEntityId,
            $this->nameId,
            $this->nameIdFormat,
            $this->sessionIndex,
            $this->sessionInstant,
            $this->firstAuthOn,
            $this->lastAuthOn,
            $options,
            $this->parameters
        ) = $data;

        // if deserialized from old format, set old options to new parameters
        if ($options && 0 == $this->parameters->count()) {
            $this->parameters->replace($options);
        }
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Action\Assertion\Inbound;

use LightSaml\Action\Assertion\AbstractAssertionAction;
use LightSaml\Context\Profile\AssertionContext;
use LightSaml\Context\Profile\Helper\LogHelper;
use LightSaml\Criteria\CriteriaSet;
use LightSaml\Error\LightSamlContextException;
use LightSaml\Model\Assertion\SubjectConfirmation;
use LightSaml\Model\Metadata\AssertionConsumerService;
use LightSaml\Model\Metadata\SpSsoDescriptor;
use LightSaml\Resolver\Endpoint\Criteria\DescriptorTypeCriteria;
use LightSaml\Resolver\Endpoint\Criteria\LocationCriteria;
use LightSaml\Resolver\Endpoint\Criteria\ServiceTypeCriteria;
use LightSaml\Resolver\Endpoint\EndpointResolverInterface;
use Psr\Log\LoggerInterface;

class RecipientValidatorAction extends AbstractAssertionAction
{
    /** @var EndpointResolverInterface */
    private $endpointResolver;

    /**
     * @param LoggerInterface           $logger
     * @param EndpointResolverInterface $endpointResolver
     */
    public function __construct(LoggerInterface $logger, EndpointResolverInterface $endpointResolver)
    {
        parent::__construct($logger);

        $this->endpointResolver = $endpointResolver;
    }

    /**
     * @param AssertionContext $context
     *
     * @return void
     */
    protected function doExecute(AssertionContext $context)
    {
        if ($context->getAssertion()->getAllAuthnStatements() && $context->getAssertion()->hasBearerSubject()) {
            $this->validateBearerAssertion($context);
        }
    }

    /**
     * @param AssertionContext $context
     */
    protected function validateBearerAssertion(AssertionContext $context)
    {
        foreach ($context->getAssertion()->getSubject()->getBearerConfirmations() as $subjectConfirmation) {
            $this->validateSubjectConfirmation($context, $subjectConfirmation);
        }
    }

    /**
     * @param AssertionContext    $context
     * @param SubjectConfirmation $subjectConfirmation
     */
    protected function validateSubjectConfirmation(AssertionContext $context, SubjectConfirmation $subjectConfirmation)
    {
        $recipient = $subjectConfirmation->getSubjectConfirmationData()->getRecipient();
        if (null == $recipient) {
            $message = 'Bearer SubjectConfirmation must contain Recipient attribute';
            $this->logger->error($message, LogHelper::getActionErrorContext($context, $this));
            throw new LightSamlContextException($context, $message);
        }

        $criteriaSet = new CriteriaSet([
            new DescriptorTypeCriteria(SpSsoDescriptor::class),
            new ServiceTypeCriteria(AssertionConsumerService::class),
            new LocationCriteria($recipient),
        ]);
        $ownEntityDescriptor = $context->getProfileContext()->getOwnEntityDescriptor();
        $arrEndpoints = $this->endpointResolver->resolve($criteriaSet, $ownEntityDescriptor->getAllEndpoints());

        if (empty($arrEndpoints)) {
            $message = sprintf("Recipient '%s' does not match SP descriptor", $recipient);
            $this->logger->error($message, LogHelper::getActionErrorContext($context, $this, [
                'recipient' => $recipient,
            ]));
            throw new LightSamlContextException($context, $message);
        }
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Model\Metadata;

use LightSaml\Error\LightSamlXmlException;
use LightSaml\Model\Context\DeserializationContext;
use LightSaml\Model\Context\SerializationContext;
use LightSaml\Model\AbstractSamlModel;
use LightSaml\SamlConstants;

class Organization extends AbstractSamlModel
{
    /** @var string */
    protected $organizationName;

    /** @var string */
    protected $organizationDisplayName;

    /** @var string */
    protected $organizationURL;

    protected $lang = 'en-US';

    /**
     * @return string
     */
    public function getLang()
    {
        return $this->lang;
    }

    /**
     * @param string $lang
     *
     * @return Organization
     */
    public function setLang($lang)
    {
        $this->lang = $lang;

        return $this;
    }

    /**
     * @param string $organizationDisplayName
     *
     * @return Organization
     */
    public function setOrganizationDisplayName($organizationDisplayName)
    {
        $this->organizationDisplayName = (string) $organizationDisplayName;

        return $this;
    }

    /**
     * @return string
     */
    public function getOrganizationDisplayName()
    {
        return $this->organizationDisplayName;
    }

    /**
     * @param string $organizationName
     *
     * @return Organization
     */
    public function setOrganizationName($organizationName)
    {
        $this->organizationName = (string) $organizationName;

        return $this;
    }

    /**
     * @return string
     */
    public function getOrganizationName()
    {
        return $this->organizationName;
    }

    /**
     * @param string $organizationURL
     *
     * @return Organization
     */
    public function setOrganizationURL($organizationURL)
    {
        $this->organizationURL = (string) $organizationURL;

        return $this;
    }

    /**
     * @return string
     */
    public function getOrganizationURL()
    {
        return $this->organizationURL;
    }

    /**
     * @param \DOMNode             $parent
     * @param SerializationContext $context
     *
     * @return void
     */
    public function serialize(\DOMNode $parent, SerializationContext $context)
    {
        if (!$this->lang) {
            throw new LightSamlXmlException('Lang is required');
        }

        $result = $this->createElement('Organization', SamlConstants::NS_METADATA, $parent, $context);

        $elements = array('OrganizationName', 'OrganizationDisplayName', 'OrganizationURL');
        $this->singleElementsToXml(
            $elements,
            $result,
            $context,
            SamlConstants::NS_METADATA
        );

        /** @var \DOMNode $node */
        foreach ($result->childNodes as $node) {
            if ($node instanceof \DOMElement) {
                if (in_array($node->tagName, $elements)) {
                    $node->setAttribute('xml:lang', $this->lang);
                }
            }
        }
    }

    /**
     * @param \DOMNode               $node
     * @param DeserializationContext $context
     */
    public function deserialize(\DOMNode $node, DeserializationContext $context)
    {
        $this->checkXmlNodeName($node, 'Organization', SamlConstants::NS_METADATA);

        $this->singleElementsFromXml($node, $context, array(
            'OrganizationName' => array('md', null),
            'OrganizationDisplayName' => array('md', null),
            'OrganizationURL' => array('md', null),
        ));
    }
}


<?php

/*
 * This file is part of the LightSAML-Core package.
 *
 * (c) Milos Tomic <tmilos@lightsaml.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace LightSaml\Resolver\Session;

use LightSaml\Model\Assertion\Assertion;
use LightSaml\Provider\TimeProvider\TimeProviderInterface;
use LightSaml\State\Sso\SsoSessionState;
use LightSaml\State\Sso\SsoState;
use LightSaml\Store\Sso\SsoStateStoreInterface;

class SessionProcessor implements SessionProcessorInterface
{
    /** @var SsoStateStoreInterface */
    protected $ssoStateStore;

    /** @var TimeProviderInterface */
    protected $timeProvider;

    /**
     * @param SsoStateStoreInterface $ssoStateStore
     * @param TimeProviderInterface  $timeProvider
     */
    public function __construct(SsoStateStoreInterface $ssoStateStore, TimeProviderInterface $timeProvider)
    {
        $this->ssoStateStore = $ssoStateStore;
        $this->timeProvider = $timeProvider;
    }

    /**
     * @param Assertion[] $assertions
     * @param string      $ownEntityId
     * @param string      $partyEntityId
     */
    public function processAssertions(array $assertions, $ownEntityId, $partyEntityId)
    {
        $now = $this->timeProvider->getDateTime()->setTimezone(new \DateTimeZone('GMT'));
        $ssoState = $this->ssoStateStore->get();

        foreach ($assertions as $assertion) {
            if ($assertion instanceof Assertion) {
                if ($this->supportsSession($assertion)) {
                    $this->checkSession($ownEntityId, $partyEntityId, $ssoState, $assertion, $now);
                }
            } else {
                throw new \InvalidArgumentException('Expected Assertion');
            }
        }

        $this->ssoStateStore->set($ssoState);
    }

    /**
     * @param Assertion $assertion
     *
     * @return bool
     */
    protected function supportsSession(Assertion $assertion)
    {
        return
            $assertion->hasBearerSubject() &&
            null != $assertion->getSubject() &&
            null != $assertion->getSubject()->getNameID()
            ;
    }

    /**
     * @param string    $ownEntityId
     * @param string    $partyEntityId
     * @param SsoState  $ssoState
     * @param Assertion $assertion
     * @param \DateTime $now
     */
    protected function checkSession($ownEntityId, $partyEntityId, SsoState $ssoState, Assertion $assertion, \DateTime $now)
    {
        $sessions = $this->filterSessions($ssoState, $assertion, $ownEntityId, $partyEntityId);

        if (empty($sessions)) {
            $this->createSession($ssoState, $assertion, $now, $ownEntityId, $partyEntityId);
        } else {
            $this->updateLastAuthn($sessions, $now);
        }
    }

    /**
     * @param SsoState  $ssoState
     * @param Assertion $assertion
     * @param \DateTime $now
     * @param string    $ownEntityId
     * @param string    $partyEntityId
     *
     * @return SsoSessionState
     */
    protected function createSession(SsoState $ssoState, Assertion $assertion, \DateTime $now, $ownEntityId, $partyEntityId)
    {
        $ssoSession = new SsoSessionState();
        $ssoSession->setIdpEntityId($partyEntityId)
            ->setSpEntityId($ownEntityId)
            ->setNameId($assertion->getSubject()->getNameID()->getValue())
            ->setNameIdFormat($assertion->getSubject()->getNameID()->getFormat())
            ->setSessionIndex($assertion->getFirstAuthnStatement()->getSessionIndex())
            ->setSessionInstant($assertion->getFirstAuthnStatement()->getAuthnInstantDateTime())
            ->setFirstAuthOn($now)
            ->setLastAuthOn($now)
        ;
        $ssoState->addSsoSession($ssoSession);

        return $ssoSession;
    }

    /**
     * @param SsoSessionState[] $sessions
     * @param \DateTime         $now
     */
    protected function updateLastAuthn(array $sessions, \DateTime $now)
    {
        foreach ($sessions as $session) {
            $session->setLastAuthOn($now);
        }
    }

    /**
     * @param SsoState  $ssoState
     * @param Assertion $assertion
     * @param string    $ownEntityId
     * @param string    $partyEntityId
     *
     * @return \LightSaml\State\Sso\SsoSessionState[]
     */
    protected function filterSessions(SsoState $ssoState, Assertion $assertion, $ownEntityId, $partyEntityId)
    {
        return $ssoState->filter(
            $partyEntityId,
            $ownEntityId,
            $assertion->getSubject()->getNameID()->getValue(),
            $assertion->getSubject()->getNameID()->getFormat(),
            $assertion->getFirstAuthnStatement()->getSessionIndex()
        );
    }
}