Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/lib/output.php (2 issues)

1
<?php
2
/**
3
 * Output functions
4
 * Processing text for output such as pulling out URLs and extracting excerpts
5
 *
6
 * @package Elgg
7
 * @subpackage Core
8
 */
9
10
/**
11
 * Takes a string and turns any URLs into formatted links
12
 *
13
 * @param string $text The input string
14
 *
15
 * @return string The output string with formatted links
16
 */
17
function parse_urls($text) {
18
19 1
	$linkify = new \Misd\Linkify\Linkify();
20
		
21 1
	return $linkify->processUrls($text, ['attr' => ['rel' => 'nofollow']]);
22
}
23
24
/**
25
 * Takes a string and turns any email addresses into formatted links
26
 *
27
 * @param string $text The input string
28
 *
29
 * @return string The output string with formatted links
30
 *
31
 * @since 2.3
32
 */
33
function elgg_parse_emails($text) {
34 1
	$linkify = new \Misd\Linkify\Linkify();
35
		
36 1
	return $linkify->processEmails($text, ['attr' => ['rel' => 'nofollow']]);
37
}
38
39
/**
40
 * Create paragraphs from text with line spacing
41
 *
42
 * @param string $string The string
43
 *
44
 * @return string
45
 **/
46
function elgg_autop($string) {
47
	try {
48 6
		return _elgg_services()->autoP->process($string);
0 ignored issues
show
Bug Best Practice introduced by
The expression return _elgg_services()->autoP->process($string) could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
49
	} catch (\RuntimeException $e) {
50
		_elgg_services()->logger->warn('ElggAutoP failed to process the string: ' . $e->getMessage());
51
		return $string;
52
	}
53
}
54
55
/**
56
 * Returns an excerpt.
57
 * Will return up to n chars stopping at the nearest space.
58
 * If no spaces are found (like in Japanese) will crop off at the
59
 * n char mark. Adds ... if any text was chopped.
60
 *
61
 * @param string $text      The full text to excerpt
62
 * @param int    $num_chars Return a string up to $num_chars long
63
 *
64
 * @return string
65
 * @since 1.7.2
66
 */
67
function elgg_get_excerpt($text, $num_chars = 250) {
68 12
	$view = 'output/excerpt';
69
	$vars = [
70 12
		'text' => $text,
71 12
		'num_chars' => $num_chars,
72
	];
73 12
	$viewtype = elgg_view_exists($view) ? '' : 'default';
74
75 12
	return _elgg_view_under_viewtype($view, $vars, $viewtype);
76
}
77
78
/**
79
 * Format bytes to a human readable format
80
 *
81
 * @param int $size      File size in bytes to format
82
 *
83
 * @param int $precision Precision to round formatting bytes to
84
 *
85
 * @return string
86
 * @since 1.9.0
87
 */
88
function elgg_format_bytes($size, $precision = 2) {
89
	if (!$size || $size < 0) {
90
		return false;
91
	}
92
93
	$base = log($size) / log(1024);
94
	$suffixes = ['B', 'kB', 'MB', 'GB', 'TB'];
95
96
	return round(pow(1024, $base - floor($base)), $precision) . ' ' . $suffixes[floor($base)];
97
}
98
99
/**
100
 * Converts an associative array into a string of well-formed HTML/XML attributes
101
 * Returns a concatenated string of HTML attributes to be inserted into a tag (e.g., <tag $attrs>)
102
 *
103
 * @param array $attrs Attributes
104
 *                     An array of attribute => value pairs
105
 *                     Attribute value can be a scalar value, an array of scalar values, or true
106
 *                     <code>
107
 *                     $attrs = array(
108
 *                         'class' => ['elgg-input', 'elgg-input-text'], // will be imploded with spaces
109
 *                         'style' => ['margin-left:10px;', 'color: #666;'], // will be imploded with spaces
110
 *                         'alt' => 'Alt text', // will be left as is
111
 *                         'disabled' => true, // will be converted to disabled="disabled"
112
 *                         'data-options' => json_encode(['foo' => 'bar']), // will be output as an escaped JSON string
113
 *                         'batch' => <\ElggBatch>, // will be ignored
114
 *                         'items' => [<\ElggObject>], // will be ignored
115
 *                     );
116
 *                     </code>
117
 *
118
 * @return string
119
 *
120
 * @see elgg_format_element()
121
 */
