1<?php
2
3use Elgg\Http\ResponseBuilder;
4
5/**
6 * Bootstrapping and helper procedural code available for use in Elgg core and plugins.
7 *
8 * @todo These functions can't be subpackaged because they cover a wide mix of
9 * purposes and subsystems.  Many of them should be moved to more relevant files.
10 */
11
12/**
13 * Get a reference to the global Application object
14 *
15 * @return \Elgg\Di\PublicContainer
16 * @since 2.0.0
17 */
18function elgg() {
19	return _elgg_services()->dic;
20}
21
22/**
23 * Forward to $location.
24 *
25 * Sends a 'Location: $location' header and exits.  If headers have already been sent, throws an exception.
26 *
27 * @param string $location URL to forward to browser to. This can be a path
28 *                         relative to the network's URL.
29 * @param string $reason   Short explanation for why we're forwarding. Set to
30 *                         '404' to forward to error page. Default message is
31 *                         'system'.
32 *
33 * @return void
34 * @throws SecurityException|InvalidParameterException
35 */
36function forward($location = "", $reason = 'system') {
37	if (headers_sent($file, $line)) {
38		throw new \SecurityException("Redirect could not be issued due to headers already being sent. Halting execution for security. "
39			. "Output started in file $file at line $line. Search http://learn.elgg.org/ for more information.");
40	}
41
42	_elgg_services()->responseFactory->redirect($location, $reason);
43	exit;
44}
45
46/**
47 * Set a response HTTP header
48 *
49 * @see header()
50 *
51 * @param string $header  Header
52 * @param bool   $replace Replace existing header
53 * @return void
54 * @since 2.3
55 */
56function elgg_set_http_header($header, $replace = true) {
57	if (!preg_match('~^HTTP/\\d\\.\\d~', $header)) {
58		list($name, $value) = explode(':', $header, 2);
59		_elgg_services()->responseFactory->setHeader($name, ltrim($value), $replace);
60	}
61}
62
63/**
64 * Defines a JS lib as an AMD module. This is useful for shimming
65 * traditional JS or for setting the paths of AMD modules.
66 *
67 * Calling multiple times for the same name will:
68 *     * set the preferred path to the last call setting a path
69 *     * overwrite the shimmed AMD modules with the last call setting a shimmed module
70 *
71 * Use elgg_require_js($name) to load on the current page.
72 *
73 * Calling this function is not needed if your JS are in views named like `module/name.js`
74 * Instead, simply call elgg_require_js("module/name").
75 *
76 * @note The configuration is cached in simplecache, so logic should not depend on user-
77 *       specific values like get_current_language().
78 *
79 * @param string $name   The module name
80 * @param array  $config An array like the following:
81 *                       array  'deps'    An array of AMD module dependencies
82 *                       string 'exports' The name of the exported module
83 *                       string 'src'     The URL to the JS. Can be relative.
84 *
85 * @return void
86 */
87function elgg_define_js($name, $config) {
88	$src = elgg_extract('src', $config);
89
90	if ($src) {
91		$url = elgg_normalize_url($src);
92		_elgg_services()->amdConfig->addPath($name, $url);
93	}
94
95	// shimmed module
96	if (isset($config['deps']) || isset($config['exports'])) {
97		_elgg_services()->amdConfig->addShim($name, $config);
98	}
99}
100
101/**
102 * Request that Elgg load an AMD module onto the page.
103 *
104 * @param string $name The AMD module name.
105 * @return void
106 * @since 1.9.0
107 */
108function elgg_require_js($name) {
109	_elgg_services()->amdConfig->addDependency($name);
110}
111
112/**
113 * Cancel a request to load an AMD module onto the page.
114 *
115 * @note The elgg, jquery, and jquery-ui modules cannot be cancelled.
116 *
117 * @param string $name The AMD module name.
118 * @return void
119 * @since 2.1.0
120 */
121function elgg_unrequire_js($name) {
122	_elgg_services()->amdConfig->removeDependency($name);
123}
124
125/**
126 * Get the JavaScript URLs that are loaded
127 *
128 * @param string $location 'head' or 'footer'
129 *
130 * @return array
131 * @since 1.8.0
132 */
133function elgg_get_loaded_js($location = 'head') {
134	return elgg_get_loaded_external_files('js', $location);
135}
136
137/**
138 * Register a CSS view name to be included in the HTML head
139 *
140 * @param string $view The css view name
141 *
142 * @return void
143 *
144 * @since 3.1
145 */
146function elgg_require_css(string $view) {
147	$view_name = "{$view}.css";
148	if (!elgg_view_exists($view_name)) {
149		$view_name = $view;
150	}
151
152	elgg_register_external_file('css', $view, elgg_get_simplecache_url($view_name));
153	elgg_load_external_file('css', $view);
154}
155
156/**
157 * Unregister a CSS view name to be included in the HTML head
158 *
159 * @param string $view The css view name
160 *
161 * @return void
162 *
163 * @since 3.1
164 */
165function elgg_unrequire_css(string $view) {
166	elgg_unregister_external_file('css', $view);
167}
168
169/**
170 * Get the loaded CSS URLs
171 *
172 * @return array
173 * @since 1.8.0
174 */
175function elgg_get_loaded_css() {
176	return elgg_get_loaded_external_files('css', 'head');
177}
178
179/**
180 * Core registration function for external files
181 *
182 * @param string $type     Type of external resource (js or css)
183 * @param string $name     Identifier used as key
184 * @param string $url      URL
185 * @param string $location Location in the page to include the file (default = 'head')
186 * @param int    $priority Loading priority of the file
187 *
188 * @return bool
189 * @since 1.8.0
190 */
191function elgg_register_external_file($type, $name, $url, $location = 'head', $priority = 500) {
192	return _elgg_services()->externalFiles->register($type, $name, $url, $location, $priority);
193}
194
195/**
196 * Unregister an external file
197 *
198 * @param string $type Type of file: js or css
199 * @param string $name The identifier of the file
200 *
201 * @return bool
202 * @since 1.8.0
203 */
204function elgg_unregister_external_file($type, $name) {
205	return _elgg_services()->externalFiles->unregister($type, $name);
206}
207
208/**
209 * Load an external resource for use on this page
210 *
211 * @param string $type Type of file: js or css
212 * @param string $name The identifier for the file
213 *
214 * @return void
215 * @since 1.8.0
216 */
217function elgg_load_external_file($type, $name) {
218	_elgg_services()->externalFiles->load($type, $name);
219}
220
221/**
222 * Get external resource descriptors
223 *
224 * @param string $type     Type of file: js or css
225 * @param string $location Page location
226 *
227 * @return array
228 * @since 1.8.0
229 */
230function elgg_get_loaded_external_files($type, $location) {
231	return _elgg_services()->externalFiles->getLoadedFiles($type, $location);
232}
233
234/**
235 * Display a system message on next page load.
236 *
237 * @param string|array $message Message or messages to add
238 *
239 * @return bool
240 */
241function system_message($message) {
242	elgg()->system_messages->addSuccessMessage($message);
243	return true;
244}
245
246/**
247 * Display an error on next page load.
248 *
249 * @param string|array $error Error or errors to add
250 *
251 * @return bool
252 */
253function register_error($error) {
254	elgg()->system_messages->addErrorMessage($error);
255	return true;
256}
257
258/**
259 * Get a copy of the current system messages.
260 *
261 * @return \Elgg\SystemMessages\RegisterSet
262 * @since 2.1
263 */
264function elgg_get_system_messages() {
265	return elgg()->system_messages->loadRegisters();
266}
267
268/**
269 * Set the system messages. This will overwrite the state of all messages and errors!
270 *
271 * @param \Elgg\SystemMessages\RegisterSet $set Set of messages
272 * @return void
273 * @since 2.1
274 */
275function elgg_set_system_messages(\Elgg\SystemMessages\RegisterSet $set) {
276	elgg()->system_messages->saveRegisters($set);
277}
278
279/**
280 * Register a callback as an Elgg event handler.
281 *
282 * Events are emitted by Elgg when certain actions occur.  Plugins
283 * can respond to these events or halt them completely by registering a handler
284 * as a callback to an event.  Multiple handlers can be registered for
285 * the same event and will be executed in order of $priority.
286 *
287 * For most events, any handler returning false will halt the execution chain and
288 * cause the event to be "cancelled". For After Events, the return values of the
289 * handlers will be ignored and all handlers will be called.
290 *
291 * This function is called with the event name, event type, and handler callback name.
292 * Setting the optional $priority allows plugin authors to specify when the
293 * callback should be run.  Priorities for plugins should be 1-1000.
294 *
295 * The callback is passed 3 arguments when called: $event, $type, and optional $params.
296 *
297 * $event is the name of event being emitted.
298 * $type is the type of event or object concerned.
299 * $params is an optional parameter passed that can include a related object.  See
300 * specific event documentation for details on which events pass what parameteres.
301 *
302 * @tip If a priority isn't specified it is determined by the order the handler was
303 * registered relative to the event and type.  For plugins, this generally means
304 * the earlier the plugin is in the load order, the earlier the priorities are for
305 * any event handlers.
306 *
307 * @tip $event and $object_type can use the special keyword 'all'.  Handler callbacks registered
308 * with $event = all will be called for all events of type $object_type.  Similarly,
309 * callbacks registered with $object_type = all will be called for all events of type
310 * $event, regardless of $object_type.  If $event and $object_type both are 'all', the
311 * handler callback will be called for all events.
312 *
313 * @tip Event handler callbacks are considered in the follow order:
314 *  - Specific registration where 'all' isn't used.
315 *  - Registration where 'all' is used for $event only.
316 *  - Registration where 'all' is used for $type only.
317 *  - Registration where 'all' is used for both.
318 *
319 * @warning If you use the 'all' keyword, you must have logic in the handler callback to
320 * test the passed parameters before taking an action.
321 *
322 * @tip When referring to events, the preferred syntax is "event, type".
323 *
324 * @param string   $event       The event type
325 * @param string   $object_type The object type
326 * @param callable $callback    The handler callback
327 * @param int      $priority    The priority - 0 is default, negative before, positive after
328 *
329 * @return bool
330 * @example documentation/events/basic.php
331 * @example documentation/events/advanced.php
332 * @example documentation/events/all.php
333 */
334function elgg_register_event_handler($event, $object_type, $callback, $priority = 500) {
335	return _elgg_services()->events->registerHandler($event, $object_type, $callback, $priority);
336}
337
338/**
339 * Unregisters a callback for an event.
340 *
341 * @param string   $event       The event type
342 * @param string   $object_type The object type
343 * @param callable $callback    The callback. Since 1.11, static method callbacks will match dynamic methods
344 *
345 * @return bool true if a handler was found and removed
346 * @since 1.7
347 */
348function elgg_unregister_event_handler($event, $object_type, $callback) {
349	return _elgg_services()->events->unregisterHandler($event, $object_type, $callback);
350}
351
352/**
353 * Clears all callback registrations for a event.
354 *
355 * @param string $event       The name of the event
356 * @param string $object_type The objecttype of the event
357 *
358 * @return void
359 * @since 2.3
360 */
361function elgg_clear_event_handlers($event, $object_type) {
362	_elgg_services()->events->clearHandlers($event, $object_type);
363}
364
365/**
366 * Trigger an Elgg Event and attempt to run all handler callbacks registered to that
367 * event, type.
368 *
369 * This function attempts to run all handlers registered to $event, $object_type or
370 * the special keyword 'all' for either or both. If a handler returns false, the
371 * event will be cancelled (no further handlers will be called, and this function
372 * will return false).
373 *
374 * $event is usually a verb: create, update, delete, annotation.
375 *
376 * $object_type is usually a noun: object, group, user, annotation, relationship, metadata.
377 *
378 * $object is usually an Elgg* object associated with the event.
379 *
380 * @warning Elgg events should only be triggered by core.  Plugin authors should use
381 * {@link trigger_elgg_plugin_hook()} instead.
382 *
383 * @tip When referring to events, the preferred syntax is "event, type".
384 *
385 * @note Internal: Only rarely should events be changed, added, or removed in core.
386 * When making changes to events, be sure to first create a ticket on Github.
387 *
388 * @note Internal: @tip Think of $object_type as the primary namespace element, and
389 * $event as the secondary namespace.
390 *
391 * @param string $event       The event type
392 * @param string $object_type The object type
393 * @param mixed  $object      The object involved in the event
394 *
395 * @return bool False if any handler returned false, otherwise true.
396 * @example documentation/examples/events/trigger.php
397 */
398function elgg_trigger_event($event, $object_type, $object = null) {
399	return elgg()->events->trigger($event, $object_type, $object);
400}
401
402/**
403 * Trigger a "Before event" indicating a process is about to begin.
404 *
405 * Like regular events, a handler returning false will cancel the process and false
406 * will be returned.
407 *
408 * To register for a before event, append ":before" to the event name when registering.
409 *
410 * @param string $event       The event type. The fired event type will be appended with ":before".
411 * @param string $object_type The object type
412 * @param mixed  $object      The object involved in the event
413 *
414 * @return bool False if any handler returned false, otherwise true
415 *
416 * @see elgg_trigger_event()
417 * @see elgg_trigger_after_event()
418 */
419function elgg_trigger_before_event($event, $object_type, $object = null) {
420	return elgg()->events->triggerBefore($event, $object_type, $object);
421}
422
423/**
424 * Trigger an "After event" indicating a process has finished.
425 *
426 * Unlike regular events, all the handlers will be called, their return values ignored.
427 *
428 * To register for an after event, append ":after" to the event name when registering.
429 *
430 * @param string $event       The event type. The fired event type will be appended with ":after".
431 * @param string $object_type The object type
432 * @param string $object      The object involved in the event
433 *
434 * @return true
435 *
436 * @see elgg_trigger_before_event()
437 */
438function elgg_trigger_after_event($event, $object_type, $object = null) {
439	return elgg()->events->triggerAfter($event, $object_type, $object);
440}
441
442/**
443 * Trigger an event normally, but send a notice about deprecated use if any handlers are registered.
444 *
445 * @param string $event       The event type
446 * @param string $object_type The object type
447 * @param string $object      The object involved in the event
448 * @param string $message     The deprecation message
449 * @param string $version     Human-readable *release* version: 1.9, 1.10, ...
450 *
451 * @return bool
452 *
453 * @see elgg_trigger_event()
454 */
455function elgg_trigger_deprecated_event($event, $object_type, $object = null, $message = null, $version = null) {
456	return elgg()->events->triggerDeprecated($event, $object_type, $object, $message, $version);
457}
458
459/**
460 * Register a callback as a plugin hook handler.
461 *
462 * Plugin hooks allow developers to losely couple plugins and features by
463 * responding to and emitting {@link elgg_trigger_plugin_hook()} customizable hooks.
464 * Handler callbacks can respond to the hook, change the details of the hook, or
465 * ignore it.
466 *
467 * Multiple handlers can be registered for a plugin hook, and each callback
468 * is called in order of priority.  If the return value of a handler is not
469 * null, that value is passed to the next callback in the call stack.  When all
470 * callbacks have been run, the final value is passed back to the caller
471 * via {@link elgg_trigger_plugin_hook()}.
472 *
473 * Similar to Elgg Events, plugin hook handler callbacks are registered by passing
474 * a hook, a type, and a priority.
475 *
476 * The callback is passed 4 arguments when called: $hook, $type, $value, and $params.
477 *
478 *  - str $hook The name of the hook.
479 *  - str $type The type of hook.
480 *  - mixed $value The return value of the last handler or the default
481 *  value if no other handlers have been called.
482 *  - mixed $params An optional array of parameters.  Used to provide additional
483 *  information to plugins.
484 *
485 * @tip Plugin hooks are similar to Elgg Events in that Elgg emits
486 * a plugin hook when certain actions occur, but a plugin hook allows you to alter the
487 * parameters, as well as halt execution.
488 *
489 * @tip If a priority isn't specified it is determined by the order the handler was
490 * registered relative to the event and type.  For plugins, this generally means
491 * the earlier the plugin is in the load order, the earlier the priorities are for
492 * any event handlers.
493 *
494 * @tip Like Elgg Events, $hook and $type can use the special keyword 'all'.
495 * Handler callbacks registered with $hook = all will be called for all hooks
496 * of type $type.  Similarly, handlers registered with $type = all will be
497 * called for all hooks of type $event, regardless of $object_type.  If $hook
498 * and $type both are 'all', the handler will be called for all hooks.
499 *
500 * @tip Plugin hooks are sometimes used to gather lists from plugins.  This is
501 * usually done by pushing elements into an array passed in $params.  Be sure
502 * to append to and then return $value so you don't overwrite other plugin's
503 * values.
504 *
505 * @warning Unlike Elgg Events, a handler that returns false will NOT halt the
506 * execution chain.
507 *
508 * @param string   $hook     The name of the hook
509 * @param string   $type     The type of the hook
510 * @param callable $callback The name of a valid function or an array with object and method
511 * @param int      $priority The priority - 500 is default, lower numbers called first
512 *
513 * @return bool
514 *
515 * @example hooks/register/basic.php Registering for a plugin hook and examining the variables.
516 * @example hooks/register/advanced.php Registering for a plugin hook and changing the params.
517 * @since 1.8.0
518 */
519function elgg_register_plugin_hook_handler($hook, $type, $callback, $priority = 500) {
520	return elgg()->hooks->registerHandler($hook, $type, $callback, $priority);
521}
522
523/**
524 * Unregister a callback as a plugin hook.
525 *
526 * @param string   $hook        The name of the hook
527 * @param string   $entity_type The name of the type of entity (eg "user", "object" etc)
528 * @param callable $callback    The PHP callback to be removed. Since 1.11, static method
529 *                              callbacks will match dynamic methods
530 *
531 * @return void
532 * @since 1.8.0
533 */
534function elgg_unregister_plugin_hook_handler($hook, $entity_type, $callback) {
535	elgg()->hooks->unregisterHandler($hook, $entity_type, $callback);
536}
537
538/**
539 * Clears all callback registrations for a plugin hook.
540 *
541 * @param string $hook The name of the hook
542 * @param string $type The type of the hook
543 *
544 * @return void
545 * @since 2.0
546 */
547function elgg_clear_plugin_hook_handlers($hook, $type) {
548	elgg()->hooks->clearHandlers($hook, $type);
549}
550
551/**
552 * Trigger a Plugin Hook and run all handler callbacks registered to that hook:type.
553 *
554 * This function runs all handlers registered to $hook, $type or
555 * the special keyword 'all' for either or both.
556 *
557 * Use $params to send additional information to the handler callbacks.
558 *
559 * $returnvalue is the initial value to pass to the handlers, which can
560 * change it by returning non-null values. It is useful to use $returnvalue
561 * to set defaults. If no handlers are registered, $returnvalue is immediately
562 * returned.
563 *
564 * Handlers that return null (or with no explicit return or return value) will
565 * not change the value of $returnvalue.
566 *
567 * $hook is usually a verb: import, get_views, output.
568 *
569 * $type is usually a noun: user, ecml, page.
570 *
571 * @tip Like Elgg Events, $hook and $type can use the special keyword 'all'.
572 * Handler callbacks registered with $hook = all will be called for all hooks
573 * of type $type.  Similarly, handlers registered with $type = all will be
574 * called for all hooks of type $event, regardless of $object_type.  If $hook
575 * and $type both are 'all', the handler will be called for all hooks.
576 *
577 * @tip It's not possible for a plugin hook to change a non-null $returnvalue
578 * to null.
579 *
580 * @note Internal: The checks for $hook and/or $type not being equal to 'all' is to
581 * prevent a plugin hook being registered with an 'all' being called more than
582 * once if the trigger occurs with an 'all'. An example in core of this is in
583 * actions.php:
584 * elgg_trigger_plugin_hook('action_gatekeeper:permissions:check', 'all', ...)
585 *
586 * @see elgg_register_plugin_hook_handler()
587 *
588 * @param string $hook        The name of the hook to trigger ("all" will
589 *                            trigger for all $types regardless of $hook value)
590 * @param string $type        The type of the hook to trigger ("all" will
591 *                            trigger for all $hooks regardless of $type value)
592 * @param mixed  $params      Additional parameters to pass to the handlers
593 * @param mixed  $returnvalue An initial return value
594 *
595 * @return mixed|null The return value of the last handler callback called
596 *
597 * @example hooks/trigger/basic.php    Trigger a hook that determines if execution
598 *                                     should continue.
599 * @example hooks/trigger/advanced.php Trigger a hook with a default value and use
600 *                                     the results to populate a menu.
601 * @example hooks/basic.php            Trigger and respond to a basic plugin hook.
602 *
603 * @since 1.8.0
604 */
605function elgg_trigger_plugin_hook($hook, $type, $params = null, $returnvalue = null) {
606	return elgg()->hooks->trigger($hook, $type, $params, $returnvalue);
607}
608
609/**
610 * Trigger an plugin hook normally, but send a notice about deprecated use if any handlers are registered.
611 *
612 * @param string $hook        The name of the plugin hook
613 * @param string $type        The type of the plugin hook
614 * @param mixed  $params      Supplied params for the hook
615 * @param mixed  $returnvalue The value of the hook, this can be altered by registered callbacks
616 * @param string $message     The deprecation message
617 * @param string $version     Human-readable *release* version: 1.9, 1.10, ...
618 *
619 * @return mixed
620 *
621 * @see elgg_trigger_plugin_hook()
622 * @since 3.0
623 */
624function elgg_trigger_deprecated_plugin_hook($hook, $type, $params = null, $returnvalue = null, $message = null, $version = null) {
625	return elgg()->hooks->triggerDeprecated($hook, $type, $params, $returnvalue, $message, $version);
626}
627
628/**
629 * Log a message.
630 *
631 * If $level is >= to the debug setting in {@link $CONFIG->debug}, the
632 * message will be sent to {@link elgg_dump()}.  Messages with lower
633 * priority than {@link $CONFIG->debug} are ignored.
634 *
635 * @note Use the developers plugin to display logs
636 *
637 * @param string $message User message
638 * @param string $level   NOTICE | WARNING | ERROR
639 *
640 * @return bool
641 * @since 1.7.0
642 */
643function elgg_log($message, $level = \Psr\Log\LogLevel::NOTICE) {
644	return _elgg_services()->logger->log($level, $message);
645}
646
647/**
648 * Logs $value to PHP's {@link error_log()}
649 *
650 * A {@elgg_plugin_hook debug log} is called.  If a handler returns
651 * false, it will stop the default logging method.
652 *
653 * @note Use the developers plugin to display logs
654 *
655 * @param mixed $value The value
656 * @return void
657 * @since 1.7.0
658 */
659function elgg_dump($value) {
660	_elgg_services()->logger->dump($value);
661}
662
663/**
664 * Get the current Elgg version information
665 *
666 * @param bool $human_readable Whether to return a human readable version (default: false)
667 *
668 * @return string|false Depending on success
669 * @since 1.9
670 */
671function elgg_get_version($human_readable = false) {
672	static $version, $release;
673
674	if (!isset($version) || !isset($release)) {
675		$path = \Elgg\Application::elggDir()->getPath('version.php');
676		if (!is_file($path)) {
677			return false;
678		}
679		include $path;
680	}
681
682	return $human_readable ? $release : $version;
683}
684
685/**
686 * Log a notice about deprecated use of a function, view, etc.
687 *
688 * @param string $msg         Message to log
689 * @param string $dep_version Human-readable *release* version: 1.7, 1.8, ...
690 * @param mixed  $ignored     No longer used argument
691 *
692 * @return bool
693 * @since 1.7.0
694 */
695function elgg_deprecated_notice($msg, $dep_version, $ignored = null) {
696	return _elgg_services()->deprecation->sendNotice($msg, $dep_version);
697}
698
699/**
700 * Builds a URL from the a parts array like one returned by {@link parse_url()}.
701 *
702 * @note If only partial information is passed, a partial URL will be returned.
703 *
704 * @param array $parts       Associative array of URL components like parse_url() returns
705 *                           'user' and 'pass' parts are ignored because of security reasons
706 * @param bool  $html_encode HTML Encode the url?
707 *
708 * @see https://github.com/Elgg/Elgg/pull/8146#issuecomment-91544585
709 * @return string Full URL
710 * @since 1.7.0
711 */
712function elgg_http_build_url(array $parts, $html_encode = true) {
713	// build only what's given to us.
714	$scheme = isset($parts['scheme']) ? "{$parts['scheme']}://" : '';
715	$host = isset($parts['host']) ? "{$parts['host']}" : '';
716	$port = isset($parts['port']) ? ":{$parts['port']}" : '';
717	$path = isset($parts['path']) ? "{$parts['path']}" : '';
718	$query = isset($parts['query']) ? "?{$parts['query']}" : '';
719	$fragment = isset($parts['fragment']) ? "#{$parts['fragment']}" : '';
720
721	$string = $scheme . $host . $port . $path . $query . $fragment;
722
723	if ($html_encode) {
724		return htmlspecialchars($string, ENT_QUOTES, 'UTF-8', false);
725	} else {
726		return $string;
727	}
728}
729
730/**
731 * Adds action tokens to URL
732 *
733 * As of 1.7.0 action tokens are required on all actions.
734 * Use this function to append action tokens to a URL's GET parameters.
735 * This will preserve any existing GET parameters.
736 *
737 * @note If you are using {@elgg_view input/form} you don't need to
738 * add tokens to the action.  The form view automatically handles
739 * tokens.
740 *
741 * @param string $url         Full action URL
742 * @param bool   $html_encode HTML encode the url? (default: false)
743 *
744 * @return string URL with action tokens
745 * @since 1.7.0
746 */
747function elgg_add_action_tokens_to_url($url, $html_encode = false) {
748	$url = elgg_normalize_url($url);
749	$components = parse_url($url);
750
751	if (isset($components['query'])) {
752		$query = elgg_parse_str($components['query']);
753	} else {
754		$query = [];
755	}
756
757	if (isset($query['__elgg_ts']) && isset($query['__elgg_token'])) {
758		return $url;
759	}
760
761	// append action tokens to the existing query
762	$query['__elgg_ts'] = elgg()->csrf->getCurrentTime()->getTimestamp();
763	$query['__elgg_token'] = elgg()->csrf->generateActionToken($query['__elgg_ts']);
764	$components['query'] = http_build_query($query);
765
766	// rebuild the full url
767	return elgg_http_build_url($components, $html_encode);
768}
769
770/**
771 * Removes an element from a URL's query string.
772 *
773 * @note You can send a partial URL string.
774 *
775 * @param string $url     Full URL
776 * @param string $element The element to remove
777 *
778 * @return string The new URL with the query element removed.
779 * @since 1.7.0
780 */
781function elgg_http_remove_url_query_element($url, $element) {
782	return elgg_http_add_url_query_elements($url, [$element => null]);
783}
784
785/**
786 * Sets elements in a URL's query string.
787 *
788 * @param string $url      The URL
789 * @param array  $elements Key/value pairs to set in the URL. If the value is null, the
790 *                         element is removed from the URL.
791 *
792 * @return string The new URL with the query strings added
793 * @since 1.7.0
794 */
795function elgg_http_add_url_query_elements($url, array $elements) {
796	$url_array = parse_url($url);
797
798	if (isset($url_array['query'])) {
799		$query = elgg_parse_str($url_array['query']);
800	} else {
801		$query = [];
802	}
803
804	foreach ($elements as $k => $v) {
805		if ($v === null) {
806			unset($query[$k]);
807		} else {
808			$query[$k] = $v;
809		}
810	}
811
812	// why check path? A: if no path, this may be a relative URL like "?foo=1". In this case,
813	// the output "" would be interpreted the current URL, so in this case we *must* set
814	// a query to make sure elements are removed.
815	if ($query || empty($url_array['path'])) {
816		$url_array['query'] = http_build_query($query);
817	} else {
818		unset($url_array['query']);
819	}
820	$string = elgg_http_build_url($url_array, false);
821
822	// Restore relative protocol to url if missing and is provided as part of the initial url (see #9874)
823	if (!isset($url['scheme']) && (substr($url, 0, 2) == '//')) {
824		$string = "//{$string}";
825	}
826
827	return $string;
828}
829
830/**
831 * Test if two URLs are functionally identical.
832 *
833 * @tip If $ignore_params is used, neither the name nor its value will be considered when comparing.
834 *
835 * @tip The order of GET params doesn't matter.
836 *
837 * @param string $url1          First URL
838 * @param string $url2          Second URL
839 * @param array  $ignore_params GET params to ignore in the comparison
840 *
841 * @return bool
842 * @since 1.8.0
843 */
844function elgg_http_url_is_identical($url1, $url2, $ignore_params = ['offset', 'limit']) {
845	if (!is_string($url1) || !is_string($url2)) {
846		return false;
847	}
848
849	$url1 = elgg_normalize_url($url1);
850	$url2 = elgg_normalize_url($url2);
851
852	if ($url1 == $url2) {
853		return true;
854	}
855
856	$url1_info = parse_url($url1);
857	$url2_info = parse_url($url2);
858
859	if (isset($url1_info['path'])) {
860		$url1_info['path'] = trim($url1_info['path'], '/');
861	}
862	if (isset($url2_info['path'])) {
863		$url2_info['path'] = trim($url2_info['path'], '/');
864	}
865
866	// compare basic bits
867	$parts = ['scheme', 'host', 'path'];
868
869	foreach ($parts as $part) {
870		if ((isset($url1_info[$part]) && isset($url2_info[$part]))
871		&& $url1_info[$part] != $url2_info[$part]) {
872			return false;
873		} elseif (isset($url1_info[$part]) && !isset($url2_info[$part])) {
874			return false;
875		} elseif (!isset($url1_info[$part]) && isset($url2_info[$part])) {
876			return false;
877		}
878	}
879
880	// quick compare of get params
881	if (isset($url1_info['query']) && isset($url2_info['query'])
882	&& $url1_info['query'] == $url2_info['query']) {
883		return true;
884	}
885
886	// compare get params that might be out of order
887	$url1_params = [];
888	$url2_params = [];
889
890	if (isset($url1_info['query'])) {
891		if ($url1_info['query'] = html_entity_decode($url1_info['query'])) {
892			$url1_params = elgg_parse_str($url1_info['query']);
893		}
894	}
895
896	if (isset($url2_info['query'])) {
897		if ($url2_info['query'] = html_entity_decode($url2_info['query'])) {
898			$url2_params = elgg_parse_str($url2_info['query']);
899		}
900	}
901
902	// drop ignored params
903	foreach ($ignore_params as $param) {
904		if (isset($url1_params[$param])) {
905			unset($url1_params[$param]);
906		}
907		if (isset($url2_params[$param])) {
908			unset($url2_params[$param]);
909		}
910	}
911
912	// array_diff_assoc only returns the items in arr1 that aren't in arrN
913	// but not the items that ARE in arrN but NOT in arr1
914	// if arr1 is an empty array, this function will return 0 no matter what.
915	// since we only care if they're different and not how different,
916	// add the results together to get a non-zero (ie, different) result
917	$diff_count = count(array_diff_assoc($url1_params, $url2_params));
918	$diff_count += count(array_diff_assoc($url2_params, $url1_params));
919	if ($diff_count > 0) {
920		return false;
921	}
922
923	return true;
924}
925
926/**
927 * Signs provided URL with a SHA256 HMAC key
928 *
929 * @note Signed URLs do not offer CSRF protection and should not be used instead of action tokens.
930 *
931 * @param string $url     URL to sign
932 * @param string $expires Expiration time
933 *                        A string suitable for strtotime()
934 *                        Falsey values indicate non-expiring URL
935 * @return string
936 */
937function elgg_http_get_signed_url($url, $expires = false) {
938	return _elgg_services()->urlSigner->sign($url, $expires);
939}
940
941/**
942 * Validates if the HMAC signature of the URL is valid
943 *
944 * @param string $url URL to validate
945 * @return bool
946 */
947function elgg_http_validate_signed_url($url) {
948	return _elgg_services()->urlSigner->isValid($url);
949}
950
951/**
952 * Validates if the HMAC signature of the current request is valid
953 * Issues 403 response if signature is invalid
954 *
955 * @return void
956 * @throws \Elgg\HttpException
957 */
958function elgg_signed_request_gatekeeper() {
959
960	if (\Elgg\Application::isCli()) {
961		return;
962	}
963
964	_elgg_services()->urlSigner->assertValid(current_page_url());
965}
966
967/**
968 * Checks for $array[$key] and returns its value if it exists, else
969 * returns $default.
970 *
971 * Shorthand for $value = (isset($array['key'])) ? $array['key'] : 'default';
972 *
973 * @param string $key     Key to check in the source array
974 * @param array  $array   Source array
975 * @param mixed  $default Value to return if key is not found
976 * @param bool   $strict  Return array key if it's set, even if empty. If false,
977 *                        return $default if the array key is unset or empty.
978 *
979 * @return mixed
980 * @since 1.8.0
981 */
982function elgg_extract($key, $array, $default = null, $strict = true) {
983	if (!is_array($array) && !$array instanceof ArrayAccess) {
984		return $default;
985	}
986
987	if ($strict) {
988		return (isset($array[$key])) ? $array[$key] : $default;
989	} else {
990		return (isset($array[$key]) && !empty($array[$key])) ? $array[$key] : $default;
991	}
992}
993
994/**
995 * Extract class names from an array, optionally merging into a preexisting set.
996 *
997 * @param array           $array       Source array
998 * @param string|string[] $existing    Existing name(s)
999 * @param string          $extract_key Key to extract new classes from
1000 * @return string[]
1001 *
1002 * @since 2.3.0
1003 */
1004function elgg_extract_class(array $array, $existing = [], $extract_key = 'class') {
1005	$existing = empty($existing) ? [] : (array) $existing;
1006
1007	$merge = (array) elgg_extract($extract_key, $array, []);
1008
1009	array_splice($existing, count($existing), 0, $merge);
1010
1011	return array_values(array_unique($existing));
1012}
1013
1014/**
1015 * Calls a callable autowiring the arguments using public DI services
1016 * and applying logic based on flags
1017 *
1018 * @param int     $flags   Bitwise flags
1019 *                         ELGG_IGNORE_ACCESS
1020 *                         ELGG_ENFORCE_ACCESS
1021 *                         ELGG_SHOW_DISABLED_ENTITIES
1022 *                         ELGG_HIDE_DISABLED_ENTITIES
1023 * @param Closure $closure Callable to call
1024 *
1025 * @return mixed
1026 */
1027function elgg_call(int $flags, Closure $closure) {
1028	return _elgg_services()->invoker->call($flags, $closure);
1029}
1030
1031/**
1032 * Returns a PHP INI setting in bytes.
1033 *
1034 * @tip Use this for arithmetic when determining if a file can be uploaded.
1035 *
1036 * @param string $setting The php.ini setting
1037 *
1038 * @return int
1039 * @since 1.7.0
1040 * @link http://www.php.net/manual/en/function.ini-get.php
1041 */
1042function elgg_get_ini_setting_in_bytes($setting) {
1043	// retrieve INI setting
1044	$val = ini_get($setting);
1045
1046	// convert INI setting when shorthand notation is used
1047	$last = strtolower($val[strlen($val) - 1]);
1048	if (in_array($last, ['g', 'm', 'k'])) {
1049		$val = substr($val, 0, -1);
1050	}
1051	$val = (int) $val;
1052	switch ($last) {
1053		case 'g':
1054			$val *= 1024;
1055			// fallthrough intentional
1056		case 'm':
1057			$val *= 1024;
1058			// fallthrough intentional
1059		case 'k':
1060			$val *= 1024;
1061	}
1062
1063	// return byte value
1064	return $val;
1065}
1066
1067/**
1068 * Get the global service provider
1069 *
1070 * @return \Elgg\Di\ServiceProvider
1071 * @internal
1072 */
1073function _elgg_services() {
1074	// This yields a more shallow stack depth in recursive APIs like views. This aids in debugging and
1075	// reduces false positives in xdebug's infinite recursion protection.
1076	return \Elgg\Application::$_instance->_services;
1077}
1078
1079/**
1080 * Serve individual views for Ajax.
1081 *
1082 * /ajax/view/<view_name>?<key/value params>
1083 * /ajax/form/<action_name>?<key/value params>
1084 *
1085 * @param string[] $segments URL segments (not including "ajax")
1086 * @return false|ResponseBuilder
1087 *
1088 * @see elgg_register_ajax_view()
1089 * @elgg_pagehandler ajax
1090 * @internal
1091 */
1092function _elgg_ajax_page_handler($segments) {
1093	elgg_ajax_gatekeeper();
1094
1095	if (count($segments) < 2) {
1096		return elgg_error_response("Ajax pagehandler called with invalid segments", REFERRER, ELGG_HTTP_BAD_REQUEST);
1097	}
1098
1099	if ($segments[0] === 'view' || $segments[0] === 'form') {
1100		if ($segments[0] === 'view') {
1101			if ($segments[1] === 'admin') {
1102				// protect admin views similar to all admin pages that are protected automatically in the admin_page_handler
1103				elgg_admin_gatekeeper();
1104			}
1105			// ignore 'view/'
1106			$view = implode('/', array_slice($segments, 1));
1107		} else {
1108			if ($segments[1] === 'admin') {
1109				// protect admin forms similar to all admin pages that are protected automatically in the admin_page_handler
1110				elgg_admin_gatekeeper();
1111			}
1112			// form views start with "forms", not "form"
1113			$view = 'forms/' . implode('/', array_slice($segments, 1));
1114		}
1115
1116		$ajax_api = _elgg_services()->ajax;
1117		$allowed_views = $ajax_api->getViews();
1118
1119		// cacheable views are always allowed
1120		if (!in_array($view, $allowed_views) && !_elgg_services()->views->isCacheableView($view)) {
1121			return elgg_error_response("Ajax view '$view' was not registered", REFERRER, ELGG_HTTP_FORBIDDEN);
1122		}
1123
1124		if (!elgg_view_exists($view)) {
1125			return elgg_error_response("Ajax view '$view' was not found", REFERRER, ELGG_HTTP_NOT_FOUND);
1126		}
1127
1128		// pull out GET parameters through filter
1129		$vars = [];
1130		foreach (_elgg_services()->request->query->keys() as $name) {
1131			$vars[$name] = get_input($name);
1132		}
1133
1134		if (isset($vars['guid'])) {
1135			$vars['entity'] = get_entity($vars['guid']);
1136		}
1137
1138		if (isset($vars['river_id'])) {
1139			$vars['item'] = elgg_get_river_item_from_id($vars['river_id']);
1140		}
1141
1142		$content_type = '';
1143		if ($segments[0] === 'view') {
1144			$output = elgg_view($view, $vars);
1145
1146			// Try to guess the mime-type
1147			switch ($segments[1]) {
1148				case "js":
1149					$content_type = 'text/javascript;charset=utf-8';
1150					break;
1151				case "css":
1152					$content_type = 'text/css;charset=utf-8';
1153					break;
1154				default :
1155					if (_elgg_services()->views->isCacheableView($view)) {
1156						$file = _elgg_services()->views->findViewFile($view, elgg_get_viewtype());
1157						$content_type = 'text/html';
1158						try {
1159							$content_type = elgg()->mimetype->getMimeType($file, $content_type);
1160						} catch (InvalidArgumentException $e) {
1161							// nothing for now
1162						}
1163					}
1164					break;
1165			}
1166		} else {
1167			$action = implode('/', array_slice($segments, 1));
1168			$output = elgg_view_form($action, ['prevent_double_submit' => true], $vars);
1169		}
1170
1171		if ($content_type) {
1172			elgg_set_http_header("Content-Type: $content_type");
1173		}
1174
1175		return elgg_ok_response($output);
1176	}
1177
1178	return false;
1179}
1180
1181/**
1182 * Checks if there are some constraints on the options array for
1183 * potentially dangerous operations.
1184 *
1185 * @param array  $options Options array
1186 * @param string $type    Options type: metadata, annotation or river
1187 *
1188 * @return bool
1189 * @internal
1190 */
1191function _elgg_is_valid_options_for_batch_operation($options, $type) {
1192	if (empty($options) || !is_array($options)) {
1193		return false;
1194	}
1195
1196	// at least one of these is required.
1197	$required = [
1198		// generic restraints
1199		'guid', 'guids'
1200	];
1201
1202	switch ($type) {
1203		case 'metadata':
1204			$metadata_required = [
1205				'metadata_name', 'metadata_names',
1206				'metadata_value', 'metadata_values'
1207			];
1208
1209			$required = array_merge($required, $metadata_required);
1210			break;
1211
1212		case 'annotations':
1213		case 'annotation':
1214			$annotations_required = [
1215				'annotation_owner_guid', 'annotation_owner_guids',
1216				'annotation_name', 'annotation_names',
1217				'annotation_value', 'annotation_values'
1218			];
1219
1220			$required = array_merge($required, $annotations_required);
1221			break;
1222
1223		case 'river':
1224			// overriding generic restraints as guids isn't supported in river
1225			$required = [
1226				'id', 'ids',
1227				'subject_guid', 'subject_guids',
1228				'object_guid', 'object_guids',
1229				'target_guid', 'target_guids',
1230				'annotation_id', 'annotation_ids',
1231				'view', 'views',
1232			];
1233			break;
1234
1235		default:
1236			return false;
1237	}
1238
1239	foreach ($required as $key) {
1240		// check that it exists and is something.
1241		if (isset($options[$key]) && $options[$key]) {
1242			return true;
1243		}
1244	}
1245
1246	return false;
1247}
1248
1249/**
1250 * Checks the status of the Walled Garden and forwards to a login page
1251 * if required.
1252 *
1253 * If the site is in Walled Garden mode, all page except those registered as
1254 * plugin pages by {@elgg_hook public_pages walled_garden} will redirect to
1255 * a login page.
1256 *
1257 * @since 1.8.0
1258 * @elgg_event_handler init system
1259 * @return void
1260 * @internal
1261 */
1262function _elgg_walled_garden_init() {
1263	if (!_elgg_config()->walled_garden) {
1264		return;
1265	}
1266
1267	elgg_register_external_file('css', 'elgg.walled_garden', elgg_get_simplecache_url('walled_garden.css'));
1268
1269	elgg_register_plugin_hook_handler('register', 'menu:walled_garden', '_elgg_walled_garden_menu');
1270
1271	if (_elgg_config()->default_access == ACCESS_PUBLIC) {
1272		elgg_set_config('default_access', ACCESS_LOGGED_IN);
1273	}
1274
1275	elgg_register_plugin_hook_handler('access:collections:write', 'all', '_elgg_walled_garden_remove_public_access', 9999);
1276
1277	if (!elgg_is_logged_in()) {
1278		// override the front page
1279		elgg_register_route('index', [
1280			'path' => '/',
1281			'resource' => 'walled_garden',
1282		]);
1283	}
1284}
1285
1286/**
1287 * Adds home link to walled garden menu
1288 *
1289 * @param \Elgg\Hook $hook 'register', 'menu:walled_garden'
1290 *
1291 * @return array
1292 *
1293 * @internal
1294 */
1295function _elgg_walled_garden_menu(\Elgg\Hook $hook) {
1296
1297	if (current_page_url() === elgg_get_site_url()) {
1298		return;
1299	}
1300
1301	$return_value = $hook->getValue();
1302
1303	$return_value[] = \ElggMenuItem::factory([
1304		'name' => 'home',
1305		'href' => '/',
1306		'text' => elgg_echo('walled_garden:home'),
1307		'priority' => 10,
1308	]);
1309
1310	return $return_value;
1311}
1312
1313/**
1314 * Remove public access for walled gardens
1315 *
1316 * @param \Elgg\Hook $hook 'access:collections:write', 'all'
1317 *
1318 * @return array
1319 *
1320 * @internal
1321 */
1322function _elgg_walled_garden_remove_public_access(\Elgg\Hook $hook) {
1323	$accesses = $hook->getValue();
1324	if (isset($accesses[ACCESS_PUBLIC])) {
1325		unset($accesses[ACCESS_PUBLIC]);
1326	}
1327	return $accesses;
1328}
1329
1330/**
1331 * Adds the manifest.json to head links
1332 *
1333 * @param \Elgg\Hook $hook 'head', 'page'
1334 *
1335 * @return array
1336 *
1337 * @internal
1338 * @since 3.1
1339 */
1340function _elgg_head_manifest(\Elgg\Hook $hook) {
1341	$result = $hook->getValue();
1342	$result['links']['manifest'] = [
1343		'rel' => 'manifest',
1344		'href' => elgg_get_simplecache_url('resources/manifest.json'),
1345	];
1346
1347	return $result;
1348}
1349
1350/**
1351 * Elgg's main init.
1352 *
1353 * Handles core actions, the JS pagehandler, and the shutdown function.
1354 *
1355 * @elgg_event_handler init system
1356 * @return void
1357 * @internal
1358 */
1359function _elgg_init() {
1360	elgg_register_simplecache_view('resources/manifest.json');
1361
1362	elgg_register_plugin_hook_handler('head', 'page', '_elgg_head_manifest');
1363
1364	if (_elgg_config()->enable_profiling) {
1365		/**
1366		 * @see \Elgg\Profiler::handlePageOutput
1367		 */
1368		elgg_register_plugin_hook_handler('output', 'page', [\Elgg\Profiler::class, 'handlePageOutput'], 999);
1369	}
1370
1371	elgg_register_plugin_hook_handler('seeds', 'database', '_elgg_db_register_seeds', 1);
1372}
1373
1374/**
1375 * Register core routes
1376 * @return void
1377 * @internal
1378 */
1379function _elgg_register_routes() {
1380	$conf = \Elgg\Project\Paths::elgg() . 'engine/routes.php';
1381	$routes = \Elgg\Includer::includeFile($conf);
1382
1383	foreach ($routes as $name => $def) {
1384		elgg_register_route($name, $def);
1385	}
1386}
1387
1388/**
1389 * Register core actions
1390 * @return void
1391 * @internal
1392 */
1393function _elgg_register_actions() {
1394	$conf = \Elgg\Project\Paths::elgg() . 'engine/actions.php';
1395	$actions = \Elgg\Includer::includeFile($conf);
1396
1397	$root_path = \Elgg\Project\Paths::elgg();
1398
1399	foreach ($actions as $action => $action_spec) {
1400		if (!is_array($action_spec)) {
1401			continue;
1402		}
1403
1404		$access = elgg_extract('access', $action_spec, 'logged_in');
1405		$handler = elgg_extract('controller', $action_spec);
1406		if (!$handler) {
1407			$handler = elgg_extract('filename', $action_spec);
1408			if (!$handler) {
1409				$handler = "$root_path/actions/{$action}.php";
1410			}
1411		}
1412
1413		elgg_register_action($action, $handler, $access);
1414	}
1415}
1416
1417/**
1418 * Register database seeds
1419 *
1420 * @elgg_plugin_hook seeds database
1421 *
1422 * @param \Elgg\Hook $hook Hook
1423 * @return array
1424 */
1425function _elgg_db_register_seeds(\Elgg\Hook $hook) {
1426
1427	$seeds = $hook->getValue();
1428
1429	$seeds[] = \Elgg\Database\Seeds\Users::class;
1430	$seeds[] = \Elgg\Database\Seeds\Groups::class;
1431
1432	return $seeds;
1433}
1434
1435/**
1436 * @see \Elgg\Application::loadCore Do not do work here. Just register for events.
1437 */
1438return function(\Elgg\EventsService $events) {
1439	$events->registerHandler('init', 'system', '_elgg_init');
1440	$events->registerHandler('init', 'system', '_elgg_walled_garden_init', 1000);
1441};
1442