Test Failed
Branch master (4a3c5b)
by Greg
12:31
created

Filter::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2017 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees;
17
18
use Fisharebest\Webtrees\CommonMark\CensusTableExtension;
19
use Fisharebest\Webtrees\CommonMark\XrefExtension;
20
use Fisharebest\Webtrees\CommonMark\XrefParser;
21
use League\CommonMark\Block\Renderer\DocumentRenderer;
22
use League\CommonMark\Block\Renderer\ParagraphRenderer;
23
use League\CommonMark\Converter;
24
use League\CommonMark\DocParser;
25
use League\CommonMark\Environment;
26
use League\CommonMark\HtmlRenderer;
27
use League\CommonMark\Inline\Parser\AutolinkParser;
28
use League\CommonMark\Inline\Renderer\LinkRenderer;
29
use League\CommonMark\Inline\Renderer\TextRenderer;
30
use Webuni\CommonMark\TableExtension\TableExtension;
31
32
/**
33
 * Filter input and escape output.
34
 */
35
class Filter {
36
	// REGEX to match a URL
37
	// Some versions of RFC3987 have an appendix B which gives the following regex
38
	// (([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
39
	// This matches far too much while a “precise” regex is several pages long.
40
	// This is a compromise.
41
	const URL_REGEX = '((https?|ftp]):)(//([^\s/?#<>]*))?([^\s?#<>]*)(\?([^\s#<>]*))?(#[^\s?#<>]+)?';
42
43
	/**
44
	 * Format block-level text such as notes or transcripts, etc.
45
	 *
46
	 * @param string $text
47
	 * @param Tree   $WT_TREE
48
	 *
49
	 * @return string
50
	 */
51
	public static function formatText($text, Tree $WT_TREE) {
52
		switch ($WT_TREE->getPreference('FORMAT_TEXT')) {
53
		case 'markdown':
54
			return '<div class="markdown" dir="auto">' . self::markdown($text, $WT_TREE) . '</div>';
55
		default:
56
			return '<div class="markdown" style="white-space: pre-wrap;" dir="auto">' . self::expandUrls($text, $WT_TREE) . '</div>';
57
		}
58
	}
59
60
	/**
61
	 * Format a block of text, expanding URLs and XREFs.
62
	 *
63
	 * @param string $text
64
	 * @param        Tree   tree
65
	 *
66
	 * @return string
67
	 */
68
	public static function expandUrls($text, Tree $tree) {
69
		// If it looks like a URL, turn it into a markdown autolink.
70
		$text = preg_replace('/' . addcslashes(self::URL_REGEX, '/') . '/', '<$0>', $text);
71
72
		// Create a minimal commonmark processor - just add support for autolinks.
73
		$environment = new Environment;
74
		$environment->mergeConfig([
75
			'renderer' => [
76
				'block_separator' => "\n",
77
				'inner_separator' => "\n",
78
				'soft_break'      => "\n",
79
			],
80
			'html_input'         => Environment::HTML_INPUT_ESCAPE,
81
			'allow_unsafe_links' => true,
82
		]);
83
84
		$environment
85
			->addBlockRenderer('League\CommonMark\Block\Element\Document', new DocumentRenderer)
86
			->addBlockRenderer('League\CommonMark\Block\Element\Paragraph', new ParagraphRenderer)
87
			->addInlineRenderer('League\CommonMark\Inline\Element\Text', new TextRenderer)
88
			->addInlineRenderer('League\CommonMark\Inline\Element\Link', new LinkRenderer)
89
			->addInlineParser(new AutolinkParser);
90
91
		$environment->addExtension(new CensusTableExtension);
92
		$environment->addExtension(new XrefExtension($tree));
93
94
		$converter = new Converter(new DocParser($environment), new HtmlRenderer($environment));
95
96
		return $converter->convertToHtml($text);
97
	}
98
99
	/**
100
	 * Format a block of text, using "Markdown".
101
	 *
102
	 * @param string $text
103
	 * @param Tree   $tree
104
	 *
105
	 * @return string
106
	 */
107
	public static function markdown($text, Tree $tree) {
108
		$environment = Environment::createCommonMarkEnvironment();
109
		$environment->mergeConfig(['html_input' => 'escape']);
110
		$environment->addExtension(new TableExtension);
111
		$environment->addExtension(new CensusTableExtension);
112
		$environment->addExtension(new XrefExtension($tree));
113
114
		$converter = new Converter(new DocParser($environment), new HtmlRenderer($environment));
115
116
		return $converter->convertToHtml($text);
117
	}
118
119
	/**
120
	 * Validate INPUT parameters
121
	 *
122
	 * @param string      $source
123
	 * @param string      $variable
124
	 * @param string|null $regexp
125
	 * @param string      $default
126
	 *
127
	 * @return string
128
	 */
129
	private static function input($source, $variable, $regexp = null, $default = '') {
130
		if ($regexp) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $regexp of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
131
			return filter_input($source, $variable, FILTER_VALIDATE_REGEXP, [
0 ignored issues
show
Bug introduced by
$source of type string is incompatible with the type integer expected by parameter $type of filter_input(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

131
			return filter_input(/** @scrutinizer ignore-type */ $source, $variable, FILTER_VALIDATE_REGEXP, [
Loading history...
132
				'options' => [
133
					'regexp'  => '/^(' . $regexp . ')$/u',
134
					'default' => $default,
135
				],
136
			]);
137
		} else {
138
			$tmp = filter_input($source, $variable, FILTER_CALLBACK, [
139
				'options' => function ($x) {
140
					return mb_check_encoding($x, 'UTF-8') ? $x : false;
141
				},
142
			]);
143
144
			return ($tmp === null || $tmp === false) ? $default : $tmp;
145
		}
146
	}
147
148
	/**
149
	 * Validate array INPUT parameters
150
	 *
151
	 * @param string      $source
152
	 * @param string      $variable
153
	 * @param string|null $regexp
154
	 * @param string      $default
155
	 *
156
	 * @return string[]
157
	 */
158
	private static function inputArray($source, $variable, $regexp = null, $default = '') {
159
		if ($regexp) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $regexp of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
160
			return filter_input_array($source, [
0 ignored issues
show
Bug introduced by
$source of type string is incompatible with the type integer expected by parameter $type of filter_input_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
			return filter_input_array(/** @scrutinizer ignore-type */ $source, [
Loading history...
161
				$variable => [
162
					'flags'   => FILTER_REQUIRE_ARRAY,
163
					'filter'  => FILTER_VALIDATE_REGEXP,
164
					'options' => [
165
						'regexp'  => '/^(' . $regexp . ')$/u',
166
						'default' => $default,
167
					],
168
				],
169
			])[$variable] ?: [];
170
		} else {
171
			return filter_input_array($source, [
172
				$variable => [
173
					'flags'   => FILTER_REQUIRE_ARRAY,
174
					'filter'  => FILTER_CALLBACK,
175
					'options' => function ($x) {
176
						return mb_check_encoding($x, 'UTF-8') ? $x : false;
177
					},
178
				],
179
			])[$variable] ?: [];
180
		}
181
	}
182
183
	/**
184
	 * Validate GET parameters
185
	 *
186
	 * @param string      $variable
187
	 * @param string|null $regexp
188
	 * @param string      $default
189
	 *
190
	 * @return string
191
	 */
192
	public static function get($variable, $regexp = null, $default = '') {
193
		return self::input(INPUT_GET, $variable, $regexp, $default);
194
	}
195
196
	/**
197
	 * Validate array GET parameters
198
	 *
199
	 * @param string      $variable
200
	 * @param string|null $regexp
201
	 * @param string      $default
202
	 *
203
	 * @return string[]
204
	 */
205
	public static function getArray($variable, $regexp = null, $default = '') {
206
		return self::inputArray(INPUT_GET, $variable, $regexp, $default);
207
	}
208
209
	/**
210
	 * Validate boolean GET parameters
211
	 *
212
	 * @param string $variable
213
	 *
214
	 * @return bool
215
	 */
216
	public static function getBool($variable) {
217
		return (bool) filter_input(INPUT_GET, $variable, FILTER_VALIDATE_BOOLEAN);
218
	}
219
220
	/**
221
	 * Validate integer GET parameters
222
	 *
223
	 * @param string $variable
224
	 * @param int    $min
225
	 * @param int    $max
226
	 * @param int    $default
227
	 *
228
	 * @return int
229
	 */
230 View Code Duplication
	public static function getInteger($variable, $min = 0, $max = PHP_INT_MAX, $default = 0) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
		return filter_input(INPUT_GET, $variable, FILTER_VALIDATE_INT, [
232
			'options' => [
233
				'min_range' => $min,
234
				'max_range' => $max,
235
				'default'   => $default,
236
			],
237
		]);
238
	}
239
240
	/**
241
	 * Validate URL GET parameters
242
	 *
243
	 * @param string $variable
244
	 * @param string $default
245
	 *
246
	 * @return string
247
	 */
248
	public static function getUrl($variable, $default = '') {
249
		return filter_input(INPUT_GET, $variable, FILTER_VALIDATE_URL) ?: $default;
250
	}
251
252
	/**
253
	 * Validate POST parameters
254
	 *
255
	 * @param string      $variable
256
	 * @param string|null $regexp
257
	 * @param string      $default
258
	 *
259
	 * @return string
260
	 */
261
	public static function post($variable, $regexp = null, $default = '') {
262
		return self::input(INPUT_POST, $variable, $regexp, $default);
263
	}
264
265
	/**
266
	 * Validate array POST parameters
267
	 *
268
	 * @param string      $variable
269
	 * @param string|null $regexp
270
	 * @param string      $default
271
	 *
272
	 * @return string[]|string[][]
273
	 */
274
	public static function postArray($variable, $regexp = null, $default = '') {
275
		return self::inputArray(INPUT_POST, $variable, $regexp, $default);
276
	}
277
278
	/**
279
	 * Validate boolean POST parameters
280
	 *
281
	 * @param string $variable
282
	 *
283
	 * @return bool
284
	 */
285
	public static function postBool($variable) {
286
		return (bool) filter_input(INPUT_POST, $variable, FILTER_VALIDATE_BOOLEAN);
287
	}
288
289
	/**
290
	 * Validate integer POST parameters
291
	 *
292
	 * @param string $variable
293
	 * @param int    $min
294
	 * @param int    $max
295
	 * @param int    $default
296
	 *
297
	 * @return int
298
	 */
299 View Code Duplication
	public static function postInteger($variable, $min = 0, $max = PHP_INT_MAX, $default = 0) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
300
		return filter_input(INPUT_POST, $variable, FILTER_VALIDATE_INT, [
301
			'options' => [
302
				'min_range' => $min,
303
				'max_range' => $max,
304
				'default'   => $default,
305
			],
306
		]);
307
	}
308
309
	/**
310
	 * Validate URL GET parameters
311
	 *
312
	 * @param string $variable
313
	 * @param string $default
314
	 *
315
	 * @return string
316
	 */
317
	public static function postUrl($variable, $default = '') {
318
		return filter_input(INPUT_POST, $variable, FILTER_VALIDATE_URL) ?: $default;
319
	}
320
321
	/**
322
	 * Validate COOKIE parameters
323
	 *
324
	 * @param string      $variable
325
	 * @param string|null $regexp
326
	 * @param string      $default
327
	 *
328
	 * @return string
329
	 */
330
	public static function cookie($variable, $regexp = null, $default = '') {
331
		return self::input(INPUT_COOKIE, $variable, $regexp, $default);
332
	}
333
334
	/**
335
	 * Validate SERVER parameters
336
	 *
337
	 * @param string      $variable
338
	 * @param string|null $regexp
339
	 * @param string      $default
340
	 *
341
	 * @return string
342
	 */
343
	public static function server($variable, $regexp = null, $default = '') {
344
		// On some servers, variables that are present in $_SERVER cannot be
345
		// found via filter_input(INPUT_SERVER). Instead, they are found via
346
		// filter_input(INPUT_ENV). Since we cannot rely on filter_input(),
347
		// we must use the superglobal directly.
348
		if (array_key_exists($variable, $_SERVER) && ($regexp === null || preg_match('/^(' . $regexp . ')$/', $_SERVER[$variable]))) {
349
			return $_SERVER[$variable];
350
		} else {
351
			return $default;
352
		}
353
	}
354
355
	/**
356
	 * Cross-Site Request Forgery tokens - ensure that the user is submitting
357
	 * a form that was generated by the current session.
358
	 *
359
	 * @return string
360
	 */
361
	public static function getCsrfToken() {
362
		if (!Session::has('CSRF_TOKEN')) {
363
			$charset    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789';
364
			$csrf_token = '';
365
			for ($n = 0; $n < 32; ++$n) {
366
				$csrf_token .= substr($charset, mt_rand(0, 61), 1);
367
			}
368
			Session::put('CSRF_TOKEN', $csrf_token);
369
		}
370
371
		return Session::get('CSRF_TOKEN');
372
	}
373
374
	/**
375
	 * Generate an <input> element - to protect the current form from CSRF attacks.
376
	 *
377
	 * @return string
378
	 */
379
	public static function getCsrf() {
380
		return '<input type="hidden" name="csrf" value="' . self::getCsrfToken() . '">';
381
	}
382
383
	/**
384
	 * Check that the POST request contains the CSRF token generated above.
385
	 *
386
	 * @return bool
387
	 */
388
	public static function checkCsrf() {
389
		if (isset($_SERVER['HTTP_X_CSRF_TOKEN']) && $_SERVER['HTTP_X_CSRF_TOKEN'] !== self::getCsrfToken()) {
390
			// Oops. Something is not quite right
391
			Log::addAuthenticationLog('CSRF mismatch - session expired or malicious attack');
392
			FlashMessages::addMessage(I18N::translate('This form has expired. Try again.'), 'error');
393
394
			return false;
395
		}
396
397
		return true;
398
	}
399
}
400