122
function elgg_format_attributes(array $attrs = []) {
123 75
	if (!is_array($attrs) || empty($attrs)) {
124 2
		return '';
125
	}
126
127 75
	$attributes = [];
128
129 75
	foreach ($attrs as $attr => $val) {
130 75
		if (0 !== strpos($attr, 'data-') && false !== strpos($attr, '_')) {
131
			// this is probably a view $vars variable not meant for output
132 7
			continue;
133
		}
134
135 75
		$attr = strtolower($attr);
136
137 75
		if (!isset($val) || $val === false) {
138 19
			continue;
139
		}
140
141 75
		if ($val === true) {
142 3
			$val = $attr; //e.g. checked => true ==> checked="checked"
143
		}
144
145 75
		if (is_scalar($val)) {
146 71
			$val = [$val];
147
		}
148
149 75
		if (!is_array($val)) {
150 7
			continue;
151
		}
152
153
		// Check if array contains non-scalar values and bail if so
154
		$filtered_val = array_filter($val, function($e) {
155 75
			return is_scalar($e);
156 75
		});
157
158 75
		if (count($val) != count($filtered_val)) {
159 7
			continue;
160
		}
161
162 75
		$val = implode(' ', $val);
163
164 75
		$val = htmlspecialchars($val, ENT_QUOTES, 'UTF-8', false);
165 75
		$attributes[] = "$attr=\"$val\"";
166
	}
167
168 75
	return implode(' ', $attributes);
169
}
170
171
/**
172
 * Format an HTML element
173
 *
174
 * @param string|array $tag_name   The element tagName. e.g. "div". This will not be validated.
175
 *                                 All function arguments can be given as a single array: The array will be used
176
 *                                 as $attributes, except for the keys "#tag_name", "#text", and "#options", which
177
 *                                 will be extracted as the other arguments.
178
 *
179
 * @param array        $attributes The element attributes. This is passed to elgg_format_attributes().
180
 *
181
 * @param string       $text       The contents of the element. Assumed to be HTML unless encode_text is true.
182
 *
183
 * @param array        $options    Options array with keys:
184
 *
185
 *   encode_text   => (bool, default false) If true, $text will be HTML-escaped. Already-escaped entities
186
 *                    will not be double-escaped.
187
 *
188
 *   double_encode => (bool, default false) If true, the $text HTML escaping will be allowed to double
189
 *                    encode HTML entities: '&times;' will become '&amp;times;'
190
 *
191
 *   is_void       => (bool) If given, this determines whether the function will return just the open tag.
192
 *                    Otherwise this will be determined by the tag name according to this list:
193
 *                    http://www.w3.org/html/wg/drafts/html/master/single-page.html#void-elements
194
 *
195
 *   is_xml        => (bool, default false) If true, void elements will be formatted like "<tag />"
196
 *
197
 * @return string
198
 * @throws InvalidArgumentException
199
 * @since 1.9.0
200
 */
