. */ namespace Fisharebest\Webtrees; use Fisharebest\Webtrees\CommonMark\CensusTableExtension; use Fisharebest\Webtrees\CommonMark\XrefExtension; use Fisharebest\Webtrees\CommonMark\XrefParser; use League\CommonMark\Block\Renderer\DocumentRenderer; use League\CommonMark\Block\Renderer\ParagraphRenderer; use League\CommonMark\Converter; use League\CommonMark\DocParser; use League\CommonMark\Environment; use League\CommonMark\HtmlRenderer; use League\CommonMark\Inline\Parser\AutolinkParser; use League\CommonMark\Inline\Renderer\LinkRenderer; use League\CommonMark\Inline\Renderer\TextRenderer; use Webuni\CommonMark\TableExtension\TableExtension; /** * Filter input and escape output. */ class Filter { // REGEX to match a URL // Some versions of RFC3987 have an appendix B which gives the following regex // (([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? // This matches far too much while a “precise” regex is several pages long. // This is a compromise. const URL_REGEX = '((https?|ftp]):)(//([^\s/?#<>]*))?([^\s?#<>]*)(\?([^\s#<>]*))?(#[^\s?#<>]+)?'; /** * Format block-level text such as notes or transcripts, etc. * * @param string $text * @param Tree $WT_TREE * * @return string */ public static function formatText($text, Tree $WT_TREE) { switch ($WT_TREE->getPreference('FORMAT_TEXT')) { case 'markdown': return '
' . self::markdown($text, $WT_TREE) . '
'; default: return '
' . self::expandUrls($text, $WT_TREE) . '
'; } } /** * Format a block of text, expanding URLs and XREFs. * * @param string $text * @param Tree tree * * @return string */ public static function expandUrls($text, Tree $tree) { // If it looks like a URL, turn it into a markdown autolink. $text = preg_replace('/' . addcslashes(self::URL_REGEX, '/') . '/', '<$0>', $text); // Create a minimal commonmark processor - just add support for autolinks. $environment = new Environment; $environment->mergeConfig([ 'renderer' => [ 'block_separator' => "\n", 'inner_separator' => "\n", 'soft_break' => "\n", ], 'html_input' => Environment::HTML_INPUT_ESCAPE, 'allow_unsafe_links' => true, ]); $environment ->addBlockRenderer('League\CommonMark\Block\Element\Document', new DocumentRenderer) ->addBlockRenderer('League\CommonMark\Block\Element\Paragraph', new ParagraphRenderer) ->addInlineRenderer('League\CommonMark\Inline\Element\Text', new TextRenderer) ->addInlineRenderer('League\CommonMark\Inline\Element\Link', new LinkRenderer) ->addInlineParser(new AutolinkParser); $environment->addExtension(new CensusTableExtension); $environment->addExtension(new XrefExtension($tree)); $converter = new Converter(new DocParser($environment), new HtmlRenderer($environment)); return $converter->convertToHtml($text); } /** * Format a block of text, using "Markdown". * * @param string $text * @param Tree $tree * * @return string */ public static function markdown($text, Tree $tree) { $environment = Environment::createCommonMarkEnvironment(); $environment->mergeConfig(['html_input' => 'escape']); $environment->addExtension(new TableExtension); $environment->addExtension(new CensusTableExtension); $environment->addExtension(new XrefExtension($tree)); $converter = new Converter(new DocParser($environment), new HtmlRenderer($environment)); return $converter->convertToHtml($text); } /** * Validate INPUT parameters * * @param string $source * @param string $variable * @param string|null $regexp * @param string $default * * @return string */ private static function input($source, $variable, $regexp = null, $default = '') { if ($regexp) { return filter_input($source, $variable, FILTER_VALIDATE_REGEXP, [ 'options' => [ 'regexp' => '/^(' . $regexp . ')$/u', 'default' => $default, ], ]); } else { $tmp = filter_input($source, $variable, FILTER_CALLBACK, [ 'options' => function ($x) { return mb_check_encoding($x, 'UTF-8') ? $x : false; }, ]); return ($tmp === null || $tmp === false) ? $default : $tmp; } } /** * Validate array INPUT parameters * * @param string $source * @param string $variable * @param string|null $regexp * @param string $default * * @return string[] */ private static function inputArray($source, $variable, $regexp = null, $default = '') { if ($regexp) { return filter_input_array($source, [ $variable => [ 'flags' => FILTER_REQUIRE_ARRAY, 'filter' => FILTER_VALIDATE_REGEXP, 'options' => [ 'regexp' => '/^(' . $regexp . ')$/u', 'default' => $default, ], ], ])[$variable] ?: []; } else { return filter_input_array($source, [ $variable => [ 'flags' => FILTER_REQUIRE_ARRAY, 'filter' => FILTER_CALLBACK, 'options' => function ($x) { return mb_check_encoding($x, 'UTF-8') ? $x : false; }, ], ])[$variable] ?: []; } } /** * Validate GET parameters * * @param string $variable * @param string|null $regexp * @param string $default * * @return string */ public static function get($variable, $regexp = null, $default = '') { return self::input(INPUT_GET, $variable, $regexp, $default); } /** * Validate array GET parameters * * @param string $variable * @param string|null $regexp * @param string $default * * @return string[] */ public static function getArray($variable, $regexp = null, $default = '') { return self::inputArray(INPUT_GET, $variable, $regexp, $default); } /** * Validate boolean GET parameters * * @param string $variable * * @return bool */ public static function getBool($variable) { return (bool) filter_input(INPUT_GET, $variable, FILTER_VALIDATE_BOOLEAN); } /** * Validate integer GET parameters * * @param string $variable * @param int $min * @param int $max * @param int $default * * @return int */ public static function getInteger($variable, $min = 0, $max = PHP_INT_MAX, $default = 0) { return filter_input(INPUT_GET, $variable, FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => $min, 'max_range' => $max, 'default' => $default, ], ]); } /** * Validate URL GET parameters * * @param string $variable * @param string $default * * @return string */ public static function getUrl($variable, $default = '') { return filter_input(INPUT_GET, $variable, FILTER_VALIDATE_URL) ?: $default; } /** * Validate POST parameters * * @param string $variable * @param string|null $regexp * @param string $default * * @return string */ public static function post($variable, $regexp = null, $default = '') { return self::input(INPUT_POST, $variable, $regexp, $default); } /** * Validate array POST parameters * * @param string $variable * @param string|null $regexp * @param string $default * * @return string[]|string[][] */ public static function postArray($variable, $regexp = null, $default = '') { return self::inputArray(INPUT_POST, $variable, $regexp, $default); } /** * Validate boolean POST parameters * * @param string $variable * * @return bool */ public static function postBool($variable) { return (bool) filter_input(INPUT_POST, $variable, FILTER_VALIDATE_BOOLEAN); } /** * Validate integer POST parameters * * @param string $variable * @param int $min * @param int $max * @param int $default * * @return int */ public static function postInteger($variable, $min = 0, $max = PHP_INT_MAX, $default = 0) { return filter_input(INPUT_POST, $variable, FILTER_VALIDATE_INT, [ 'options' => [ 'min_range' => $min, 'max_range' => $max, 'default' => $default, ], ]); } /** * Validate URL GET parameters * * @param string $variable * @param string $default * * @return string */ public static function postUrl($variable, $default = '') { return filter_input(INPUT_POST, $variable, FILTER_VALIDATE_URL) ?: $default; } /** * Validate COOKIE parameters * * @param string $variable * @param string|null $regexp * @param string $default * * @return string */ public static function cookie($variable, $regexp = null, $default = '') { return self::input(INPUT_COOKIE, $variable, $regexp, $default); } /** * Validate SERVER parameters * * @param string $variable * @param string|null $regexp * @param string $default * * @return string */ public static function server($variable, $regexp = null, $default = '') { // On some servers, variables that are present in $_SERVER cannot be // found via filter_input(INPUT_SERVER). Instead, they are found via // filter_input(INPUT_ENV). Since we cannot rely on filter_input(), // we must use the superglobal directly. if (array_key_exists($variable, $_SERVER) && ($regexp === null || preg_match('/^(' . $regexp . ')$/', $_SERVER[$variable]))) { return $_SERVER[$variable]; } else { return $default; } } /** * Cross-Site Request Forgery tokens - ensure that the user is submitting * a form that was generated by the current session. * * @return string */ public static function getCsrfToken() { if (!Session::has('CSRF_TOKEN')) { $charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789'; $csrf_token = ''; for ($n = 0; $n < 32; ++$n) { $csrf_token .= substr($charset, mt_rand(0, 61), 1); } Session::put('CSRF_TOKEN', $csrf_token); } return Session::get('CSRF_TOKEN'); } /** * Generate an element - to protect the current form from CSRF attacks. * * @return string */ public static function getCsrf() { return ''; } /** * Check that the POST request contains the CSRF token generated above. * * @return bool */ public static function checkCsrf() { if (isset($_SERVER['HTTP_X_CSRF_TOKEN']) && $_SERVER['HTTP_X_CSRF_TOKEN'] !== self::getCsrfToken()) { // Oops. Something is not quite right Log::addAuthenticationLog('CSRF mismatch - session expired or malicious attack'); FlashMessages::addMessage(I18N::translate('This form has expired. Try again.'), 'error'); return false; } return true; } }