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

engine/lib/output.php (1 issue)

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);
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
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;
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