201
function elgg_format_element($tag_name, array $attributes = [], $text = '', array $options = []) {
202 70
	if (is_array($tag_name)) {
203
		$args = $tag_name;
204
205
		if ($attributes !== [] || $text !== '' || $options !== []) {
206
			throw new \InvalidArgumentException('If $tag_name is an array, the other arguments must not be set');
207
		}
208
209
		if (isset($args['#tag_name'])) {
210
			$tag_name = $args['#tag_name'];
211
		}
212
		if (isset($args['#text'])) {
213
			$text = $args['#text'];
214
		}
215
		if (isset($args['#options'])) {
216
			$options = $args['#options'];
217
		}
218
219
		unset($args['#tag_name'], $args['#text'], $args['#options']);
220
		$attributes = $args;
221
	}
222
223 70
	if (!is_string($tag_name) || $tag_name === '') {
224
		throw new \InvalidArgumentException('$tag_name is required');
225
	}
226
227 70
	if (isset($options['is_void'])) {
228
		$is_void = $options['is_void'];
229
	} else {
230
		// from http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
231 70
		$is_void = in_array(strtolower($tag_name), [
232
			'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem',
233
			'meta', 'param', 'source', 'track', 'wbr'
234
		]);
235
	}
236
237 70
	if (!empty($options['encode_text'])) {
238 1
		$double_encode = empty($options['double_encode']) ? false : true;
239 1
		$text = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', $double_encode);
240
	}
241
242 70
	if ($attributes) {
243 70
		$attrs = elgg_format_attributes($attributes);
244 70
		if ($attrs !== '') {
245 70
			$attrs = " $attrs";
246
		}
247
	} else {
248 4
		$attrs = '';
249
	}
250
251 70
	if ($is_void) {
252 9
		return empty($options['is_xml']) ? "<{$tag_name}{$attrs}>" : "<{$tag_name}{$attrs} />";
253
	} else {
254 70
		return "<{$tag_name}{$attrs}>$text</$tag_name>";
255
	}
256
}
257
258
/**
259
 * Converts shorthand URLs to absolute URLs, unless the given URL is absolute, protocol-relative,
260
 * or starts with a protocol/fragment/query
261
 *
262
 * @example
263
 * elgg_normalize_url('');                   // 'http://my.site.com/'
264
 * elgg_normalize_url('dashboard');          // 'http://my.site.com/dashboard'
265
 * elgg_normalize_url('http://google.com/'); // no change
266
 * elgg_normalize_url('//google.com/');      // no change
267
 *
268
 * @param string $url The URL to normalize
269
 *
270
 * @return string The absolute URL
271
 */
272
function elgg_normalize_url($url) {
273 304
	$url = str_replace(' ', '%20', $url);
274
275 304
	if (_elgg_sane_validate_url($url)) {
276 97
		return $url;
277
	}
278
279 288
	if (preg_match("#^([a-z]+)\\:#", $url, $m)) {
280
		// we don't let http/https: URLs fail filter_var(), but anything else starting with a protocol
281
		// is OK
282 6
		if ($m[1] !== 'http' && $m[1] !== 'https') {
283 6
			return $url;
284
		}
285
	}
286
287 288
	if (preg_match("#^(\\#|\\?|//)#", $url)) {
288
		// starts with '//' (protocol-relative link), query, or fragment
289 3
		return $url;
290
	}
291
292 286
	if (preg_match("#^[^/]*\\.php(\\?.*)?$#", $url)) {
293
		// root PHP scripts: 'install.php', 'install.php?step=step'. We don't want to confuse these
294
		// for domain names.
295 5
		return elgg_get_site_url() . $url;
296
	}
297
298 286
	if (preg_match("#^[^/?]*\\.#", $url)) {
299
		// URLs starting with domain: 'example.com', 'example.com/subpage'
300 2
		return "http://$url";
301
	}
302
303
	// 'page/handler', 'mod/plugin/file.php'
304
	// trim off any leading / because the site URL is stored
305
	// with a trailing /
306 285
	return elgg_get_site_url() . ltrim($url, '/');
307
}
308
309
/**
310
 * From untrusted input, get a site URL safe for forwarding.
311
 *
312
 * @param string $unsafe_url URL from untrusted input
313
 *
314
 * @return bool|string Normalized URL or false if given URL was not a path.
315
 *
316
 * @since 3.0.0
317
 */
318
function elgg_normalize_site_url($unsafe_url) {
319 2
	if (!is_string($unsafe_url)) {
320
		return false;
321
	}
322
323 2
	$unsafe_url = elgg_normalize_url($unsafe_url);
324 2
	if (0 === strpos($unsafe_url, elgg_get_site_url())) {
325 2
		return $unsafe_url;
326
	}
327
328
	return false;
329
}
330
331
/**
332
 * When given a title, returns a version suitable for inclusion in a URL
333
 *
334
 * @param string $title The title
335
 *
336
 * @return string The optimized title
337
 * @since 1.7.2
338
 */
