1<?php
2/**
3 * Elgg's view system.
4 *
5 * The view system is the primary templating engine in Elgg and renders
6 * all output.  Views are short, parameterised PHP scripts for displaying
7 * output that can be regsitered, overridden, or extended.  The view type
8 * determines the output format and location of the files that renders the view.
9 *
10 * Elgg uses a two step process to render full output: first
11 * content-specific elements are rendered, then the resulting
12 * content is inserted into a layout and displayed.  This makes it
13 * easy to maintain a consistent look on all pages.
14 *
15 * A view corresponds to a single file on the filesystem and the views
16 * name is its directory structure.  A file in
17 * <code>mod/plugins/views/default/myplugin/example.php</code>
18 * is called by saying (with the default viewtype):
19 * <code>echo elgg_view('myplugin/example');</code>
20 *
21 * View names that are registered later override those that are
22 * registered earlier.  For plugins this corresponds directly
23 * to their load order: views in plugins lower in the list override
24 * those higher in the list.
25 *
26 * Plugin views belong in the views/ directory under an appropriate
27 * viewtype.  Views are automatically registered.
28 *
29 * Views can be embedded-you can call a view from within a view.
30 * Views can also be prepended or extended by any other view.
31 *
32 * Any view can extend any other view if registered with
33 * {@link elgg_extend_view()}.
34 *
35 * Viewtypes are set by passing $_REQUEST['view'].  The viewtype
36 * 'default' is a standard HTML view.  Types can be defined on the fly
37 * and you can get the current viewtype with {@link elgg_get_viewtype()}.
38 *
39 * @note Internal: Plugin views are autoregistered before their init functions
40 * are called, so the init order doesn't affect views.
41 *
42 * @note Internal: The file that determines the output of the view is the last
43 * registered by {@link elgg_set_view_location()}.
44 */
45
46use Elgg\Menu\Menu;
47use Elgg\Menu\UnpreparedMenu;
48use Elgg\Project\Paths;
49
50/**
51 * Manually set the viewtype.
52 *
53 * View types are detected automatically.  This function allows
54 * you to force subsequent views to use a different viewtype.
55 *
56 * @tip Call elgg_set_viewtype() with no parameter to reset.
57 *
58 * @param string $viewtype The view type, e.g. 'rss', or 'default'.
59 *
60 * @return bool
61 */
62function elgg_set_viewtype($viewtype = '') {
63	return _elgg_services()->views->setViewtype($viewtype);
64}
65
66/**
67 * Return the current view type.
68 *
69 * Viewtypes are automatically detected and can be set with $_REQUEST['view']
70 * or {@link elgg_set_viewtype()}.
71 *
72 * @return string The viewtype
73 * @see elgg_set_viewtype()
74 */
75function elgg_get_viewtype() {
76	return _elgg_services()->views->getViewtype();
77}
78
79/**
80 * Register a viewtype to fall back to a default view if a view isn't
81 * found for that viewtype.
82 *
83 * @tip This is useful for alternate html viewtypes (such as for mobile devices).
84 *
85 * @param string $viewtype The viewtype to register
86 *
87 * @return void
88 * @since 1.7.2
89 */
90function elgg_register_viewtype_fallback($viewtype) {
91	_elgg_services()->views->registerViewtypeFallback($viewtype);
92}
93
94/**
95 * Checks if a viewtype falls back to default.
96 *
97 * @param string $viewtype Viewtype
98 *
99 * @return boolean
100 * @since 1.7.2
101 */
102function elgg_does_viewtype_fallback($viewtype) {
103	return _elgg_services()->views->doesViewtypeFallback($viewtype);
104}
105
106/**
107 * Register a view to be available for ajax calls
108 *
109 * @warning Only views that begin with 'js/' and 'css/' have their content
110 * type set to 'text/javascript' and 'text/css'. Other views are served as
111 * 'text/html'.
112 *
113 * @param string $view The view name
114 * @return void
115 * @since 1.8.3
116 */
117function elgg_register_ajax_view($view) {
118	elgg_register_external_view($view, false);
119}
120
121/**
122 * Unregister a view for ajax calls
123 *
124 * @param string $view The view name
125 * @return void
126 * @since 1.8.3
127 */
128function elgg_unregister_ajax_view($view) {
129	elgg_unregister_external_view($view);
130}
131
132/**
133 * Registers a view as being available externally (i.e. via URL).
134 *
135 * @param string  $view      The name of the view.
136 * @param boolean $cacheable Whether this view can be cached.
137 * @return void
138 * @since 1.9.0
139 */
140function elgg_register_external_view($view, $cacheable = false) {
141
142	_elgg_services()->ajax->registerView($view);
143
144	if ($cacheable) {
145		_elgg_services()->views->registerCacheableView($view);
146	}
147}
148
149/**
150 * Unregister a view for ajax calls
151 *
152 * @param string $view The view name
153 * @return void
154 * @since 1.9.0
155 */
156function elgg_unregister_external_view($view) {
157	_elgg_services()->ajax->unregisterView($view);
158}
159
160/**
161 * Set an alternative base location for a view.
162 *
163 * Views are expected to be in plugin_name/views/.  This function can
164 * be used to change that location.
165 *
166 * @tip This is useful to optionally register views in a plugin.
167 *
168 * @param string $view     The name of the view
169 * @param string $location The full path to the view
170 * @param string $viewtype The view type
171 *
172 * @return void
173 */
174function elgg_set_view_location($view, $location, $viewtype = '') {
175	_elgg_services()->views->setViewDir($view, $location, $viewtype);
176}
177
178/**
179 * Returns whether the specified view exists
180 *
181 * @note If $recurse is true, also checks if a view exists only as an extension.
182 *
183 * @param string $view     The view name
184 * @param string $viewtype If set, forces the viewtype
185 * @param bool   $recurse  If false, do not check extensions
186 *
187 * @return bool
188 */
189function elgg_view_exists($view, $viewtype = '', $recurse = true) {
190	return _elgg_services()->views->viewExists($view, $viewtype, $recurse);
191}
192
193/**
194 * Return a parsed view.
195 *
196 * Views are rendered by a template handler and returned as strings.
197 *
198 * Views are called with a special $vars variable set,
199 * which includes any variables passed as the second parameter.
200 *
201 * The input of views can be intercepted by registering for the
202 * view_vars, $view_name plugin hook.
203 *
204 * If the input contains the key "__view_output", the view will output this value as a string.
205 * No extensions are used, and the "view" hook is not triggered).
206 *
207 * The output of views can be intercepted by registering for the
208 * view, $view_name plugin hook.
209 *
210 * @param string $view     The name and location of the view to use
211 * @param array  $vars     Variables to pass to the view.
212 * @param string $viewtype If set, forces the viewtype for the elgg_view call to be
213 *                          this value (default: standard detection)
214 *
215 * @return string The parsed view
216 */
217function elgg_view($view, $vars = [], $viewtype = '') {
218	if (func_num_args() == 5) {
219		elgg_log(__FUNCTION__ . ' now has only 3 arguments. Update your usage.', 'ERROR');
220		$viewtype = func_get_arg(4);
221	}
222	return _elgg_services()->views->renderView($view, $vars, $viewtype);
223}
224
225/**
226 * Display a view with a deprecation notice. No missing view NOTICE is logged
227 *
228 * @param string $view       The name and location of the view to use
229 * @param array  $vars       Variables to pass to the view
230 * @param string $suggestion Suggestion with the deprecation message
231 * @param string $version    Human-readable *release* version: 1.7, 1.8, ...
232 *
233 * @return string The parsed view
234 *
235 * @see elgg_view()
236 */
237function elgg_view_deprecated($view, array $vars, $suggestion, $version) {
238	return _elgg_services()->views->renderDeprecatedView($view, $vars, $suggestion, $version);
239}
240
241/**
242 * Extends a view with another view.
243 *
244 * The output of any view can be prepended or appended to any other view.
245 *
246 * The default action is to append a view.  If the priority is less than 500,
247 * the output of the extended view will be appended to the original view.
248 *
249 * Views can be extended multiple times, and extensions are not checked for
250 * uniqueness. Use {@link elgg_unextend_view()} to help manage duplicates.
251 *
252 * Priority can be specified and affects the order in which extensions
253 * are appended or prepended.
254 *
255 * @see elgg_prepend_css_urls() If the extension is CSS, you may need to use this to fix relative URLs.
256 *
257 * @param string $view           The view to extend.
258 * @param string $view_extension This view is added to $view
259 * @param int    $priority       The priority, from 0 to 1000, to add at (lowest numbers displayed first)
260 *
261 * @return void
262 * @since 1.7.0
263 */
264function elgg_extend_view($view, $view_extension, $priority = 501) {
265	_elgg_services()->views->extendView($view, $view_extension, $priority);
266}
267
268/**
269 * Unextends a view.
270 *
271 * @param string $view           The view that was extended.
272 * @param string $view_extension This view that was added to $view
273 *
274 * @return bool
275 * @since 1.7.2
276 */
277function elgg_unextend_view($view, $view_extension) {
278	return _elgg_services()->views->unextendView($view, $view_extension);
279}
280
281/**
282 * Get the views (and priorities) that extend a view.
283 *
284 * @note extensions may change anytime, especially during the [init, system] event
285 *
286 * @param string $view View name
287 *
288 * @return string[] Keys returned are view priorities.
289 * @since 2.3
290 */
291function elgg_get_view_extensions($view) {
292	$list = _elgg_services()->views->getViewList($view);
293	unset($list[500]);
294	return $list;
295}
296
297/**
298 * In CSS content, prepend a path to relative URLs.
299 *
300 * This is useful to process a CSS view being used as an extension.
301 *
302 * @param string $css  CSS
303 * @param string $path Path to prepend. E.g. "foo/bar/" or "../"
304 *
305 * @return string
306 * @since 2.2
307 */
308function elgg_prepend_css_urls($css, $path) {
309	return Minify_CSS_UriRewriter::prepend($css, $path);
310}
311
312/**
313 * Assembles and outputs a full page.
314 *
315 * A "page" in Elgg is determined by the current view type and
316 * can be HTML for a browser, RSS for a feed reader, or
317 * Javascript, PHP and a number of other formats.
318 *
319 * For HTML pages, use the 'head', 'page' plugin hook for setting meta elements
320 * and links.
321 *
322 * @param string       $title      Title
323 * @param string|array $body       Body as a string or as an array (which will be passed to elgg_view_layout('default', $body)
324 * @param string       $page_shell Optional page shell to use. See page/shells view directory
325 * @param array        $vars       Optional vars array to pass to the page
326 *                                 shell. Automatically adds title, body, head, and sysmessages
327 *
328 * @return string The contents of the page
329 * @since  1.8
330 */
331function elgg_view_page($title, $body, $page_shell = 'default', $vars = []) {
332
333	if (is_array($body)) {
334		$body['title'] = elgg_extract('title', $body, $title);
335		$body = elgg_view_layout('default', $body);
336	}
337
338	$timer = _elgg_services()->timer;
339	if (!$timer->hasEnded(['build page'])) {
340		$timer->end(['build page']);
341	}
342	$timer->begin([__FUNCTION__]);
343
344	$params = [];
345	$params['identifier'] = _elgg_services()->request->getFirstUrlSegment();
346	$params['segments'] = _elgg_services()->request->getUrlSegments();
347	array_shift($params['segments']);
348	$page_shell = elgg_trigger_plugin_hook('shell', 'page', $params, $page_shell);
349
350
351	$system_messages = _elgg_services()->systemMessages;
352
353	$messages = null;
354	if ($system_messages->count()) {
355		$messages = $system_messages->dumpRegister();
356
357		if (isset($messages['error'])) {
358			// always make sure error is the first type
359			$errors = [
360				'error' => $messages['error']
361			];
362
363			unset($messages['error']);
364			$messages = array_merge($errors, $messages);
365		}
366	}
367
368	$vars['title'] = $title;
369	$vars['body'] = $body;
370	$vars['sysmessages'] = $messages;
371	$vars['page_shell'] = $page_shell;
372
373	// head has keys 'title', 'metas', 'links'
374	$head_params = _elgg_views_prepare_head($title);
375
376	$vars['head'] = elgg_trigger_plugin_hook('head', 'page', $vars, $head_params);
377
378	$vars = elgg_trigger_plugin_hook('output:before', 'page', null, $vars);
379
380	$output = elgg_view("page/$page_shell", $vars);
381
382
383	// Allow plugins to modify the output
384	$output = elgg_trigger_plugin_hook('output', 'page', $vars, $output);
385
386	$timer->end([__FUNCTION__]);
387	return $output;
388}
389
390/**
391 * Render a resource view. Use this in your page handler to hand off page rendering to
392 * a view in "resources/". If not found in the current viewtype, we try the "default" viewtype.
393 *
394 * @param string $name The view name without the leading "resources/"
395 * @param array  $vars Arguments passed to the view
396 *
397 * @return string
398 * @throws \Elgg\PageNotFoundException
399 */
400function elgg_view_resource($name, array $vars = []) {
401	$view = "resources/$name";
402
403	if (elgg_view_exists($view)) {
404		return _elgg_services()->views->renderView($view, $vars);
405	}
406
407	if (elgg_get_viewtype() !== 'default' && elgg_view_exists($view, 'default')) {
408		return _elgg_services()->views->renderView($view, $vars, 'default');
409	}
410
411	_elgg_services()->logger->error("The view $view is missing.");
412
413	// only works for default viewtype
414	throw new \Elgg\PageNotFoundException();
415}
416
417/**
418 * Prepare the variables for the html head
419 *
420 * @param string $title Page title for <head>
421 * @return array
422 * @internal
423 */
424function _elgg_views_prepare_head($title) {
425	$params = [
426		'links' => [],
427		'metas' => [],
428	];
429
430	if (empty($title)) {
431		$params['title'] = _elgg_config()->sitename;
432	} else {
433		$params['title'] = $title . ' : ' . _elgg_config()->sitename;
434	}
435
436	$params['metas']['content-type'] = [
437		'http-equiv' => 'Content-Type',
438		'content' => 'text/html; charset=utf-8',
439	];
440
441	$params['metas']['description'] = [
442		'name' => 'description',
443		'content' => _elgg_config()->sitedescription
444	];
445
446	// https://developer.chrome.com/multidevice/android/installtohomescreen
447	$params['metas']['viewport'] = [
448		'name' => 'viewport',
449		'content' => 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0',
450	];
451	$params['metas']['mobile-web-app-capable'] = [
452		'name' => 'mobile-web-app-capable',
453		'content' => 'yes',
454	];
455	$params['metas']['apple-mobile-web-app-capable'] = [
456		'name' => 'apple-mobile-web-app-capable',
457		'content' => 'yes',
458	];
459
460	// RSS feed link
461	if (_elgg_has_rss_link()) {
462		$url = current_page_url();
463		if (elgg_substr_count($url, '?')) {
464			$url .= "&view=rss";
465		} else {
466			$url .= "?view=rss";
467		}
468		$params['links']['rss'] = [
469			'rel' => 'alternative',
470			'type' => 'application/rss+xml',
471			'title' => 'RSS',
472			'href' => $url,
473		];
474	}
475
476	return $params;
477}
478
479
480/**
481 * Add favicon link tags to HTML head
482 *
483 * @param \Elgg\Hook $hook "head", "page"
484 * returnvalue contains head params
485 *		<code>
486 *      	[
487 *				'title' => '',
488 *				'metas' => [],
489 *				'links' => [],
490 *			]
491 *		</code>
492 *
493 * @return array
494 */
495function _elgg_views_prepare_favicon_links(\Elgg\Hook $hook) {
496	$head_params = $hook->getValue();
497
498	$head_params['links']['apple-touch-icon'] = [
499		'rel' => 'apple-touch-icon',
500		'href' => elgg_get_simplecache_url('graphics/favicon-128.png'),
501	];
502
503	// favicons
504	$head_params['links']['icon-ico'] = [
505		'rel' => 'icon',
506		'href' => elgg_get_simplecache_url('graphics/favicon.ico'),
507	];
508	$head_params['links']['icon-vector'] = [
509		'rel' => 'icon',
510		'sizes' => '16x16 32x32 48x48 64x64 128x128',
511		'type' => 'image/svg+xml',
512		'href' => elgg_get_simplecache_url('graphics/favicon.svg'),
513	];
514	$head_params['links']['icon-16'] = [
515		'rel' => 'icon',
516		'sizes' => '16x16',
517		'type' => 'image/png',
518		'href' => elgg_get_simplecache_url('graphics/favicon-16.png'),
519	];
520	$head_params['links']['icon-32'] = [
521		'rel' => 'icon',
522		'sizes' => '32x32',
523		'type' => 'image/png',
524		'href' => elgg_get_simplecache_url('graphics/favicon-32.png'),
525	];
526	$head_params['links']['icon-64'] = [
527		'rel' => 'icon',
528		'sizes' => '64x64',
529		'type' => 'image/png',
530		'href' => elgg_get_simplecache_url('graphics/favicon-64.png'),
531	];
532	$head_params['links']['icon-128'] = [
533		'rel' => 'icon',
534		'sizes' => '128x128',
535		'type' => 'image/png',
536		'href' => elgg_get_simplecache_url('graphics/favicon-128.png'),
537	];
538
539	return $head_params;
540}
541
542/**
543 * Displays a layout with optional parameters.
544 *
545 * Layouts are templates provide consistency by organizing blocks of content on the page.
546 *
547 * Plugins should use one of the core layouts:
548 *  - default     Primary template with one, two or no sidebars
549 *  - admin       Admin page template
550 *  - error       Error page template
551 *  - widgets     Widgets canvas
552 *
553 * Plugins can create and use custom layouts by placing a layout view
554 * in "page/layouts/<layout_name>" and calling elgg_view_layout(<layout_name>).
555 *
556 * For a full list of parameters supported by each of these layouts see
557 * corresponding layout views.
558 *
559 * @param string $layout_name Layout name
560 *                            Corresponds to a view in "page/layouts/<layout_name>".
561 * @param array  $vars        Layout parameters
562 *                            An associative array of parameters to pass to
563 *                            the layout hooks and views.
564 *                            Route 'identifier' and 'segments' of the page being
565 *                            rendered will be added to this array automatially,
566 *                            allowing plugins to alter layout views and subviews
567 *                            based on the current route.
568 * @return string
569 */
570function elgg_view_layout($layout_name, $vars = []) {
571	$timer = _elgg_services()->timer;
572	if (!$timer->hasEnded(['build page'])) {
573		$timer->end(['build page']);
574	}
575	$timer->begin([__FUNCTION__]);
576
577	if (in_array($layout_name, ['content', 'one_sidebar', 'one_column', 'two_sidebar'])) {
578		elgg_deprecated_notice("Using the '{$layout_name}' layout is deprecated. Please update your code to use the 'default' layout.", '3.3');
579	}
580
581	if ($layout_name !== 'content' && isset($vars['filter_context'])) {
582		elgg_deprecated_notice("Using 'filter_context' to set the active menu item is not supported. Please update your code to use the 'filter_value' var.", '3.3');
583	}
584
585	// Help plugins transition without breaking them
586	switch ($layout_name) {
587		case 'content' :
588			$layout_name = 'default';
589			$vars = _elgg_normalize_content_layout_vars($vars);
590			break;
591
592		case 'one_sidebar' :
593			$layout_name = 'default';
594			$vars['sidebar'] = elgg_extract('sidebar', $vars, '', false);
595			$vars['sidebar_alt'] = false;
596			break;
597
598		case 'one_column' :
599			$layout_name = 'default';
600			$vars['sidebar'] = false;
601			$vars['sidebar_alt'] = false;
602			break;
603
604		case 'two_sidebar' :
605			$layout_name = 'default';
606			$vars['sidebar'] = elgg_extract('sidebar', $vars, '', false);
607			$vars['sidebar_alt'] = elgg_extract('sidebar_alt', $vars, '', false);
608			break;
609
610		case 'default' :
611			$filter_id = elgg_extract('filter_id', $vars, 'filter');
612			$filter_context = elgg_extract('filter_value', $vars);
613			if (isset($filter_context) && $filter_id === 'filter') {
614				$context = elgg_extract('context', $vars, elgg_get_context());
615				$vars['filter'] = elgg_get_filter_tabs($context, $filter_context, null, $vars);
616				$vars['filter_id'] = $filter_id;
617				$vars['filter_value'] = $filter_context;
618			}
619			break;
620	}
621
622	if (isset($vars['nav'])) {
623		// Temporary helper until all core views are updated
624		$vars['breadcrumbs'] = $vars['nav'];
625		unset($vars['nav']);
626	}
627
628	$vars['identifier'] = _elgg_services()->request->getFirstUrlSegment();
629	$vars['segments'] = _elgg_services()->request->getUrlSegments();
630	array_shift($vars['segments']);
631
632	$layout_name = elgg_trigger_plugin_hook('layout', 'page', $vars, $layout_name);
633
634	$vars['layout'] = $layout_name;
635
636	$layout_views = [
637		"page/layouts/$layout_name",
638		"page/layouts/default",
639	];
640
641	$output = '';
642	foreach ($layout_views as $layout_view) {
643		if (elgg_view_exists($layout_view)) {
644			$output = elgg_view($layout_view, $vars);
645			break;
646		}
647	}
648
649	$timer->end([__FUNCTION__]);
650	return $output;
651}
652
653/**
654 * Normalizes deprecated content layout $vars for use in default layout
655 * Helper function to assist plugins transitioning to 3.0
656 *
657 * @param array $vars Vars
658 * @return array
659 * @internal
660 */
661function _elgg_normalize_content_layout_vars(array $vars = []) {
662
663	$context = elgg_extract('context', $vars, elgg_get_context());
664
665	$vars['title'] = elgg_extract('title', $vars, '');
666	if (!$vars['title'] && $vars['title'] !== false) {
667		$vars['title'] = elgg_echo($context);
668	}
669
670	// 1.8 supported 'filter_override'
671	if (isset($vars['filter_override'])) {
672		$vars['filter'] = $vars['filter_override'];
673	}
674
675	// register the default content filters
676	if (!isset($vars['filter']) && $context) {
677		$selected = elgg_extract('filter_context', $vars);
678		$vars['filter'] = elgg_get_filter_tabs($context, $selected, null, $vars);
679		$vars['filter_id'] = $context;
680		$vars['filter_value'] = $selected;
681	}
682
683	return $vars;
684}
685
686/**
687 * Render a menu
688 *
689 * @see elgg_register_menu_item() for documentation on adding menu items and
690 * navigation.php for information on the different menus available.
691 *
692 * This function triggers a 'register', 'menu:<menu name>' plugin hook that enables
693 * plugins to add menu items just before a menu is rendered. This is used by
694 * dynamic menus (menus that change based on some input such as the user hover
695 * menu). Using elgg_register_menu_item() in response to the hook can cause
696 * incorrect links to show up. See the blog plugin's blog_owner_block_menu()
697 * for an example of using this plugin hook.
698 *
699 * An additional hook is the 'prepare', 'menu:<menu name>' which enables plugins
700 * to modify the structure of the menu (sort it, remove items, set variables on
701 * the menu items).
702 *
703 * Preset (unprepared) menu items passed to the this function with the $vars
704 * argument, will be merged with the registered items (registered with
705 * elgg_register_menu_item()). The combined set of menu items will be passed
706 * to 'register', 'menu:<menu_name>' hook.
707 *
708 * Plugins that pass preset menu items to this function and do not wish to be
709 * affected by plugin hooks (e.g. if you are displaying multiple menus with
710 * the same name on the page) should instead choose a unqie menu name
711 * and define a menu_view argument to render menus consistently.
712 * For example, if you have multiple 'filter' menus on the page:
713 * <code>
714 *    elgg_view_menu("filter:$uid", [
715 *        'items' => $items,
716 *        'menu_view' => 'navigation/menu/filter',
717 *    ]);
718 * </code>
719 *
720 * elgg_view_menu() uses views in navigation/menu
721 *
722 * @param string|Menu|UnpreparedMenu $menu Menu name (or object)
723 * @param array                      $vars An associative array of display options for the menu.
724 *
725 *  Options include:
726 *    items                => (array) an array of unprepared menu items as ElggMenuItem or menu item factory options
727 *    sort_by              => (string) or php callback string options: 'name', 'priority' (default), 'text'
728 *                            or a php callback (a compare function for usort)
729 *    handler              => (string) the page handler to build action URLs
730 *    entity               => (\ElggEntity) entity to use to build action URLs
731 *    class                => (string) the class for the entire menu
732 *    menu_view            => (string) name of the view to be used to render the menu
733 *    show_section_headers => (bool) show headers before menu sections
734 *    selected_item_name   => (string) the menu item name to be selected
735 *
736 * @return string
737 * @since 1.8.0
738 */
739function elgg_view_menu($menu, array $vars = []) {
740
741	$menu_view = elgg_extract('menu_view', $vars);
742	unset($vars['menu_view']);
743
744	if (is_string($menu)) {
745		$menu = _elgg_services()->menus->getMenu($menu, $vars);
746	} elseif ($menu instanceof UnpreparedMenu) {
747		$menu = _elgg_services()->menus->prepareMenu($menu);
748	}
749
750	if (!$menu instanceof Menu) {
751		throw new \InvalidArgumentException('$menu must be a menu name, a Menu, or UnpreparedMenu');
752	}
753
754	$name = $menu->getName();
755	$params = $menu->getParams();
756
757	$views = [
758		$menu_view,
759		"navigation/menu/$name",
760		'navigation/menu/default',
761	];
762
763	foreach ($views as $view) {
764		if (elgg_view_exists($view)) {
765			return elgg_view($view, $params);
766		}
767	}
768}
769
770/**
771 * Render a menu item (usually as a link)
772 *
773 * @param \ElggMenuItem $item The menu item
774 * @param array         $vars Options to pass to output/url if a link
775 * @return string
776 * @since 1.9.0
777 */
778function elgg_view_menu_item(\ElggMenuItem $item, array $vars = []) {
779
780	$vars = array_merge($item->getValues(), $vars);
781	$vars['class'] = elgg_extract_class($vars, ['elgg-menu-content']);
782
783	if ($item->getLinkClass()) {
784		$vars['class'][] = $item->getLinkClass();
785	}
786
787	if ($item->getHref() === false || $item->getHref() === null) {
788		$vars['class'][] = 'elgg-non-link';
789	}
790
791	if (!isset($vars['rel']) && !isset($vars['is_trusted'])) {
792		$vars['is_trusted'] = true;
793	}
794
795	if ($item->getConfirmText()) {
796		$vars['confirm'] = $item->getConfirmText();
797	}
798
799	return elgg_view('output/url', $vars);
800}
801
802/**
803 * Returns a string of a rendered entity.
804 *
805 * Entity views are either determined by setting the view property on the entity
806 * or by having a view named after the entity $type/$subtype.  Entities that have
807 * neither a view property nor a defined $type/$subtype view will fall back to
808 * using the $type/default view.
809 *
810 * The entity view is called with the following in $vars:
811 *  - \ElggEntity 'entity' The entity being viewed
812 *
813 * @param \ElggEntity $entity The entity to display
814 * @param array       $vars   Array of variables to pass to the entity view.
815 *      'full_view'           Whether to show a full or condensed view. (Default: true)
816 *      'item_view'           Alternative view used to render this entity
817 *      'register_rss_link'   Register the rss link availability (default: depending on full_view)
818 *
819 * @return false|string HTML to display or false
820 * @todo The annotation hook might be better as a generic plugin hook to append content.
821 */
822function elgg_view_entity(\ElggEntity $entity, array $vars = []) {
823
824	$defaults = [
825		'full_view' => true,
826	];
827
828	$vars = array_merge($defaults, $vars);
829
830	if (elgg_extract('register_rss_link', $vars, elgg_extract('full_view', $vars))) {
831		elgg_register_rss_link();
832	}
833
834	$vars['entity'] = $entity;
835
836	$entity_type = $entity->getType();
837	$entity_subtype = $entity->getSubtype();
838
839	$entity_views = [
840		elgg_extract('item_view', $vars, ''),
841		"$entity_type/$entity_subtype",
842		"$entity_type/default",
843	];
844
845	$contents = '';
846	foreach ($entity_views as $view) {
847		if (elgg_view_exists($view)) {
848			$contents = elgg_view($view, $vars);
849			break;
850		}
851	}
852
853	// Marcus Povey 20090616 : Speculative and low impact approach for fixing #964
854	if ($vars['full_view']) {
855		$annotations = elgg_view_entity_annotations($entity, $vars['full_view']);
856
857		if ($annotations) {
858			$contents .= $annotations;
859		}
860	}
861	return $contents;
862}
863
864/**
865 * View the icon of an entity
866 *
867 * Entity views are determined by having a view named after the entity $type/$subtype.
868 * Entities that do not have a defined icon/$type/$subtype view will fall back to using
869 * the icon/$type/default view.
870 *
871 * @param \ElggEntity $entity The entity to display
872 * @param string      $size   The size: tiny, small, medium, large
873 * @param array       $vars   An array of variables to pass to the view. Some possible
874 *                            variables are img_class and link_class. See the
875 *                            specific icon view for more parameters.
876 *
877 * @return string HTML to display or false
878 */
879function elgg_view_entity_icon(\ElggEntity $entity, $size = 'medium', $vars = []) {
880
881	$vars['entity'] = $entity;
882	$vars['size'] = $size;
883
884	$entity_type = $entity->getType();
885
886	$subtype = $entity->getSubtype();
887
888	$contents = '';
889	if (elgg_view_exists("icon/$entity_type/$subtype")) {
890		$contents = elgg_view("icon/$entity_type/$subtype", $vars);
891	}
892	if (empty($contents) && elgg_view_exists("icon/$entity_type/default")) {
893		$contents = elgg_view("icon/$entity_type/default", $vars);
894	}
895	if (empty($contents)) {
896		$contents = elgg_view("icon/default", $vars);
897	}
898
899	return $contents;
900}
901
902/**
903 * Returns a string of a rendered annotation.
904 *
905 * Annotation views are expected to be in annotation/$annotation_name.
906 * If a view is not found for $annotation_name, the default annotation/default
907 * will be used.
908 *
909 * The annotation view is called with the following in $vars:
910 *  - \ElggEntity 'annotation' The annotation being viewed.
911 *
912 * @param \ElggAnnotation $annotation The annotation to display
913 * @param array           $vars       Variable array for view.
914 *      'item_view'  Alternative view used to render an annotation
915 *
916 * @return string|false Rendered annotation
917 */
918function elgg_view_annotation(\ElggAnnotation $annotation, array $vars = []) {
919	$defaults = [
920		'full_view' => true,
921	];
922
923	$vars = array_merge($defaults, $vars);
924	$vars['annotation'] = $annotation;
925
926	$name = $annotation->name;
927	if (empty($name)) {
928		return false;
929	}
930
931	$annotation_views = [
932		elgg_extract('item_view', $vars, ''),
933		"annotation/$name",
934		"annotation/default",
935	];
936
937	$contents = '';
938	foreach ($annotation_views as $view) {
939		if (elgg_view_exists($view)) {
940			$contents = elgg_view($view, $vars);
941			break;
942		}
943	}
944
945	return $contents;
946}
947
948/**
949 * Returns a rendered list of entities with pagination. This function should be
950 * called by wrapper functions.
951 *
952 * @see elgg_list_entities()
953 *
954 * @param array $entities Array of entities
955 * @param array $vars     Display variables
956 *      'count'            The total number of entities across all pages
957 *      'offset'           The current indexing offset
958 *      'limit'            The number of entities to display per page (default from settings)
959 *      'full_view'        Display the full view of the entities?
960 *      'list_class'       CSS class applied to the list
961 *      'item_class'       CSS class applied to the list items
962 *      'item_view'        Alternative view to render list items content
963 *      'list_item_view'   Alternative view to render list items
964 *      'pagination'       Display pagination?
965 *      'base_url'         Base URL of list (optional)
966 *      'url_fragment'     URL fragment to add to links if not present in base_url (optional)
967 *      'position'         Position of the pagination: before, after, or both
968 *      'list_type'        List type: 'list' (default), 'gallery'
969 *      'list_type_toggle' Display the list type toggle?
970 *      'no_results'       Message to display if no results (string|true|Closure)
971 *
972 * @return string The rendered list of entities
973 */
974function elgg_view_entity_list($entities, array $vars = []) {
975	$offset = (int) get_input('offset', 0);
976
977	// list type can be passed as request parameter
978	$list_type = get_input('list_type', 'list');
979
980	$defaults = [
981		'items' => $entities,
982		'list_class' => 'elgg-list-entity',
983		'full_view' => true,
984		'pagination' => true,
985		'list_type' => $list_type,
986		'list_type_toggle' => false,
987		'offset' => $offset,
988		'limit' => null,
989	];
990
991	$vars = array_merge($defaults, $vars);
992
993	if (!$vars["limit"] && !$vars["offset"]) {
994		// no need for pagination if listing is unlimited
995		$vars["pagination"] = false;
996	}
997
998	$view = "page/components/{$vars['list_type']}";
999	if (!elgg_view_exists($view)) {
1000		$view = 'page/components/list';
1001	}
1002
1003	return elgg_view($view, $vars);
1004}
1005
1006/**
1007 * Returns a rendered list of annotations, plus pagination. This function
1008 * should be called by wrapper functions.
1009 *
1010 * @param array $annotations Array of annotations
1011 * @param array $vars        Display variables
1012 *      'count'      The total number of annotations across all pages
1013 *      'offset'     The current indexing offset
1014 *      'limit'      The number of annotations to display per page
1015 *      'full_view'  Display the full view of the annotation?
1016 *      'list_class' CSS Class applied to the list
1017 *      'item_view'  Alternative view to render list items
1018 *      'offset_key' The url parameter key used for offset
1019 *      'no_results' Message to display if no results (string|true|Closure)
1020 *      'list_type'        List type: 'list' (default), 'gallery'
1021 *
1022 * @return string The list of annotations
1023 * @internal
1024 */
1025function elgg_view_annotation_list($annotations, array $vars = []) {
1026	// list type can be passed as request parameter
1027	$list_type = get_input('list_type', 'list');
1028
1029	$defaults = [
1030		'items' => $annotations,
1031		'offset' => null,
1032		'limit' => null,
1033		'list_class' => 'elgg-list-annotation elgg-annotation-list', // @todo remove elgg-annotation-list in Elgg 1.9
1034		'full_view' => true,
1035		'list_type' => $list_type,
1036		'offset_key' => 'annoff',
1037	];
1038
1039	$vars = array_merge($defaults, $vars);
1040
1041	if (!$vars["limit"] && !$vars["offset"]) {
1042		// no need for pagination if listing is unlimited
1043		$vars["pagination"] = false;
1044	}
1045
1046	$view = "page/components/{$vars['list_type']}";
1047	if (!elgg_view_exists($view)) {
1048		$view = 'page/components/list';
1049	}
1050
1051	return elgg_view($view, $vars);
1052}
1053
1054/**
1055 * Returns a rendered list of relationships, plus pagination. This function
1056 * should be called by wrapper functions.
1057 *
1058 * @param array $relationships Array of relationships
1059 * @param array $vars          Display variables
1060 *      'count'      The total number of relationships across all pages
1061 *      'offset'     The current indexing offset
1062 *      'limit'      The number of relationships to display per page
1063 *      'full_view'  Display the full view of the relationships?
1064 *      'list_class' CSS Class applied to the list
1065 *      'list_type'  List type: 'list' (default), 'gallery'
1066 *      'item_view'  Alternative view to render list items
1067 *      'offset_key' The url parameter key used for offset
1068 *      'no_results' Message to display if no results (string|true|Closure)
1069 *
1070 * @return string The list of relationships
1071 * @internal
1072 */
1073function elgg_view_relationship_list($relationships, array $vars = []) {
1074	// list type can be passed as request parameter
1075	$list_type = get_input('list_type', 'list');
1076
1077	$defaults = [
1078		'items' => $relationships,
1079		'offset' => null,
1080		'limit' => null,
1081		'list_class' => 'elgg-list-relationship',
1082		'full_view' => false,
1083		'list_type' => $list_type,
1084		'offset_key' => 'reloff',
1085	];
1086
1087	$vars = array_merge($defaults, $vars);
1088
1089	if (!$vars['limit'] && !$vars['offset']) {
1090		// no need for pagination if listing is unlimited
1091		$vars['pagination'] = false;
1092	}
1093
1094	$view = "page/components/{$vars['list_type']}";
1095	if (!elgg_view_exists($view)) {
1096		$view = 'page/components/list';
1097	}
1098
1099	return elgg_view($view, $vars);
1100}
1101
1102/**
1103 * Returns a string of a rendered relationship.
1104 *
1105 * Relationship views are expected to be in relationship/$relationship_name.
1106 * If a view is not found for $relationship_name, the default relationship/default
1107 * will be used.
1108 *
1109 * The relationship view is called with the following in $vars:
1110 *  - \ElggRelationship 'relationship' The relationship being viewed.
1111 *
1112 * @param \ElggRelationship $relationship The relationship to display
1113 * @param array             $vars         Variable array for view.
1114 *      'item_view'  Alternative view used to render a relationship
1115 *
1116 * @return string|false Rendered relationship
1117 */
1118function elgg_view_relationship(\ElggRelationship $relationship, array $vars = []) {
1119	$defaults = [
1120		'full_view' => true,
1121	];
1122
1123	$vars = array_merge($defaults, $vars);
1124	$vars['relationship'] = $relationship;
1125
1126	$name = $relationship->relationship;
1127	if (empty($name)) {
1128		return false;
1129	}
1130
1131	$relationship_views = [
1132		elgg_extract('item_view', $vars, ''),
1133		"relationship/$name",
1134		"relationship/default",
1135	];
1136
1137	$contents = '';
1138	foreach ($relationship_views as $view) {
1139		if (elgg_view_exists($view)) {
1140			$contents = elgg_view($view, $vars);
1141			break;
1142		}
1143	}
1144
1145	return $contents;
1146}
1147
1148/**
1149 * Renders a title.
1150 *
1151 * This is a shortcut for {@elgg_view page/elements/title}.
1152 *
1153 * @param string $title The page title
1154 * @param array  $vars  View variables (was submenu be displayed? (deprecated))
1155 *
1156 * @return string The HTML (etc)
1157 */
1158function elgg_view_title($title, array $vars = []) {
1159	$vars['title'] = $title;
1160
1161	return elgg_view('page/elements/title', $vars);
1162}
1163
1164/**
1165 * Displays a UNIX timestamp in a friendly way
1166 *
1167 * @see elgg_get_friendly_time()
1168 *
1169 * @param int $time A UNIX epoch timestamp
1170 *
1171 * @return string The friendly time HTML
1172 * @since 1.7.2
1173 */
1174function elgg_view_friendly_time($time) {
1175	$view = 'output/friendlytime';
1176	$vars = ['time' => $time];
1177	$viewtype = elgg_view_exists($view) ? '' : 'default';
1178
1179	return _elgg_view_under_viewtype($view, $vars, $viewtype);
1180}
1181
1182/**
1183 * Returns rendered comments and a comment form for an entity.
1184 *
1185 * @tip Plugins can override the output by registering a handler
1186 * for the comments, $entity_type hook.  The handler is responsible
1187 * for formatting the comments and the add comment form.
1188 *
1189 * @param \ElggEntity $entity      The entity to view comments of
1190 * @param bool        $add_comment Include a form to add comments?
1191 * @param array       $vars        Variables to pass to comment view
1192 *
1193 * @return string|false Rendered comments or false on failure
1194 */
1195function elgg_view_comments($entity, $add_comment = true, array $vars = []) {
1196
1197	if (!$entity instanceof \ElggEntity) {
1198		return false;
1199	}
1200
1201	$vars['entity'] = $entity;
1202	$vars['show_add_form'] = $add_comment;
1203	$vars['class'] = elgg_extract('class', $vars, "{$entity->getSubtype()}-comments");
1204
1205	$output = elgg_trigger_plugin_hook('comments', $entity->getType(), $vars, false);
1206	if ($output !== false) {
1207		return $output;
1208	}
1209
1210	return elgg_view('page/elements/comments', $vars);
1211}
1212
1213/**
1214 * Wrapper function for the image block display pattern.
1215 *
1216 * Fixed width media on the side (image, icon, flash, etc.).
1217 * Descriptive content filling the rest of the column.
1218 *
1219 * @note Use the $vars "image_alt" key to set an image on the right. If you do, you may pass
1220 *       in an empty string for $image to have only the right image.
1221 *
1222 * This is a shortcut for {@elgg_view page/components/image_block}.
1223 *
1224 * @param string $image The icon and other information
1225 * @param string $body  Description content
1226 * @param array  $vars  Additional parameters for the view
1227 *
1228 * @return string
1229 * @since 1.8.0
1230 */
1231function elgg_view_image_block($image, $body, $vars = []) {
1232	$vars['image'] = $image;
1233	$vars['body'] = $body;
1234	return elgg_view('page/components/image_block', $vars);
1235}
1236
1237/**
1238 * Wrapper function for the module display pattern.
1239 *
1240 * Box with header, body, footer
1241 *
1242 * This is a shortcut for {@elgg_view page/components/module}.
1243 *
1244 * @param string $type  The type of module (main, info, popup, aside, etc.)
1245 * @param string $title A title to put in the header
1246 * @param string $body  Content of the module
1247 * @param array  $vars  Additional parameters for the module
1248 *
1249 * @return string
1250 * @since 1.8.0
1251 */
1252function elgg_view_module($type, $title, $body, array $vars = []) {
1253	$vars['type'] = $type;
1254	$vars['title'] = $title;
1255	$vars['body'] = $body;
1256	return elgg_view('page/components/module', $vars);
1257}
1258
1259/**
1260 * Wrapper function for the message display pattern.
1261 *
1262 * Box with header, body
1263 *
1264 * This is a shortcut for {@elgg_view page/components/message}.
1265 *
1266 * @param string $type The type of message (error, success, warning, help, notice)
1267 * @param string $body Content of the message
1268 * @param array  $vars Additional parameters for the message
1269 *
1270 * @return string
1271 * @since 3.0.0
1272 */
1273function elgg_view_message($type, $body, array $vars = []) {
1274	$vars['type'] = $type;
1275	$vars['body'] = $body;
1276	return elgg_view('page/components/message', $vars);
1277}
1278
1279/**
1280 * Renders a human-readable representation of a river item
1281 *
1282 * @param \ElggRiverItem $item A river item object
1283 * @param array          $vars An array of variables for the view
1284 *      'item_view'         Alternative view to render the item
1285 *      'register_rss_link' Register the rss link availability (default: false)
1286 * @return string returns empty string if could not be rendered
1287 */
1288function elgg_view_river_item($item, array $vars = []) {
1289
1290	if (!($item instanceof \ElggRiverItem)) {
1291		return '';
1292	}
1293
1294	// checking default viewtype since some viewtypes do not have unique views per item (rss)
1295	$view = $item->getView();
1296
1297	$subject = $item->getSubjectEntity();
1298	$object = $item->getObjectEntity();
1299	if (!$subject || !$object) {
1300		// subject is disabled or subject/object deleted
1301		return '';
1302	}
1303
1304	if (elgg_extract('register_rss_link', $vars)) {
1305		elgg_register_rss_link();
1306	}
1307
1308	$vars['item'] = $item;
1309
1310	// create river view logic
1311	$type = $object->getType();
1312	$subtype = $object->getSubtype();
1313	$action = $item->action_type;
1314
1315	$river_views = [
1316		elgg_extract('item_view', $vars, ''),
1317		'river/item', // important for other viewtypes, e.g. "rss"
1318		$view,
1319		"river/{$type}/{$subtype}/{$action}",
1320		"river/{$type}/{$subtype}/default",
1321		"river/{$type}/{$action}",
1322		"river/{$type}/default",
1323		'river/elements/layout',
1324	];
1325
1326	$contents = '';
1327	foreach ($river_views as $view) {
1328		if (elgg_view_exists($view)) {
1329			$contents = elgg_view($view, $vars);
1330			break;
1331		}
1332	}
1333
1334	return $contents;
1335}
1336
1337/**
1338 * Convenience function for generating a form from a view in a standard location.
1339 *
1340 * This function assumes that the body of the form is located at "forms/$action" and
1341 * sets the action by default to "action/$action".  Automatically wraps the forms/$action
1342 * view with a <form> tag and inserts the anti-csrf security tokens.
1343 *
1344 * @tip This automatically appends elgg-form-action-name to the form's class. It replaces any
1345 * slashes with dashes (blog/save becomes elgg-form-blog-save)
1346 *
1347 * @example
1348 * <code>echo elgg_view_form('login');</code>
1349 *
1350 * This would assume a "login" form body to be at "forms/login" and would set the action
1351 * of the form to "http://yoursite.com/action/login".
1352 *
1353 * If elgg_view('forms/login') is:
1354 * <input type="text" name="username" />
1355 * <input type="password" name="password" />
1356 *
1357 * Then elgg_view_form('login') generates:
1358 * <form action="http://yoursite.com/action/login" method="post">
1359 *     ...security tokens...
1360 *     <input type="text" name="username" />
1361 *     <input type="password" name="password" />
1362 * </form>
1363 *
1364 * @param string $action    The name of the action. An action name does not include
1365 *                          the leading "action/". For example, "login" is an action name.
1366 * @param array  $form_vars $vars environment passed to the "input/form" view
1367 *                           - 'ajax' bool If true, the form will be submitted with an Ajax request
1368 * @param array  $body_vars $vars environment passed to the "forms/$action" view
1369 *
1370 * @return string The complete form
1371 */
1372function elgg_view_form($action, $form_vars = [], $body_vars = []) {
1373	return _elgg_services()->forms->render($action, $form_vars, $body_vars);
1374}
1375
1376/**
1377 * Sets form footer and defers its rendering until the form view and extensions have been rendered.
1378 * Deferring footer rendering allows plugins to extend the form view while maintaining
1379 * logical DOM structure.
1380 * Footer will be rendered using 'elements/forms/footer' view after form body has finished rendering
1381 *
1382 * @param string $footer Footer
1383 * @return bool
1384 */
1385function elgg_set_form_footer($footer = '') {
1386	return _elgg_services()->forms->setFooter($footer);
1387}
1388
1389/**
1390 * Returns currently set footer, or false if not in the form rendering stack
1391 * @return string|false
1392 */
1393function elgg_get_form_footer() {
1394	return _elgg_services()->forms->getFooter();
1395}
1396
1397/**
1398 * Split array of vars into subarrays based on property prefixes
1399 *
1400 * @see elgg_view_field()
1401 *
1402 * @param array $vars     Vars to split
1403 * @param array $prefixes Prefixes to split
1404 *
1405 * @return array
1406 */
1407function _elgg_split_vars(array $vars = [], array $prefixes = null) {
1408
1409	if (!isset($prefixes)) {
1410		$prefixes = ['#'];
1411	}
1412
1413	$return = [];
1414
1415	foreach ($vars as $key => $value) {
1416		foreach ($prefixes as $prefix) {
1417			if (substr($key, 0, 1) === $prefix) {
1418				$key = substr($key, 1);
1419				$return[$prefix][$key] = $value;
1420				break;
1421			} else {
1422				$return[''][$key] = $value;
1423			}
1424		}
1425	}
1426
1427	return $return;
1428}
1429
1430/**
1431 * Renders a form field, usually with a wrapper element, a label, help text, etc.
1432 *
1433 * @param array $params Field parameters and variables for the input view.
1434 *                      Keys not prefixed with hash (#) are passed to the input view as $vars.
1435 *                      Keys prefixed with a hash specify the field wrapper (.elgg-view-field) output.
1436 *                       - #type: specifies input view. E.g. "text" uses the view "input/text".
1437 *                       - #label: field label HTML
1438 *                       - #help: field help HTML
1439 *                       - #class: field class name
1440 *                       - #view: custom view to use to render the field
1441 *                       - #html: can be used to render custom HTML instead of in put field, helpful when you need to add a help paragraph or similar
1442 *                      Note: Both #label and #help are printed unescaped within their wrapper element.
1443 *                      Note: Some fields (like input/checkbox) need special attention because #label and label serve different purposes
1444 *                      "#label" will be used as a label in the field wrapper but "label" will be used in the input view
1445 *
1446 * @return string
1447 * @since 2.3
1448 */
1449function elgg_view_field(array $params = []) {
1450
1451	if (!empty($params['#html'])) {
1452		return $params['#html'];
1453	}
1454
1455	if (empty($params['#type'])) {
1456		_elgg_services()->logger->error(__FUNCTION__ . '(): $params["#type"] is required.');
1457		return '';
1458	}
1459
1460	$input_type = $params['#type'];
1461	if (!elgg_view_exists("input/$input_type")) {
1462		return '';
1463	}
1464
1465	$hidden_types = ['hidden', 'securitytoken'];
1466	if (in_array($input_type, $hidden_types)) {
1467		$params = _elgg_split_vars($params);
1468		return elgg_view("input/$input_type", $params['']);
1469	}
1470
1471	$id = elgg_extract('id', $params);
1472	if (!$id) {
1473		$id = "elgg-field-" . base_convert(mt_rand(), 10, 36);
1474		$params['id'] = $id;
1475	}
1476
1477	$make_special_checkbox_label = false;
1478	if ($input_type == 'checkbox' && (isset($params['label']) || isset($params['#label']))) {
1479		if (isset($params['#label']) && isset($params['label'])) {
1480			$params['label_tag'] = 'div';
1481		} else {
1482			$label = elgg_extract('label', $params);
1483			$label = elgg_extract('#label', $params, $label);
1484
1485			$params['#label'] = $label;
1486			unset($params['label']);
1487
1488			// Single checkbox input view gets special treatment
1489			// We don't want the field label to appear a checkbox without a label
1490			$make_special_checkbox_label = true;
1491		}
1492	}
1493
1494	// Need to set defaults to prevent input keys with same name ending up in element vars if not provided
1495	$defaults = [
1496		'#class' => ELGG_ENTITIES_ANY_VALUE,
1497		'#help' => ELGG_ENTITIES_ANY_VALUE,
1498		'#label' => ELGG_ENTITIES_ANY_VALUE,
1499		'#view' => ELGG_ENTITIES_ANY_VALUE,
1500	];
1501	$params = array_merge($defaults, $params);
1502
1503	// first pass non-hash keys into both
1504	$split_params = _elgg_split_vars($params);
1505
1506	// $vars passed to input/$input_name
1507	$input_vars = $split_params[''];
1508
1509	// $vars passed to label, help and field wrapper views
1510	$element_vars = array_merge($split_params[''], $split_params['#']);
1511
1512	// field input view needs this
1513	$input_vars['input_type'] = $input_type;
1514
1515	// field views get more data
1516	$element_vars['input_type'] = $input_type;
1517
1518	// wrap if present
1519	$element_vars['label'] = elgg_view('elements/forms/label', $element_vars);
1520	$element_vars['help'] = elgg_view('elements/forms/help', $element_vars);
1521
1522	if ($make_special_checkbox_label) {
1523		$input_vars['label'] = $element_vars['label'];
1524		$input_vars['label_tag'] = 'div';
1525		unset($element_vars['label']);
1526	}
1527	$element_vars['input'] = elgg_view("elements/forms/input", $input_vars);
1528
1529	return elgg_view('elements/forms/field', $element_vars);
1530}
1531
1532/**
1533 * Create a tagcloud for viewing
1534 *
1535 *
1536 * @param array $options Any elgg_get_tags() options except:
1537 *
1538 * 	type => must be single entity type
1539 *
1540 * 	subtype => must be single entity subtype
1541 *
1542 * @return string
1543 *
1544 * @see elgg_get_tags()
1545 * @since 1.7.1
1546 */
1547function elgg_view_tagcloud(array $options = []) {
1548
1549	$type = $subtype = '';
1550	if (isset($options['type'])) {
1551		$type = $options['type'];
1552	}
1553	if (isset($options['subtype'])) {
1554		$subtype = $options['subtype'];
1555	}
1556
1557	$tag_data = elgg_get_tags($options);
1558	return elgg_view("output/tagcloud", [
1559		'value' => $tag_data,
1560		'type' => $type,
1561		'subtype' => $subtype,
1562	]);
1563}
1564
1565/**
1566 * View an item in a list
1567 *
1568 * @param mixed $item Entity, annotation, river item, or other data
1569 * @param array $vars Additional parameters for the rendering
1570 *                    'item_view' - Alternative view used to render list items
1571 *                                  This parameter is required if rendering
1572 *                                  list items that are not entity, annotation or river
1573 * @return false|string
1574 * @since 1.8.0
1575 * @internal
1576 */
1577function elgg_view_list_item($item, array $vars = []) {
1578
1579	if ($item instanceof \ElggEntity) {
1580		return elgg_view_entity($item, $vars);
1581	} else if ($item instanceof \ElggAnnotation) {
1582		return elgg_view_annotation($item, $vars);
1583	} else if ($item instanceof \ElggRiverItem) {
1584		return elgg_view_river_item($item, $vars);
1585	} else if ($item instanceof ElggRelationship) {
1586		return elgg_view_relationship($item, $vars);
1587	}
1588
1589	$view = elgg_extract('item_view', $vars);
1590	if ($view && elgg_view_exists($view)) {
1591		$vars['item'] = $item;
1592		return elgg_view($view, $vars);
1593	}
1594
1595	return '';
1596}
1597
1598/**
1599 * View an icon glyph
1600 *
1601 * @param string $name The specific icon to display
1602 * @param mixed  $vars The additional classname as a string ('float', 'float-alt' or a custom class)
1603 *                     or an array of variables (array('class' => 'float')) to pass to the icon view.
1604 *
1605 * @return string The html for displaying an icon
1606 * @throws InvalidArgumentException
1607 */
1608function elgg_view_icon($name, $vars = []) {
1609	if (empty($vars)) {
1610		$vars = [];
1611	}
1612
1613	if (is_string($vars)) {
1614		$vars = ['class' => $vars];
1615	}
1616
1617	if (!is_array($vars)) {
1618		throw new \InvalidArgumentException('$vars needs to be a string or an array');
1619	}
1620
1621	$vars['class'] = elgg_extract_class($vars, "elgg-icon-$name");
1622
1623	return elgg_view("output/icon", $vars);
1624}
1625
1626/**
1627 * Include the RSS icon link and link element in the head
1628 *
1629 * @return void
1630 */
1631function elgg_register_rss_link() {
1632	_elgg_config()->_elgg_autofeed = true;
1633}
1634
1635/**
1636 * Remove the RSS icon link and link element from the head
1637 *
1638 * @return void
1639 */
1640function elgg_unregister_rss_link() {
1641	_elgg_config()->_elgg_autofeed = false;
1642}
1643
1644/**
1645 * Should the RSS view of this URL be linked to?
1646 *
1647 * @return bool
1648 * @internal
1649 */
1650function _elgg_has_rss_link() {
1651	if (_elgg_config()->disable_rss) {
1652		return false;
1653	}
1654
1655	return (bool) _elgg_config()->_elgg_autofeed;
1656}
1657
1658/**
1659 * Minifies simplecache CSS and JS views by handling the "simplecache:generate" hook
1660 *
1661 * @param \Elgg\Hook $hook 'simplecache:generate', 'css'
1662 *
1663 * @return string|null View content minified (if css/js type)
1664 * @internal
1665 */
1666function _elgg_views_minify(\Elgg\Hook $hook) {
1667	if (preg_match('~[\.-]min\.~', $hook->getParam('view'))) {
1668		// bypass minification
1669		return;
1670	}
1671
1672	$content = $hook->getValue();
1673
1674	if ($hook->getType() === 'js') {
1675		if (_elgg_config()->simplecache_minify_js) {
1676			return JSMin::minify($content);
1677		}
1678	} elseif ($hook->getType() === 'css') {
1679		if (_elgg_config()->simplecache_minify_css) {
1680			$cssmin = new CSSmin();
1681			return $cssmin->run($content);
1682		}
1683	}
1684}
1685
1686/**
1687 * Preprocesses CSS views sent by /cache URLs
1688 *
1689 * @param \Elgg\Hook $hook 'cache:generate' | 'simplecache:generate', 'css'
1690 *
1691 * @return string|null View content
1692 * @internal
1693 */
1694function _elgg_views_preprocess_css(\Elgg\Hook $hook) {
1695	$options = $hook->getParam('compiler_options', []);
1696	return _elgg_services()->cssCompiler->compile($hook->getValue(), $options);
1697}
1698
1699/**
1700 * Inserts module names into anonymous modules by handling the "simplecache:generate" and "cache:generate" hook.
1701 *
1702 * @param \Elgg\Hook $hook 'cache:generate' | 'simplecache:generate', 'js'
1703 *
1704 * @return string|null View content minified (if css/js type)
1705 * @internal
1706 */
1707function _elgg_views_amd(\Elgg\Hook $hook) {
1708	$filter = new \Elgg\Amd\ViewFilter();
1709	return $filter->filter($hook->getParam('view'), $hook->getValue());
1710}
1711
1712/**
1713 * Sends X-Frame-Options header on page requests
1714 *
1715 * @return void
1716 *
1717 * @internal
1718 */
1719function _elgg_views_send_header_x_frame_options() {
1720	elgg_set_http_header('X-Frame-Options: SAMEORIGIN');
1721}
1722
1723/**
1724 * Initialize viewtypes on system boot event
1725 * This ensures simplecache is cleared during upgrades. See #2252
1726 *
1727 * @return void
1728 * @internal
1729 * @elgg_event_handler boot system
1730 */
1731function elgg_views_boot() {
1732	_elgg_services()->viewCacher->registerCoreViews();
1733
1734	// jQuery and UI must come before require. See #9024
1735	elgg_register_external_file('js', 'jquery', elgg_get_simplecache_url('jquery.js'), 'head');
1736	elgg_load_external_file('js', 'jquery');
1737
1738	elgg_register_external_file('js', 'jquery-ui', elgg_get_simplecache_url('jquery-ui.js'), 'head');
1739	elgg_load_external_file('js', 'jquery-ui');
1740
1741	elgg_register_external_file('js', 'elgg.require_config', elgg_get_simplecache_url('elgg/require_config.js'), 'head');
1742	elgg_load_external_file('js', 'elgg.require_config');
1743
1744	elgg_register_external_file('js', 'require', elgg_get_simplecache_url('require.js'), 'head');
1745	elgg_load_external_file('js', 'require');
1746
1747	elgg_register_external_file('js', 'elgg', elgg_get_simplecache_url('elgg.js'), 'head');
1748	elgg_load_external_file('js', 'elgg');
1749
1750	elgg_register_external_file('css', 'font-awesome', elgg_get_simplecache_url('font-awesome/css/all.min.css'));
1751	elgg_load_external_file('css', 'font-awesome');
1752
1753	elgg_define_js('cropperjs', [
1754		'src' => elgg_get_simplecache_url('cropperjs/cropper.min.js'),
1755	]);
1756	elgg_define_js('jquery-cropper/jquery-cropper', [
1757		'src' => elgg_get_simplecache_url('jquery-cropper/jquery-cropper.min.js'),
1758	]);
1759
1760	elgg_require_css('elgg');
1761
1762	elgg_register_simplecache_view('elgg/init.js');
1763
1764	elgg_extend_view('initialize_elgg.js', 'elgg/prevent_clicks.js', 1);
1765
1766	elgg_extend_view('elgg.css', 'lightbox/elgg-colorbox-theme/colorbox.css');
1767	elgg_extend_view('elgg.css', 'entity/edit/icon/crop.css');
1768
1769	elgg_define_js('jquery.ui.autocomplete.html', [
1770		'deps' => ['jquery-ui'],
1771	]);
1772
1773	// @deprecated 3.1
1774	elgg_register_external_file('js', 'elgg.avatar_cropper', elgg_get_simplecache_url('elgg/ui.avatar_cropper.js'));
1775
1776	// @deprecated 2.2
1777	elgg_register_external_file('js', 'elgg.ui.river', elgg_get_simplecache_url('elgg/ui.river.js'));
1778
1779	// @deprecated 3.1 no longer use imageareaselect js and css
1780	elgg_register_external_file('js', 'jquery.imgareaselect', elgg_get_simplecache_url('jquery.imgareaselect.js'));
1781	elgg_register_external_file('css', 'jquery.imgareaselect', elgg_get_simplecache_url('jquery.imgareaselect.css'));
1782
1783	// @deprecated 3.1 no longer use treeview js and css
1784	elgg_register_external_file('css', 'jquery.treeview', elgg_get_simplecache_url('jquery-treeview/jquery.treeview.css'));
1785	elgg_define_js('jquery.treeview', [
1786		'src' => elgg_get_simplecache_url('jquery-treeview/jquery.treeview.js'),
1787		'exports' => 'jQuery.fn.treeview',
1788		'deps' => ['jquery'],
1789	]);
1790
1791	elgg_register_ajax_view('languages.js');
1792
1793	// pre-process CSS regardless of simplecache
1794	elgg_register_plugin_hook_handler('cache:generate', 'css', '_elgg_views_preprocess_css');
1795	elgg_register_plugin_hook_handler('simplecache:generate', 'css', '_elgg_views_preprocess_css');
1796
1797	elgg_register_plugin_hook_handler('simplecache:generate', 'js', '_elgg_views_amd');
1798	elgg_register_plugin_hook_handler('cache:generate', 'js', '_elgg_views_amd');
1799	elgg_register_plugin_hook_handler('simplecache:generate', 'css', '_elgg_views_minify');
1800	elgg_register_plugin_hook_handler('simplecache:generate', 'js', '_elgg_views_minify');
1801
1802	elgg_register_plugin_hook_handler('output:before', 'page', '_elgg_views_send_header_x_frame_options');
1803
1804	elgg_register_plugin_hook_handler('view_vars', 'elements/forms/help', '_elgg_views_file_help_upload_limit');
1805
1806	// registered with high priority for BC
1807	// prior to 2.2 registration used to take place in _elgg_views_prepare_head() before the hook was triggered
1808	elgg_register_plugin_hook_handler('head', 'page', '_elgg_views_prepare_favicon_links', 1);
1809
1810	// set default icon sizes - can be overridden with plugin
1811	if (!_elgg_config()->hasValue('icon_sizes')) {
1812		$icon_sizes = [
1813			'topbar' => ['w' => 16, 'h' => 16, 'square' => true, 'upscale' => true],
1814			'tiny' => ['w' => 25, 'h' => 25, 'square' => true, 'upscale' => true],
1815			'small' => ['w' => 40, 'h' => 40, 'square' => true, 'upscale' => true],
1816			'medium' => ['w' => 100, 'h' => 100, 'square' => true, 'upscale' => true],
1817			'large' => ['w' => 200, 'h' => 200, 'square' => true, 'upscale' => true],
1818			'master' => ['w' => 10240, 'h' => 10240, 'square' => false, 'upscale' => false, 'crop' => false],
1819		];
1820		elgg_set_config('icon_sizes', $icon_sizes);
1821	}
1822
1823	// Configure lightbox
1824	elgg_register_plugin_hook_handler('elgg.data', 'site', '_elgg_set_lightbox_config');
1825}
1826
1827/**
1828 * Get the site data to be merged into "elgg" in elgg.js.
1829 *
1830 * Unlike _elgg_get_js_page_data(), the keys returned are literal expressions.
1831 *
1832 * @return array
1833 * @internal
1834 */
1835function _elgg_get_js_site_data() {
1836	$language = _elgg_config()->language;
1837	if (!$language) {
1838		$language = 'en';
1839	}
1840
1841	return [
1842		'elgg.data' => (object) elgg_trigger_plugin_hook('elgg.data', 'site', null, []),
1843		'elgg.version' => elgg_get_version(),
1844		'elgg.release' => elgg_get_version(true),
1845		'elgg.config.wwwroot' => elgg_get_site_url(),
1846
1847		// refresh token 3 times during its lifetime (in microseconds 1000 * 1/3)
1848		'elgg.security.interval' => (int) elgg()->csrf->getActionTokenTimeout() * 333,
1849		'elgg.config.language' => $language,
1850	];
1851}
1852
1853/**
1854 * Get the initial contents of "elgg" client side. Will be extended by elgg.js.
1855 *
1856 * @return array
1857 * @internal
1858 */
1859function _elgg_get_js_page_data() {
1860	$data = elgg_trigger_plugin_hook('elgg.data', 'page', null, []);
1861	if (!is_array($data)) {
1862		elgg_log('"elgg.data" plugin hook handlers must return an array. Returned ' . gettype($data) . '.', 'ERROR');
1863		$data = [];
1864	}
1865
1866	$elgg = [
1867		'config' => [
1868			'lastcache' => (int) _elgg_config()->lastcache,
1869			'viewtype' => elgg_get_viewtype(),
1870			'simplecache_enabled' => (int) elgg_is_simplecache_enabled(),
1871			'current_language' => get_current_language(),
1872		],
1873		'security' => [
1874			'token' => [
1875				'__elgg_ts' => $ts = elgg()->csrf->getCurrentTime()->getTimestamp(),
1876				'__elgg_token' => elgg()->csrf->generateActionToken($ts),
1877			],
1878		],
1879		'session' => [
1880			'user' => null,
1881			'token' => _elgg_services()->session->get('__elgg_session'),
1882		],
1883		'_data' => (object) $data,
1884	];
1885
1886	if (_elgg_config()->elgg_load_sync_code) {
1887		$elgg['config']['load_sync_code'] = true;
1888	}
1889
1890	$page_owner = elgg_get_page_owner_entity();
1891	if ($page_owner instanceof ElggEntity) {
1892		$elgg['page_owner'] = $page_owner->toObject();
1893	}
1894
1895	$user = elgg_get_logged_in_user_entity();
1896	if ($user instanceof ElggUser) {
1897		$user_object = $user->toObject();
1898		$user_object->admin = $user->isAdmin();
1899		$elgg['session']['user'] = $user_object;
1900	}
1901
1902	return $elgg;
1903}
1904
1905/**
1906 * Render a view while the global viewtype is temporarily changed. This makes sure that
1907 * nested views use the same viewtype.
1908 *
1909 * @param string $view     View name
1910 * @param array  $vars     View vars
1911 * @param string $viewtype Temporary viewtype ('' to leave current)
1912 *
1913 * @return mixed
1914 * @internal
1915 */
1916function _elgg_view_under_viewtype($view, $vars, $viewtype) {
1917	$current_view_type = null;
1918	if ($viewtype) {
1919		$current_view_type = elgg_get_viewtype();
1920		elgg_set_viewtype($viewtype);
1921	}
1922
1923	$ret = elgg_view($view, $vars);
1924
1925	if (isset($current_view_type)) {
1926		elgg_set_viewtype($current_view_type);
1927	}
1928
1929	return $ret;
1930}
1931
1932/**
1933 * Set lightbox config
1934 *
1935 * @param \Elgg\Hook $hook "elgg.data", "site"
1936 *
1937 * @return array
1938 * @internal
1939 */
1940function _elgg_set_lightbox_config(\Elgg\Hook $hook) {
1941	$return = $hook->getValue();
1942
1943	$return['lightbox'] = [
1944		'current' => elgg_echo('js:lightbox:current', ['{current}', '{total}']),
1945		'previous' => elgg_view_icon('caret-left'),
1946		'next' => elgg_view_icon('caret-right'),
1947		'close' => elgg_view_icon('times'),
1948		'opacity' => 0.5,
1949		'maxWidth' => '990px',
1950		'maxHeight' => '990px',
1951		'initialWidth' => '300px',
1952		'initialHeight' => '300px',
1953	];
1954
1955	return $return;
1956}
1957
1958/**
1959 * Add a help text to input/file about upload limit
1960 *
1961 * In order to not show the help text supply 'show_upload_limit' => false to elgg_view_field()
1962 *
1963 * @param \Elgg\Hook $hook 'view_vars' 'elements/forms/help'
1964 *
1965 * @return void|array
1966 * @internal
1967 */
1968function _elgg_views_file_help_upload_limit(\Elgg\Hook $hook) {
1969
1970	$return = $hook->getValue();
1971	if (elgg_extract('input_type', $return) !== 'file') {
1972		return;
1973	}
1974
1975	if (!elgg_extract('show_upload_limit', $return, true)) {
1976		return;
1977	}
1978
1979	$help = elgg_extract('help', $return, '');
1980
1981	// Get post_max_size and upload_max_filesize
1982	$post_max_size = elgg_get_ini_setting_in_bytes('post_max_size');
1983	$upload_max_filesize = elgg_get_ini_setting_in_bytes('upload_max_filesize');
1984
1985	// Determine the correct value
1986	$max_upload = $upload_max_filesize > $post_max_size ? $post_max_size : $upload_max_filesize;
1987
1988	$help .= ' ' . elgg_echo('input:file:upload_limit', [elgg_format_bytes($max_upload)]);
1989
1990	$return['help'] = trim($help);
1991
1992	return $return;
1993}
1994
1995/**
1996 * Maps legacy sprite classes and FontAwesome 4 classes to FontAwesome 5 classes
1997 *
1998 * @param array $classes     Icon classes
1999 * @param bool  $map_sprites Map legacy Elgg sprites
2000 *
2001 * @return array
2002 * @internal
2003 */
2004function _elgg_map_icon_glyph_class(array $classes, $map_sprites = true) {
2005
2006	// these 'old' Elgg 1.x sprite icons will be converted to the FontAwesome version
2007	$legacy_sprites = [
2008		"arrow-two-head" => "arrows-h",
2009		"attention" => "exclamation-triangle",
2010		"cell-phone" => "mobile",
2011		"checkmark" => "check",
2012		"clip" => "paperclip",
2013		"cursor-drag-arrow" => "arrows",
2014		"drag-arrow" => "arrows", // 'old' admin sprite
2015		"delete-alt" => "times-circle",
2016		"delete" => "times",
2017		"facebook" => "facebook-square",
2018		"grid" => "th",
2019		"hover-menu" => "caret-down",
2020		"info" => "info-circle",
2021		"lock-closed" => "lock",
2022		"lock-open" => "unlock",
2023		"mail" => "envelope-o",
2024		"mail-alt" => "envelope",
2025		"print-alt" => "print",
2026		"push-pin" => "thumb-tack",
2027		"push-pin-alt" => "thumb-tack",
2028		"redo" => "share",
2029		"round-arrow-left" => "arrow-circle-left",
2030		"round-arrow-right" => "arrow-circle-right",
2031		"round-checkmark" => "check-circle",
2032		"round-minus" => "minus-circle",
2033		"round-plus" => "plus-circle",
2034		"rss" => "rss-square",
2035		"search-focus" => "search",
2036		"settings" => "wrench",
2037		"settings-alt" => "cog",
2038		"share" => "share-alt-square",
2039		"shop-cart" => "shopping-cart",
2040		"speech-bubble" => "comment",
2041		"speech-bubble-alt" => "comments",
2042		"star-alt" => "star",
2043		"star-empty" => "star-o",
2044		"thumbs-down-alt" => "thumbs-down",
2045		"thumbs-up-alt" => "thumbs-up",
2046		"trash" => "trash-o",
2047		"twitter" => "twitter-square",
2048		"undo" => "reply",
2049		"video" => "film"
2050	];
2051
2052	$fa5 = [
2053		'address-book-o' => ['address-book', 'far'],
2054		'address-card-o' => ['address-card', 'far'],
2055		'area-chart' => ['chart-area', 'fas'],
2056		'arrow-circle-o-down' => ['arrow-alt-circle-down', 'far'],
2057		'arrow-circle-o-left' => ['arrow-alt-circle-left', 'far'],
2058		'arrow-circle-o-right' => ['arrow-alt-circle-right', 'far'],
2059		'arrow-circle-o-up' => ['arrow-alt-circle-up', 'far'],
2060		'arrows-alt' => ['expand-arrows-alt', 'fas'],
2061		'arrows-h' => ['arrows-alt-h', 'fas'],
2062		'arrows-v' => ['arrows-alt-v', 'fas'],
2063		'arrows' => ['arrows-alt', 'fas'],
2064		'asl-interpreting' => ['american-sign-language-interpreting', 'fas'],
2065		'automobile' => ['car', 'fas'],
2066		'bank' => ['university', 'fas'],
2067		'bar-chart-o' => ['chart-bar', 'far'],
2068		'bar-chart' => ['chart-bar', 'far'],
2069		'bathtub' => ['bath', 'fas'],
2070		'battery-0' => ['battery-empty', 'fas'],
2071		'battery-1' => ['battery-quarter', 'fas'],
2072		'battery-2' => ['battery-half', 'fas'],
2073		'battery-3' => ['battery-three-quarters', 'fas'],
2074		'battery-4' => ['battery-full', 'fas'],
2075		'battery' => ['battery-full', 'fas'],
2076		'bell-o' => ['bell', 'far'],
2077		'bell-slash-o' => ['bell-slash', 'far'],
2078		'bitbucket-square' => ['bitbucket', 'fab'],
2079		'bitcoin' => ['btc', 'fab'],
2080		'bookmark-o' => ['bookmark', 'far'],
2081		'building-o' => ['building', 'far'],
2082		'cab' => ['taxi', 'fas'],
2083		'calendar-check-o' => ['calendar-check', 'far'],
2084		'calendar-minus-o' => ['calendar-minus', 'far'],
2085		'calendar-o' => ['calendar', 'far'],
2086		'calendar-plus-o' => ['calendar-plus', 'far'],
2087		'calendar-times-o' => ['calendar-times', 'far'],
2088		'calendar' => ['calendar-alt', 'fas'],
2089		'caret-square-o-down' => ['caret-square-down', 'far'],
2090		'caret-square-o-left' => ['caret-square-left', 'far'],
2091		'caret-square-o-right' => ['caret-square-right', 'far'],
2092		'caret-square-o-up' => ['caret-square-up', 'far'],
2093		'cc' => ['closed-captioning', 'far'],
2094		'chain-broken' => ['unlink', 'fas'],
2095		'chain' => ['link', 'fas'],
2096		'check-circle-o' => ['check-circle', 'far'],
2097		'check-square-o' => ['check-square', 'far'],
2098		'circle-o-notch' => ['circle-notch', 'fas'],
2099		'circle-o' => ['circle', 'far'],
2100		'circle-thin' => ['circle', 'far'],
2101		'clock-o' => ['clock', 'far'],
2102		'close' => ['times', 'fas'],
2103		'cloud-download' => ['cloud-download-alt', 'fas'],
2104		'cloud-upload' => ['cloud-upload-alt', 'fas'],
2105		'cny' => ['yen-sign', 'fas'],
2106		'code-fork' => ['code-branch', 'fas'],
2107		'comment-o' => ['comment', 'far'],
2108		'commenting-o' => ['comment-alt', 'far'],
2109		'commenting' => ['comment-alt', 'fas'],
2110		'comments-o' => ['comments', 'far'],
2111		'credit-card-alt' => ['credit-card', 'fas'],
2112		'cutlery' => ['utensils', 'fas'],
2113		'dashboard' => ['tachometer-alt', 'fas'],
2114		'deafness' => ['deaf', 'fas'],
2115		'dedent' => ['outdent', 'fas'],
2116		'diamond' => ['gem', 'far'],
2117		'dollar' => ['dollar-sign', 'fas'],
2118		'dot-circle-o' => ['dot-circle', 'far'],
2119		'drivers-license-o' => ['id-card', 'far'],
2120		'drivers-license' => ['id-card', 'fas'],
2121		'eercast' => ['sellcast', 'fab'],
2122		'envelope-o' => ['envelope', 'far'],
2123		'envelope-open-o' => ['envelope-open', 'far'],
2124		'eur' => ['euro-sign', 'fas'],
2125		'euro' => ['euro-sign', 'fas'],
2126		'exchange' => ['exchange-alt', 'fas'],
2127		'external-link-square' => ['external-link-square-alt', 'fas'],
2128		'external-link' => ['external-link-alt', 'fas'],
2129		'eyedropper' => ['eye-dropper', 'fas'],
2130		'fa' => ['font-awesome', 'fab'],
2131		'facebook-f' => ['facebook-f', 'fab'],
2132		'facebook-official' => ['facebook', 'fab'],
2133		'facebook' => ['facebook-f', 'fab'],
2134		'feed' => ['rss', 'fas'],
2135		'file-archive-o' => ['file-archive', 'far'],
2136		'file-audio-o' => ['file-audio', 'far'],
2137		'file-code-o' => ['file-code', 'far'],
2138		'file-excel-o' => ['file-excel', 'far'],
2139		'file-image-o' => ['file-image', 'far'],
2140		'file-movie-o' => ['file-video', 'far'],
2141		'file-o' => ['file', 'far'],
2142		'file-pdf-o' => ['file-pdf', 'far'],
2143		'file-photo-o' => ['file-image', 'far'],
2144		'file-picture-o' => ['file-image', 'far'],
2145		'file-powerpoint-o' => ['file-powerpoint', 'far'],
2146		'file-sound-o' => ['file-audio', 'far'],
2147		'file-text-o' => ['file-alt', 'far'],
2148		'file-text' => ['file-alt', 'fas'],
2149		'file-video-o' => ['file-video', 'far'],
2150		'file-word-o' => ['file-word', 'far'],
2151		'file-zip-o' => ['file-archive', 'far'],
2152		'files-o' => ['copy', 'far'],
2153		'flag-o' => ['flag', 'far'],
2154		'flash' => ['bolt', 'fas'],
2155		'floppy-o' => ['save', 'far'],
2156		'folder-o' => ['folder', 'far'],
2157		'folder-open-o' => ['folder-open', 'far'],
2158		'frown-o' => ['frown', 'far'],
2159		'futbol-o' => ['futbol', 'far'],
2160		'gbp' => ['pound-sign', 'fas'],
2161		'ge' => ['empire', 'fab'],
2162		'gear' => ['cog', 'fas'],
2163		'gears' => ['cogs', 'fas'],
2164		'gittip' => ['gratipay', 'fab'],
2165		'glass' => ['glass-martini', 'fas'],
2166		'google-plus-circle' => ['google-plus', 'fab'],
2167		'google-plus-official' => ['google-plus', 'fab'],
2168		'google-plus' => ['google-plus-g', 'fab'],
2169		'group' => ['users', 'fas'],
2170		'hand-grab-o' => ['hand-rock', 'far'],
2171		'hand-lizard-o' => ['hand-lizard', 'far'],
2172		'hand-o-down' => ['hand-point-down', 'far'],
2173		'hand-o-left' => ['hand-point-left', 'far'],
2174		'hand-o-right' => ['hand-point-right', 'far'],
2175		'hand-o-up' => ['hand-point-up', 'far'],
2176		'hand-paper-o' => ['hand-paper', 'far'],
2177		'hand-peace-o' => ['hand-peace', 'far'],
2178		'hand-pointer-o' => ['hand-pointer', 'far'],
2179		'hand-rock-o' => ['hand-rock', 'far'],
2180		'hand-scissors-o' => ['hand-scissors', 'far'],
2181		'hand-spock-o' => ['hand-spock', 'far'],
2182		'hand-stop-o' => ['hand-paper', 'far'],
2183		'handshake-o' => ['handshake', 'far'],
2184		'hard-of-hearing' => ['deaf', 'fas'],
2185		'hdd-o' => ['hdd', 'far'],
2186		'header' => ['heading', 'fas'],
2187		'heart-o' => ['heart', 'far'],
2188		'hospital-o' => ['hospital', 'far'],
2189		'hotel' => ['bed', 'fas'],
2190		'hourglass-1' => ['hourglass-start', 'fas'],
2191		'hourglass-2' => ['hourglass-half', 'fas'],
2192		'hourglass-3' => ['hourglass-end', 'fas'],
2193		'hourglass-o' => ['hourglass', 'far'],
2194		'id-card-o' => ['id-card', 'far'],
2195		'ils' => ['shekel-sign', 'fas'],
2196		'image' => ['image', 'far'],
2197		'inr' => ['rupee-sign', 'fas'],
2198		'institution' => ['university', 'fas'],
2199		'intersex' => ['transgender', 'fas'],
2200		'jpy' => ['yen-sign', 'fas'],
2201		'keyboard-o' => ['keyboard', 'far'],
2202		'krw' => ['won-sign', 'fas'],
2203		'legal' => ['gavel', 'fas'],
2204		'lemon-o' => ['lemon', 'far'],
2205		'level-down' => ['level-down-alt', 'fas'],
2206		'level-up' => ['level-up-alt', 'fas'],
2207		'life-bouy' => ['life-ring', 'far'],
2208		'life-buoy' => ['life-ring', 'far'],
2209		'life-saver' => ['life-ring', 'far'],
2210		'lightbulb-o' => ['lightbulb', 'far'],
2211		'line-chart' => ['chart-line', 'fas'],
2212		'linkedin-square' => ['linkedin', 'fab'],
2213		'linkedin' => ['linkedin-in', 'fab'],
2214		'long-arrow-down' => ['long-arrow-alt-down', 'fas'],
2215		'long-arrow-left' => ['long-arrow-alt-left', 'fas'],
2216		'long-arrow-right' => ['long-arrow-alt-right', 'fas'],
2217		'long-arrow-up' => ['long-arrow-alt-up', 'fas'],
2218		'mail-forward' => ['share', 'fas'],
2219		'mail-reply-all' => ['reply-all', 'fas'],
2220		'mail-reply' => ['reply', 'fas'],
2221		'map-marker' => ['map-marker-alt', 'fas'],
2222		'map-o' => ['map', 'far'],
2223		'meanpath' => ['font-awesome', 'fab'],
2224		'meh-o' => ['meh', 'far'],
2225		'minus-square-o' => ['minus-square', 'far'],
2226		'mobile-phone' => ['mobile-alt', 'fas'],
2227		'mobile' => ['mobile-alt', 'fas'],
2228		'money' => ['money-bill-alt', 'far'],
2229		'moon-o' => ['moon', 'far'],
2230		'mortar-board' => ['graduation-cap', 'fas'],
2231		'navicon' => ['bars', 'fas'],
2232		'newspaper-o' => ['newspaper', 'far'],
2233		'paper-plane-o' => ['paper-plane', 'far'],
2234		'paste' => ['clipboard', 'far'],
2235		'pause-circle-o' => ['pause-circle', 'far'],
2236		'pencil-square-o' => ['edit', 'far'],
2237		'pencil-square' => ['pen-square', 'fas'],
2238		'pencil' => ['pencil-alt', 'fas'],
2239		'photo' => ['image', 'far'],
2240		'picture-o' => ['image', 'far'],
2241		'pie-chart' => ['chart-pie', 'fas'],
2242		'play-circle-o' => ['play-circle', 'far'],
2243		'plus-square-o' => ['plus-square', 'far'],
2244		'question-circle-o' => ['question-circle', 'far'],
2245		'ra' => ['rebel', 'fab'],
2246		'refresh' => ['sync', 'fas'],
2247		'remove' => ['times', 'fas'],
2248		'reorder' => ['bars', 'fas'],
2249		'repeat' => ['redo', 'fas'],
2250		'resistance' => ['rebel', 'fab'],
2251		'rmb' => ['yen-sign', 'fas'],
2252		'rotate-left' => ['undo', 'fas'],
2253		'rotate-right' => ['redo', 'fas'],
2254		'rouble' => ['ruble-sign', 'fas'],
2255		'rub' => ['ruble-sign', 'fas'],
2256		'ruble' => ['ruble-sign', 'fas'],
2257		'rupee' => ['rupee-sign', 'fas'],
2258		's15' => ['bath', 'fas'],
2259		'scissors' => ['cut', 'fas'],
2260		'send-o' => ['paper-plane', 'far'],
2261		'send' => ['paper-plane', 'fas'],
2262		'share-square-o' => ['share-square', 'far'],
2263		'shekel' => ['shekel-sign', 'fas'],
2264		'sheqel' => ['shekel-sign', 'fas'],
2265		'shield' => ['shield-alt', 'fas'],
2266		'sign-in' => ['sign-in-alt', 'fas'],
2267		'sign-out' => ['sign-out-alt', 'fas'],
2268		'signing' => ['sign-language', 'fas'],
2269		'sliders' => ['sliders-h', 'fas'],
2270		'smile-o' => ['smile', 'far'],
2271		'snowflake-o' => ['snowflake', 'far'],
2272		'soccer-ball-o' => ['futbol', 'far'],
2273		'sort-alpha-asc' => ['sort-alpha-down', 'fas'],
2274		'sort-alpha-desc' => ['sort-alpha-up', 'fas'],
2275		'sort-amount-asc' => ['sort-amount-down', 'fas'],
2276		'sort-amount-desc' => ['sort-amount-up', 'fas'],
2277		'sort-asc' => ['sort-up', 'fas'],
2278		'sort-desc' => ['sort-down', 'fas'],
2279		'sort-numeric-asc' => ['sort-numeric-down', 'fas'],
2280		'sort-numeric-desc' => ['sort-numeric-up', 'fas'],
2281		'spoon' => ['utensil-spoon', 'fas'],
2282		'square-o' => ['square', 'far'],
2283		'star-half-empty' => ['star-half', 'far'],
2284		'star-half-full' => ['star-half', 'far'],
2285		'star-half-o' => ['star-half', 'far'],
2286		'star-o' => ['star', 'far'],
2287		'sticky-note-o' => ['sticky-note', 'far'],
2288		'stop-circle-o' => ['stop-circle', 'far'],
2289		'sun-o' => ['sun', 'far'],
2290		'support' => ['life-ring', 'far'],
2291		'tablet' => ['tablet-alt', 'fas'],
2292		'tachometer' => ['tachometer-alt', 'fas'],
2293		'television' => ['tv', 'fas'],
2294		'thermometer-0' => ['thermometer-empty', 'fas'],
2295		'thermometer-1' => ['thermometer-quarter', 'fas'],
2296		'thermometer-2' => ['thermometer-half', 'fas'],
2297		'thermometer-3' => ['thermometer-three-quarters', 'fas'],
2298		'thermometer-4' => ['thermometer-full', 'fas'],
2299		'thermometer' => ['thermometer-full', 'fas'],
2300		'thumb-tack' => ['thumbtack', 'fas'],
2301		'thumbs-o-down' => ['thumbs-down', 'far'],
2302		'thumbs-o-up' => ['thumbs-up', 'far'],
2303		'ticket' => ['ticket-alt', 'fas'],
2304		'times-circle-o' => ['times-circle', 'far'],
2305		'times-rectangle-o' => ['window-close', 'far'],
2306		'times-rectangle' => ['window-close', 'fas'],
2307		'toggle-down' => ['caret-square-down', 'far'],
2308		'toggle-left' => ['caret-square-left', 'far'],
2309		'toggle-right' => ['caret-square-right', 'far'],
2310		'toggle-up' => ['caret-square-up', 'far'],
2311		'trash-o' => ['trash-alt', 'far'],
2312		'trash' => ['trash-alt', 'fas'],
2313		'try' => ['lira-sign', 'fas'],
2314		'turkish-lira' => ['lira-sign', 'fas'],
2315		'unsorted' => ['sort', 'fas'],
2316		'usd' => ['dollar-sign', 'fas'],
2317		'user-circle-o' => ['user-circle', 'far'],
2318		'user-o' => ['user', 'far'],
2319		'vcard-o' => ['address-card', 'far'],
2320		'vcard' => ['address-card', 'fas'],
2321		'video-camera' => ['video', 'fas'],
2322		'vimeo' => ['vimeo-v', 'fab'],
2323		'volume-control-phone' => ['phone-volume', 'fas'],
2324		'warning' => ['exclamation-triangle', 'fas'],
2325		'wechat' => ['weixin', 'fab'],
2326		'wheelchair-alt' => ['accessible-icon', 'fab'],
2327		'window-close-o' => ['window-close', 'far'],
2328		'won' => ['won-sign', 'fas'],
2329		'y-combinator-square' => ['hacker-news', 'fab'],
2330		'yc-square' => ['hacker-news', 'fab'],
2331		'yc' => ['y-combinator', 'fab'],
2332		'yen' => ['yen-sign', 'fas'],
2333		'youtube-play' => ['youtube', 'fab'],
2334		'youtube-square' => ['youtube', 'fab'],
2335	];
2336
2337	$brands = [
2338		'500px',
2339		'accessible-icon',
2340		'accusoft',
2341		'adn',
2342		'adversal',
2343		'affiliatetheme',
2344		'algolia',
2345		'amazon',
2346		'amazon-pay',
2347		'amilia',
2348		'android',
2349		'angellist',
2350		'angrycreative',
2351		'angular',
2352		'app-store',
2353		'app-store-ios',
2354		'apper',
2355		'apple',
2356		'apple-pay',
2357		'asymmetrik',
2358		'audible',
2359		'autoprefixer',
2360		'avianex',
2361		'aviato',
2362		'aws',
2363		'bandcamp',
2364		'behance',
2365		'behance-square',
2366		'bimobject',
2367		'bitbucket',
2368		'bitcoin',
2369		'bity',
2370		'black-tie',
2371		'blackberry',
2372		'blogger',
2373		'blogger-b',
2374		'bluetooth',
2375		'bluetooth-b',
2376		'btc',
2377		'buromobelexperte',
2378		'buysellads',
2379		'cc-amazon-pay',
2380		'cc-amex',
2381		'cc-apple-pay',
2382		'cc-diners-club',
2383		'cc-discover',
2384		'cc-jcb',
2385		'cc-mastercard',
2386		'cc-paypal',
2387		'cc-stripe',
2388		'cc-visa',
2389		'centercode',
2390		'chrome',
2391		'cloudscale',
2392		'cloudsmith',
2393		'cloudversify',
2394		'codepen',
2395		'codiepie',
2396		'connectdevelop',
2397		'contao',
2398		'cpanel',
2399		'creative-commons',
2400		'css3',
2401		'css3-alt',
2402		'cuttlefish',
2403		'd-and-d',
2404		'dashcube',
2405		'delicious',
2406		'deploydog',
2407		'deskpro',
2408		'deviantart',
2409		'digg',
2410		'digital-ocean',
2411		'discord',
2412		'discourse',
2413		'dochub',
2414		'docker',
2415		'draft2digital',
2416		'dribbble',
2417		'dribbble-square',
2418		'dropbox',
2419		'drupal',
2420		'dyalog',
2421		'earlybirds',
2422		'edge',
2423		'elementor',
2424		'ember',
2425		'empire',
2426		'envira',
2427		'erlang',
2428		'ethereum',
2429		'etsy',
2430		'expeditedssl',
2431		'facebook',
2432		'facebook-f',
2433		'facebook-messenger',
2434		'facebook-square',
2435		'firefox',
2436		'first-order',
2437		'firstdraft',
2438		'flickr',
2439		'flipboard',
2440		'fly',
2441		'font-awesome',
2442		'font-awesome-alt',
2443		'font-awesome-flag',
2444		'fonticons',
2445		'fonticons-fi',
2446		'fort-awesome',
2447		'fort-awesome-alt',
2448		'forumbee',
2449		'foursquare',
2450		'free-code-camp',
2451		'freebsd',
2452		'get-pocket',
2453		'gg',
2454		'gg-circle',
2455		'git',
2456		'git-square',
2457		'github',
2458		'github-alt',
2459		'github-square',
2460		'gitkraken',
2461		'gitlab',
2462		'gitter',
2463		'glide',
2464		'glide-g',
2465		'gofore',
2466		'goodreads',
2467		'goodreads-g',
2468		'google',
2469		'google-drive',
2470		'google-play',
2471		'google-plus',
2472		'google-plus-g',
2473		'google-plus-square',
2474		'google-wallet',
2475		'gratipay',
2476		'grav',
2477		'gripfire',
2478		'grunt',
2479		'gulp',
2480		'hacker-news',
2481		'hacker-news-square',
2482		'hips',
2483		'hire-a-helper',
2484		'hooli',
2485		'hotjar',
2486		'houzz',
2487		'html5',
2488		'hubspot',
2489		'imdb',
2490		'instagram',
2491		'internet-explorer',
2492		'ioxhost',
2493		'itunes',
2494		'itunes-note',
2495		'jenkins',
2496		'joget',
2497		'joomla',
2498		'js',
2499		'js-square',
2500		'jsfiddle',
2501		'keycdn',
2502		'kickstarter',
2503		'kickstarter-k',
2504		'korvue',
2505		'laravel',
2506		'lastfm',
2507		'lastfm-square',
2508		'leanpub',
2509		'less',
2510		'line',
2511		'linkedin',
2512		'linkedin-in',
2513		'linode',
2514		'linux',
2515		'lyft',
2516		'magento',
2517		'maxcdn',
2518		'medapps',
2519		'medium',
2520		'medium-m',
2521		'medrt',
2522		'meetup',
2523		'microsoft',
2524		'mix',
2525		'mixcloud',
2526		'mizuni',
2527		'modx',
2528		'monero',
2529		'napster',
2530		'nintendo-switch',
2531		'node',
2532		'node-js',
2533		'npm',
2534		'ns8',
2535		'nutritionix',
2536		'odnoklassniki',
2537		'odnoklassniki-square',
2538		'opencart',
2539		'openid',
2540		'opera',
2541		'optin-monster',
2542		'osi',
2543		'page4',
2544		'pagelines',
2545		'palfed',
2546		'patreon',
2547		'paypal',
2548		'periscope',
2549		'phabricator',
2550		'phoenix-framework',
2551		'php',
2552		'pied-piper',
2553		'pied-piper-alt',
2554		'pied-piper-pp',
2555		'pinterest',
2556		'pinterest-p',
2557		'pinterest-square',
2558		'playstation',
2559		'product-hunt',
2560		'pushed',
2561		'python',
2562		'qq',
2563		'quinscape',
2564		'quora',
2565		'ravelry',
2566		'react',
2567		'rebel',
2568		'red-river',
2569		'reddit',
2570		'reddit-alien',
2571		'reddit-square',
2572		'rendact',
2573		'renren',
2574		'replyd',
2575		'resolving',
2576		'rocketchat',
2577		'rockrms',
2578		'safari',
2579		'sass',
2580		'schlix',
2581		'scribd',
2582		'searchengin',
2583		'sellcast',
2584		'sellsy',
2585		'servicestack',
2586		'shirtsinbulk',
2587		'simplybuilt',
2588		'sistrix',
2589		'skyatlas',
2590		'skype',
2591		'slack',
2592		'slack-hash',
2593		'slideshare',
2594		'snapchat',
2595		'snapchat-ghost',
2596		'snapchat-square',
2597		'soundcloud',
2598		'speakap',
2599		'spotify',
2600		'stack-exchange',
2601		'stack-overflow',
2602		'staylinked',
2603		'steam',
2604		'steam-square',
2605		'steam-symbol',
2606		'sticker-mule',
2607		'strava',
2608		'stripe',
2609		'stripe-s',
2610		'studiovinari',
2611		'stumbleupon',
2612		'stumbleupon-circle',
2613		'superpowers',
2614		'supple',
2615		'telegram',
2616		'telegram-plane',
2617		'tencent-weibo',
2618		'themeisle',
2619		'trello',
2620		'tripadvisor',
2621		'tumblr',
2622		'tumblr-square',
2623		'twitch',
2624		'twitter',
2625		'twitter-square',
2626		'typo3',
2627		'uber',
2628		'uikit',
2629		'uniregistry',
2630		'untappd',
2631		'usb',
2632		'ussunnah',
2633		'vaadin',
2634		'viacoin',
2635		'viadeo',
2636		'viadeo-square',
2637		'viber',
2638		'vimeo',
2639		'vimeo-square',
2640		'vimeo-v',
2641		'vine',
2642		'vk',
2643		'vnv',
2644		'vuejs',
2645		'weibo',
2646		'weixin',
2647		'whatsapp',
2648		'whatsapp-square',
2649		'whmcs',
2650		'wikipedia-w',
2651		'windows',
2652		'wordpress',
2653		'wordpress-simple',
2654		'wpbeginner',
2655		'wpexplorer',
2656		'wpforms',
2657		'xbox',
2658		'xing',
2659		'xing-square',
2660		'y-combinator',
2661		'yahoo',
2662		'yandex',
2663		'yandex-international',
2664		'yelp',
2665		'yoast',
2666		'youtube',
2667		'youtube-square',
2668	];
2669
2670	foreach ($classes as $index => $c) {
2671		if ($c === 'fa') {
2672			// FontAwesome 5 deprecated the use of fa prefix in favour of fas, far and fab
2673			unset($classes[$index]);
2674			continue;
2675		}
2676
2677		if (preg_match_all('/^elgg-icon-(.+)/i', $c)) {
2678			// convert
2679			$base_icon = preg_replace('/^elgg-icon-(.+)/i', '$1', $c);
2680
2681			if ($map_sprites) {
2682				if (strpos($base_icon, '-hover') !== false) {
2683					$base_icon = str_replace('-hover', '', $base_icon);
2684					$classes[] = 'elgg-state';
2685					$classes[] = 'elgg-state-notice';
2686				}
2687
2688				$base_icon = elgg_extract($base_icon, $legacy_sprites, $base_icon);
2689			}
2690
2691			// map solid/regular/light iconnames to correct classes
2692			if (preg_match('/.*-solid$/', $base_icon)) {
2693				$base_icon = preg_replace('/(.*)-solid$/', '$1', $base_icon);
2694				$classes[] = 'fas';
2695			} elseif (preg_match('/.*-regular$/', $base_icon)) {
2696				$base_icon = preg_replace('/(.*)-regular$/', '$1', $base_icon);
2697				$classes[] = 'far';
2698			} elseif (preg_match('/.*-light$/', $base_icon)) {
2699				// currently light is only available in FontAwesome 5 Pro
2700				$base_icon = preg_replace('/(.*)-light$/', '$1', $base_icon);
2701				$classes[] = 'fal';
2702			} else {
2703				if (array_key_exists($base_icon, $fa5)) {
2704					$classes[] = $fa5[$base_icon][1];
2705					$base_icon = $fa5[$base_icon][0];
2706				} else if (in_array($base_icon, $brands)) {
2707					$classes[] = 'fab';
2708				} else {
2709					$classes[] = 'fas';
2710				}
2711			}
2712
2713			$classes[] = "fa-{$base_icon}";
2714		}
2715	}
2716
2717	$classes = array_unique($classes);
2718
2719	return elgg_trigger_plugin_hook('classes', 'icon', null, $classes);
2720}
2721