.
*/
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;
}
}