339
function elgg_get_friendly_title($title) {
340
341
	// return a URL friendly title to short circuit normal title formatting
342 1
	$params = ['title' => $title];
343 1
	$result = elgg_trigger_plugin_hook('format', 'friendly:title', $params, null);
344 1
	if ($result) {
345
		return $result;
346
	}
347
348
	// titles are often stored HTML encoded
349 1
	$title = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
350
	
351 1
	$title = \Elgg\Translit::urlize($title);
352
353 1
	return $title;
354
}
355
356
/**
357
 * Formats a UNIX timestamp in a friendly way (eg "less than a minute ago")
358
 *
359
 * @see elgg_view_friendly_time()
360
 *
361
 * @param int $time         A UNIX epoch timestamp
362
 * @param int $current_time Current UNIX epoch timestamp (optional)
363
 *
364
 * @return string The friendly time string
365
 * @since 1.7.2
366
 */
367
function elgg_get_friendly_time($time, $current_time = null) {
368
	
369 8
	if (!$current_time) {
370 8
		$current_time = time();
371
	}
372
373
	// return a time string to short circuit normal time formatting
374 8
	$params = ['time' => $time, 'current_time' => $current_time];
375 8
	$result = elgg_trigger_plugin_hook('format', 'friendly:time', $params, null);
376 8
	if ($result) {
377
		return $result;
378
	}
379
380 8
	$diff = abs((int) $current_time - (int) $time);
381
382 8
	$minute = 60;
383 8
	$hour = $minute * 60;
384 8
	$day = $hour * 24;
385
386 8
	if ($diff < $minute) {
387 8
		return elgg_echo("friendlytime:justnow");
388
	}
389
	
390
	if ($diff < $hour) {
391
		$granularity = ':minutes';
392
		$diff = round($diff / $minute);
393
	} else if ($diff < $day) {
394
		$granularity = ':hours';
395
		$diff = round($diff / $hour);
396
	} else {
397
		$granularity = ':days';
398
		$diff = round($diff / $day);
399
	}
400
401
	if ($diff == 0) {
402
		$diff = 1;
403
	}
404
	
405
	$future = ((int) $current_time - (int) $time < 0) ? ':future' : '';
406
	$singular = ($diff == 1) ? ':singular' : '';
407
408
	return elgg_echo("friendlytime{$future}{$granularity}{$singular}", [$diff]);
409
}
410
411
/**
412
 * Returns a human-readable message for PHP's upload error codes
413
 *
414
 * @param int $error_code The code as stored in $_FILES['name']['error']
415
 * @return string
416
 */
417
function elgg_get_friendly_upload_error($error_code) {
418
	switch ($error_code) {
419
		case UPLOAD_ERR_OK:
420
			return '';
421
			
422
		case UPLOAD_ERR_INI_SIZE:
423
			$key = 'ini_size';
424
			break;
425
		
426
		case UPLOAD_ERR_FORM_SIZE:
427
			$key = 'form_size';
428
			break;
429
430
		case UPLOAD_ERR_PARTIAL:
431
			$key = 'partial';
432
			break;
433
434
		case UPLOAD_ERR_NO_FILE:
435
			$key = 'no_file';
436
			break;
437
438
		case UPLOAD_ERR_NO_TMP_DIR:
439
			$key = 'no_tmp_dir';
440
			break;
441
442
		case UPLOAD_ERR_CANT_WRITE:
443
			$key = 'cant_write';
444
			break;
445
446
		case UPLOAD_ERR_EXTENSION:
447
			$key = 'extension';
448
			break;
449
		
450
		default:
451
			$key = 'unknown';
452
			break;
453
	}
454
455
	return elgg_echo("upload:error:$key");
456
}
457
458
459
/**
460
 * Strip tags and offer plugins the chance.
461
 * Plugins register for output:strip_tags plugin hook.
462
 * Original string included in $params['original_string']
463
 *
464
 * @param string $string         Formatted string
465
 * @param string $allowable_tags Optional parameter to specify tags which should not be stripped
466
 *
467
 * @return string String run through strip_tags() and any plugin hooks.
468
 */
