Completed
Push — 2.3 ( e6f9d2...a69256 )
by Jeroen
09:57 queued 11s
created

engine/lib/output.php (1 issue)

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