. */ namespace Fisharebest\Webtrees; use HTMLPurifier; use HTMLPurifier_Config; use Michelf\MarkdownExtra; /** * 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?#<>]+)?'; /** * Escape a string for use in HTML * * @param string $string * * @return string */ public static function escapeHtml($string) { if (defined('ENT_SUBSTITUTE')) { // PHP5.4 allows us to substitute invalid UTF8 sequences return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } else { return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); } } /** * Escape a string for use in a URL * * @param string $string * * @return string */ public static function escapeUrl($string) { return rawurlencode($string); } /** * Escape a string for use in Javascript * * @param string $string * * @return string */ public static function escapeJs($string) { return preg_replace_callback('/[^A-Za-z0-9,. _]/Su', function ($x) { if (strlen($x[0]) === 1) { return sprintf('\\u%04s', strtoupper(bin2hex(mb_convert_encoding($x[0], 'UTF-16BE', 'UTF-8')))); } else { return $x[0]; } }, $string); } /** * Escape a string for use in a SQL "LIKE" clause * * @param string $string * * @return string */ public static function escapeLike($string) { return strtr( $string, [ '\\' => '\\\\', '%' => '\%', '_' => '\_', ] ); } /** * Unescape an HTML string, giving just the literal text * * @param string $string * * @return string */ public static function unescapeHtml($string) { return html_entity_decode(strip_tags($string), ENT_QUOTES, 'UTF-8'); } /** * 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) . '
'; default: return '
' . self::expandUrls($text) . '
'; } } /** * Escape a string for use in HTML, and additionally convert URLs to links. * * @param string $text * * @return string */ public static function expandUrls($text) { return preg_replace_callback( '/' . addcslashes('(?!>)' . self::URL_REGEX . '(?!)', '/') . '/i', function ($m) { return '' . $m[0] . ''; }, self::escapeHtml($text) ); } /** * Format a block of text, using "Markdown". * * @param string $text * * @return string */ public static function markdown($text) { $parser = new MarkdownExtra; $parser->empty_element_suffix = '>'; $parser->no_markup = true; $text = $parser->transform($text); // HTMLPurifier needs somewhere to write temporary files $HTML_PURIFIER_CACHE_DIR = WT_DATA_DIR . 'html_purifier_cache'; if (!is_dir($HTML_PURIFIER_CACHE_DIR)) { mkdir($HTML_PURIFIER_CACHE_DIR); } $config = HTMLPurifier_Config::createDefault(); $config->set('Cache.SerializerPath', $HTML_PURIFIER_CACHE_DIR); $purifier = new HTMLPurifier($config); $text = $purifier->purify($text); return $text; } /** * Validate INPUT parameters * * @param string $source * @param string $variable * @param string|null $regexp * @param string|null $default * * @return string|null */ private static function input($source, $variable, $regexp = null, $default = null) { 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|null $default * * @return string[] */ private static function inputArray($source, $variable, $regexp = null, $default = null) { 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|null $default * * @return null|string */ public static function get($variable, $regexp = null, $default = null) { return self::input(INPUT_GET, $variable, $regexp, $default); } /** * Validate array GET parameters * * @param string $variable * @param string|null $regexp * @param string|null $default * * @return string[] */ public static function getArray($variable, $regexp = null, $default = null) { 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 email GET parameters * * @param string $variable * @param string|null $default * * @return null|string */ public static function getEmail($variable, $default = null) { return filter_input(INPUT_GET, $variable, FILTER_VALIDATE_EMAIL) ?: $default; } /** * Validate URL GET parameters * * @param string $variable * @param string|null $default * * @return null|string */ public static function getUrl($variable, $default = null) { return filter_input(INPUT_GET, $variable, FILTER_VALIDATE_URL) ?: $default; } /** * Validate POST parameters * * @param string $variable * @param string|null $regexp * @param string|null $default * * @return null|string */ public static function post($variable, $regexp = null, $default = null) { return self::input(INPUT_POST, $variable, $regexp, $default); } /** * Validate array POST parameters * * @param string $variable * @param string|null $regexp * @param string|null $default * * @return string[] */ public static function postArray($variable, $regexp = null, $default = null) { 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 email POST parameters * * @param string $variable * @param string|null $default * * @return null|string */ public static function postEmail($variable, $default = null) { return filter_input(INPUT_POST, $variable, FILTER_VALIDATE_EMAIL) ?: $default; } /** * Validate URL GET parameters * * @param string $variable * @param string|null $default * * @return null|string */ public static function postUrl($variable, $default = null) { return filter_input(INPUT_POST, $variable, FILTER_VALIDATE_URL) ?: $default; } /** * Validate COOKIE parameters * * @param string $variable * @param string|null $regexp * @param string|null $default * * @return null|string */ public static function cookie($variable, $regexp = null, $default = null) { return self::input(INPUT_COOKIE, $variable, $regexp, $default); } /** * Validate SERVER parameters * * @param string $variable * @param string|null $regexp * @param string|null $default * * @return null|string */ public static function server($variable, $regexp = null, $default = null) { // 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 (self::post('csrf') !== 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; } }