1<?php
2/**
3 * Parameter input functions.
4 * This file contains functions for getting input from get/post variables.
5 */
6
7/**
8 * Get some input from variables passed submitted through GET or POST.
9 *
10 * If using any data obtained from get_input() in a web page, please be aware that
11 * it is a possible vector for a reflected XSS attack. If you are expecting an
12 * integer, cast it to an int. If it is a string, escape quotes.
13 *
14 * @param string $variable      The variable name we want.
15 * @param mixed  $default       A default value for the variable if it is not found.
16 * @param bool   $filter_result If true, then the result is filtered for bad tags.
17 *
18 * @return mixed
19 */
20function get_input($variable, $default = null, $filter_result = true) {
21	return _elgg_services()->request->getParam($variable, $default, $filter_result);
22}
23
24/**
25 * Sets an input value that may later be retrieved by get_input
26 *
27 * Note: this function does not handle nested arrays (ex: form input of param[m][n])
28 *
29 * @param string          $variable The name of the variable
30 * @param string|string[] $value    The value of the variable
31 *
32 * @return void
33 */
34function set_input($variable, $value) {
35	_elgg_services()->request->setParam($variable, $value, true);
36}
37
38/**
39 * Returns all values parsed from the current request, including $_GET and $_POST values,
40 * as well as any values set with set_input()
41 *
42 * @see get_input()
43 * @see set_input()
44 *
45 * @param bool $filter_result Sanitize input values
46 *
47 * @return array
48 *
49 * @since 3.0
50 */
51function elgg_get_request_data($filter_result = true) {
52	return _elgg_services()->request->getParams($filter_result);
53}
54
55/**
56 * Get an HTML-escaped title from input. E.g. "How to use &lt;b&gt; tags"
57 *
58 * @param string $variable The desired variable name
59 * @param string $default  The default if none given
60 *
61 * @return string
62 * @since 3.0
63 */
64function elgg_get_title_input($variable = 'title', $default = '') {
65	$raw_input = get_input($variable, $default, false);
66	return htmlspecialchars($raw_input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
67}
68
69/**
70 * Filter tags from a given string based on registered hooks.
71 *
72 * @param mixed $var Anything that does not include an object (strings, ints, arrays)
73 *					 This includes multi-dimensional arrays.
74 *
75 * @return mixed The filtered result - everything will be strings
76 */
77function filter_tags($var) {
78	return elgg_trigger_plugin_hook('validate', 'input', null, $var);
79}
80
81/**
82 * Returns the current page's complete URL.
83 *
84 * It uses the configured site URL for the hostname rather than depending on
85 * what the server uses to populate $_SERVER.
86 *
87 * @return string The current page URL.
88 */
89function current_page_url() {
90	return _elgg_services()->request->getCurrentURL();
91}
92
93/**
94 * Validates an email address.
95 *
96 * @param string $address Email address.
97 *
98 * @return bool
99 */
100function is_email_address($address) {
101	return elgg()->accounts->isValidEmail($address);
102}
103
104/**
105 * Save form submission data (all GET and POST vars) into a session cache
106 *
107 * Call this from an action when you want all your submitted variables
108 * available if the submission fails validation and is sent back to the form
109 *
110 * @param string $form_name Name of the sticky form
111 *
112 * @return void
113 * @since 1.8.0
114 */
115function elgg_make_sticky_form($form_name) {
116	_elgg_services()->stickyForms->makeStickyForm($form_name);
117}
118
119/**
120 * Remove form submission data from the session
121 *
122 * Call this if validation is successful in the action handler or
123 * when they sticky values have been used to repopulate the form
124 * after a validation error.
125 *
126 * @param string $form_name Form namespace
127 *
128 * @return void
129 * @since 1.8.0
130 */
131function elgg_clear_sticky_form($form_name) {
132	_elgg_services()->stickyForms->clearStickyForm($form_name);
133}
134
135/**
136 * Does form submission data exist for this form?
137 *
138 * @param string $form_name Form namespace
139 *
140 * @return boolean
141 * @since 1.8.0
142 */
143function elgg_is_sticky_form($form_name) {
144	return _elgg_services()->stickyForms->isStickyForm($form_name);
145}
146
147/**
148 * Get a specific value from cached form submission data
149 *
150 * @param string  $form_name     The name of the form
151 * @param string  $variable      The name of the variable
152 * @param mixed   $default       Default value if the variable does not exist in sticky cache
153 * @param boolean $filter_result Filter for bad input if true
154 *
155 * @return mixed
156 *
157 * @todo should this filter the default value?
158 * @since 1.8.0
159 */
160function elgg_get_sticky_value($form_name, $variable = '', $default = null, $filter_result = true) {
161	return _elgg_services()->stickyForms->getStickyValue($form_name, $variable, $default, $filter_result);
162}
163
164/**
165 * Get all submission data cached for a form
166 *
167 * @param string $form_name     The name of the form
168 * @param bool   $filter_result Filter for bad input if true
169 *
170 * @return array
171 * @since 1.8.0
172 */
173function elgg_get_sticky_values($form_name, $filter_result = true) {
174	return _elgg_services()->stickyForms->getStickyValues($form_name, $filter_result);
175}
176
177/**
178 * Remove one value of form submission data from the session
179 *
180 * @param string $form_name The name of the form
181 * @param string $variable  The name of the variable to clear
182 *
183 * @return void
184 * @since 1.8.0
185 */
186function elgg_clear_sticky_value($form_name, $variable) {
187	_elgg_services()->stickyForms->clearStickyValue($form_name, $variable);
188}
189
190/**
191 * Check if a value isn't empty, but allow 0 and '0'
192 *
193 * @param mixed $value the value to check
194 *
195 * @see empty()
196 * @see Elgg\Values::isEmpty()
197 *
198 * @return bool
199 * @since 3.0.0
200 */
201function elgg_is_empty($value) {
202	return Elgg\Values::isEmpty($value);
203}
204
205/**
206 * htmLawed filtering of data
207 *
208 * Called on the 'validate', 'input' plugin hook
209 *
210 * htmLawed's $config argument is filtered by the [config, htmlawed] hook.
211 * htmLawed's $spec argument is filtered by the [spec, htmlawed] hook.
212 *
213 * For information on these arguments, see
214 * http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/htmLawed_README.htm#s2.2
215 *
216 * @param \Elgg\Hook $hook 'validate', 'input'
217 *
218 * @return mixed
219 */
220function _elgg_htmlawed_filter_tags(\Elgg\Hook $hook) {
221	$var = $hook->getValue();
222
223	$config = [
224		// seems to handle about everything we need.
225		'safe' => true,
226
227		// remove comments/CDATA instead of converting to text
228		'comment' => 1,
229		'cdata' => 1,
230
231		// do not check for unique ids as the full input stack could be checked multiple times
232		// @see https://github.com/Elgg/Elgg/issues/12934
233		'unique_ids' => 0,
234
235		'elements' => '*-applet-button-form-input-textarea-iframe-script-style-embed-object',
236		'deny_attribute' => 'class, on*, formaction',
237		'hook_tag' => '_elgg_htmlawed_tag_post_processor',
238
239		'schemes' => '*:http,https,ftp,news,mailto,rtsp,teamspeak,gopher,mms,callto',
240		// apparent this doesn't work.
241		// 'style:color,cursor,text-align,font-size,font-weight,font-style,border,margin,padding,float'
242	];
243
244	// add nofollow to all links on output
245	if (!elgg_in_context('input')) {
246		$config['anti_link_spam'] = ['/./', ''];
247	}
248
249	$config = elgg_trigger_plugin_hook('config', 'htmlawed', null, $config);
250	$spec = elgg_trigger_plugin_hook('spec', 'htmlawed', null, '');
251
252	if (!is_array($var)) {
253		return Htmlawed::filter($var, $config, $spec);
254	} else {
255		$callback = function (&$v, $k, $config_spec) {
256			list ($config, $spec) = $config_spec;
257			$v = Htmlawed::filter($v, $config, $spec);
258		};
259
260		array_walk_recursive($var, $callback, [$config, $spec]);
261
262		return $var;
263	}
264}
265
266/**
267 * Post processor for tags in htmlawed
268 *
269 * This runs after htmlawed has filtered. It runs for each tag and filters out
270 * style attributes we don't want.
271 *
272 * This function triggers the 'allowed_styles', 'htmlawed' plugin hook.
273 *
274 * @param string $element    The tag element name
275 * @param array  $attributes An array of attributes
276 * @return string
277 */
278function _elgg_htmlawed_tag_post_processor($element, $attributes = false) {
279	if ($attributes === false) {
280		// This is a closing tag. Prevent further processing to avoid inserting a duplicate tag
281		return "</${element}>";
282	}
283
284	// this list should be coordinated with the WYSIWYG editor used (tinymce, ckeditor, etc.)
285	$allowed_styles = [
286		'color', 'cursor', 'text-align', 'vertical-align', 'font-size',
287		'font-weight', 'font-style', 'border', 'border-top', 'background-color',
288		'border-bottom', 'border-left', 'border-right',
289		'margin', 'margin-top', 'margin-bottom', 'margin-left',
290		'margin-right',	'padding', 'float', 'text-decoration'
291	];
292
293	$params = ['tag' => $element];
294	$allowed_styles = elgg_trigger_plugin_hook('allowed_styles', 'htmlawed', $params, $allowed_styles);
295
296	// must return something.
297	$string = '';
298
299	foreach ($attributes as $attr => $value) {
300		if ($attr == 'style') {
301			$styles = explode(';', $value);
302
303			$style_str = '';
304			foreach ($styles as $style) {
305				if (!trim($style)) {
306					continue;
307				}
308				list($style_attr, $style_value) = explode(':', trim($style));
309				$style_attr = trim($style_attr);
310				$style_value = trim($style_value);
311
312				if (in_array($style_attr, $allowed_styles)) {
313					$style_str .= "$style_attr: $style_value; ";
314				}
315			}
316
317			if ($style_str) {
318				$style_str = trim($style_str);
319				$string .= " style=\"$style_str\"";
320			}
321		} else {
322			$string .= " $attr=\"$value\"";
323		}
324	}
325
326	// Some WYSIWYG editors do not like tags like <p > so only add a space if needed.
327	if ($string = trim($string)) {
328		$string = " $string";
329	}
330
331	$r = "<$element$string>";
332	return $r;
333}
334
335/**
336 * Disable the autocomplete feature on password fields
337 *
338 * @param \Elgg\Hook $hook 'view_vars', 'input/password'
339 *
340 * @return void|array
341 */
342function _elgg_disable_password_autocomplete(\Elgg\Hook $hook) {
343
344	if (!_elgg_config()->security_disable_password_autocomplete) {
345		return;
346	}
347
348	$return_value = $hook->getValue();
349
350	$return_value['autocomplete'] = 'off';
351
352	return $return_value;
353}
354
355/**
356 * Initialize the input library
357 *
358 * @return void
359 * @internal
360 */
361function _elgg_input_init() {
362
363	elgg_register_plugin_hook_handler('validate', 'input', '_elgg_htmlawed_filter_tags', 1);
364
365	elgg_register_plugin_hook_handler('view_vars', 'input/password', '_elgg_disable_password_autocomplete');
366	elgg_register_plugin_hook_handler('view_vars', 'input/password', [_elgg_services()->passwordGenerator, 'addInputRequirements']);
367}
368
369/**
370 * @see \Elgg\Application::loadCore Do not do work here. Just register for events.
371 */
372return function(\Elgg\EventsService $events) {
373	$events->registerHandler('init', 'system', '_elgg_input_init');
374};
375