469
function elgg_strip_tags($string, $allowable_tags = null) {
470 16
	$params['original_string'] = $string;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.
Loading history...
471 16
	$params['allowable_tags'] = $allowable_tags;
472
473 16
	$string = strip_tags($string, $allowable_tags);
474 16
	$string = elgg_trigger_plugin_hook('format', 'strip_tags', $params, $string);
475
476 16
	return $string;
477
}
478
479
/**
480
 * Decode HTML markup into a raw text string
481
 *
482
 * This applies html_entity_decode() to a string while re-entitising HTML
483
 * special char entities to prevent them from being decoded back to their
484
 * unsafe original forms.
485
 *
486
 * This relies on html_entity_decode() not translating entities when
487
 * doing so leaves behind another entity, e.g. &amp;gt; if decoded would
488
 * create &gt; which is another entity itself. This seems to escape the
489
 * usual behaviour where any two paired entities creating a HTML tag are
490
 * usually decoded, i.e. a lone &gt; is not decoded, but &lt;foo&gt; would
491
 * be decoded to <foo> since it creates a full tag.
492
 *
493
 * Note: html_entity_decode() is poorly explained in the manual - which is really
494
 * bad given its potential for misuse on user input already escaped elsewhere.
495
 * Stackoverflow is littered with advice to use this function in the precise
496
 * way that would lead to user input being capable of injecting arbitrary HTML.
497
 *
498
 * @param string $string Encoded HTML
499
 *
500
 * @return string
501
 *
502
 * @author Pádraic Brady
503
 * @copyright Copyright (c) 2010 Pádraic Brady (http://blog.astrumfutura.com)
504
 * @license Released under dual-license GPL2/MIT by explicit permission of Pádraic Brady
505
 */
506
function elgg_html_decode($string) {
507
	$string = str_replace(
508
		['&gt;', '&lt;', '&amp;', '&quot;', '&#039;'],
509
		['&amp;gt;', '&amp;lt;', '&amp;amp;', '&amp;quot;', '&amp;#039;'],
510
		$string
511
	);
512
	$string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8');
513
	$string = str_replace(
514
		['&amp;gt;', '&amp;lt;', '&amp;amp;', '&amp;quot;', '&amp;#039;'],
515
		['&gt;', '&lt;', '&amp;', '&quot;', '&#039;'],
516
		$string
517
	);
518
	return $string;
519
}
520
521
/**
522
 * Prepares query string for output to prevent CSRF attacks.
523
 *
524
 * @param string $string string to prepare
525
 * @return string
526
 *
527
 * @access private
528
 */
529
function _elgg_get_display_query($string) {
530
	//encode <,>,&, quotes and characters above 127
531 3
	if (function_exists('mb_convert_encoding')) {
532 3
		$display_query = mb_convert_encoding($string, 'HTML-ENTITIES', 'UTF-8');
533
	} else {
534
		// if no mbstring extension, we just strip characters
535
		$display_query = preg_replace("/[^\x01-\x7F]/", "", $string);
536
	}
537 3
	return htmlspecialchars($display_query, ENT_QUOTES, 'UTF-8', false);
538
}
539
540
/**
541
 * Use a "fixed" filter_var() with FILTER_VALIDATE_URL that handles multi-byte chars.
542
 *
543
 * @param string $url URL to validate
544
 * @return string|false
545
 * @access private
546
 */
547
function _elgg_sane_validate_url($url) {
548
	// based on http://php.net/manual/en/function.filter-var.php#104160
549 304
	$res = filter_var($url, FILTER_VALIDATE_URL);
550 304
	if ($res) {
551 98
		return $res;
552
	}
553
554
	// Check if it has unicode chars.
555 288
	$l = elgg_strlen($url);
556 288
	if (strlen($url) == $l) {
557 288
		return $res;
558
	}
559
560
	// Replace wide chars by “X”.
561 1
	$s = '';
562 1
	for ($i = 0; $i < $l; ++$i) {
563 1
		$ch = elgg_substr($url, $i, 1);
564 1
		$s .= (strlen($ch) > 1) ? 'X' : $ch;
565
	}
566
567
	// Re-check now.
568 1
	return filter_var($s, FILTER_VALIDATE_URL) ? $url : false;
569
}
570