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

engine/lib/elgglib.php (3 issues)

Code
1
<?php
2
3
use Elgg\Http\ResponseBuilder;
4
5
/**
6
 * Bootstrapping and helper procedural code available for use in Elgg core and plugins.
7
 *
8
 * @package Elgg.Core
9
 * @todo These functions can't be subpackaged because they cover a wide mix of
10
 * purposes and subsystems.  Many of them should be moved to more relevant files.
11
 */
12
13
/**
14
 * Get a reference to the global Application object
15
 *
16
 * @return Elgg\Application
17
 * @since 2.0.0
18
 */
19
function elgg() {
20 5390
	if (!isset(Elgg\Application::$_instance)) {
21
		throw new \RuntimeException(__FUNCTION__ . ' should not be called before an instanceof ' . \Elgg\Application::class . ' is bootstrapped');
22
	}
23
24 5390
	return Elgg\Application::$_instance;
25
}
26
27
/**
28
 * Register a PHP file as a library.
29
 *
30
 * @see elgg_load_library()
31
 *
32
 * @param string $name     The name of the library
33
 * @param string $location The location of the file
34
 *
35
 * @return void
36
 * @since 1.8.0
37
 */
38
function elgg_register_library($name, $location) {
39 31
	$libraries = _elgg_config()->libraries;
40 31
	if ($libraries === null) {
41 18
		$libraries = [];
42
	}
43 31
	$libraries[$name] = $location;
44 31
	_elgg_config()->libraries = $libraries;
45 31
}
46
47
/**
48
 * Load a PHP library.
49
 *
50
 * @see elgg_register_library()
51
 *
52
 * @param string $name The name of the library
53
 *
54
 * @return void
55
 * @throws InvalidParameterException
56
 * @since 1.8.0
57
 */
58
function elgg_load_library($name) {
59 54
	static $loaded_libraries = [];
60
61 54
	if (in_array($name, $loaded_libraries)) {
62 54
		return;
63
	}
64
65 1
	$libraries = _elgg_config()->libraries;
66
67 1
	if (!isset($libraries[$name])) {
68
		$error = "$name is not a registered library";
69
		throw new \InvalidParameterException($error);
70
	}
71
72 1
	if (!include_once($libraries[$name])) {
73
		$error = "Could not load the $name library from {$libraries[$name]}";
74
		throw new \InvalidParameterException($error);
75
	}
76
77 1
	$loaded_libraries[] = $name;
78 1
}
79
80
/**
81
 * Forward to $location.
82
 *
83
 * Sends a 'Location: $location' header and exits.  If headers have already been sent, throws an exception.
84
 *
85
 * @param string $location URL to forward to browser to. This can be a path
86
 *                         relative to the network's URL.
87
 * @param string $reason   Short explanation for why we're forwarding. Set to
88
 *                         '404' to forward to error page. Default message is
89
 *                         'system'.
90
 *
91
 * @return void
92
 * @throws SecurityException|InvalidParameterException
93
 */
94
function forward($location = "", $reason = 'system') {
95
	if (headers_sent($file, $line)) {
96
		throw new \SecurityException("Redirect could not be issued due to headers already being sent. Halting execution for security. "
97
			. "Output started in file $file at line $line. Search http://learn.elgg.org/ for more information.");
98
	}
99
100
	_elgg_services()->responseFactory->redirect($location, $reason);
101
	exit;
102
}
103
104
/**
105
 * Set a response HTTP header
106
 *
107
 * @see header()
108
 *
109
 * @param string $header  Header
110
 * @param bool   $replace Replace existing header
111
 * @return void
112
 * @since 2.3
113
 */
114
function elgg_set_http_header($header, $replace = true) {
115 27
	if (headers_sent($file, $line)) {
116 27
		_elgg_services()->logger->error("Cannot modify header information - headers already sent by
117 27
			(output started at $file:$line)");
118
	} else {
119
		header($header, $replace);
120
	}
121
122 27
	if (!preg_match('~^HTTP/\\d\\.\\d~', $header)) {
123 27
		list($name, $value) = explode(':', $header, 2);
124 27
		_elgg_services()->responseFactory->setHeader($name, ltrim($value), $replace);
125
	}
126 27
}
127
128
/**
129
 * Register a JavaScript file for inclusion
130
 *
131
 * This function handles adding JavaScript to a web page. If multiple
132
 * calls are made to register the same JavaScript file based on the $id
133
 * variable, only the last file is included. This allows a plugin to add
134
 * JavaScript from a view that may be called more than once. It also handles
135
 * more than one plugin adding the same JavaScript.
136
 *
137
 * jQuery plugins often have filenames such as jquery.rating.js. A best practice
138
 * is to base $name on the filename: "jquery.rating". It is recommended to not
139
 * use version numbers in the name.
140
 *
141
 * The JavaScript files can be local to the server or remote (such as
142
 * Google's CDN).
143
 *
144
 * @note Since 2.0, scripts with location "head" will also be output in the footer, but before
145
 *       those with location "footer".
146
 *
147
 * @param string $name     An identifier for the JavaScript library
148
 * @param string $url      URL of the JavaScript file
149
 * @param string $location Page location: head or footer. (default: head)
150
 * @param int    $priority Priority of the JS file (lower numbers load earlier)
151
 *
152
 * @return bool
153
 * @since 1.8.0
154
 */
155
function elgg_register_js($name, $url, $location = 'head', $priority = null) {
156 35
	return elgg_register_external_file('js', $name, $url, $location, $priority);
157
}
158
159
/**
160
 * Defines a JS lib as an AMD module. This is useful for shimming
161
 * traditional JS or for setting the paths of AMD modules.
162
 *
163
 * Calling multiple times for the same name will:
164
 *     * set the preferred path to the last call setting a path
165
 *     * overwrite the shimmed AMD modules with the last call setting a shimmed module
166
 *
167
 * Use elgg_require_js($name) to load on the current page.
168
 *
169
 * Calling this function is not needed if your JS are in views named like `module/name.js`
170
 * Instead, simply call elgg_require_js("module/name").
171
 *
172
 * @note The configuration is cached in simplecache, so logic should not depend on user-
173
 *       specific values like get_language().
174
 *
175
 * @param string $name   The module name
176
 * @param array  $config An array like the following:
177
 *                       array  'deps'    An array of AMD module dependencies
178
 *                       string 'exports' The name of the exported module
179
 *                       string 'src'     The URL to the JS. Can be relative.
180
 *
181
 * @return void
182
 */
183
function elgg_define_js($name, $config) {
184 31
	$src = elgg_extract('src', $config);
185
186 31
	if ($src) {
187 18
		$url = elgg_normalize_url($src);
188 18
		_elgg_services()->amdConfig->addPath($name, $url);
189
	}
190
191
	// shimmed module
192 31
	if (isset($config['deps']) || isset($config['exports'])) {
193 31
		_elgg_services()->amdConfig->addShim($name, $config);
194
	}
195 31
}
196
197
/**
198
 * Unregister a JavaScript file
199
 *
200
 * @param string $name The identifier for the JavaScript library
201
 *
202
 * @return bool
203
 * @since 1.8.0
204
 */
205
function elgg_unregister_js($name) {
206 1
	return elgg_unregister_external_file('js', $name);
207
}
208
209
/**
210
 * Load a JavaScript resource on this page
211
 *
212
 * This must be called before elgg_view_page(). It can be called before the
213
 * script is registered. If you do not want a script loaded, unregister it.
214
 *
215
 * @param string $name Identifier of the JavaScript resource
216
 *
217
 * @return void
218
 * @since 1.8.0
219
 */
220
function elgg_load_js($name) {
221 21
	elgg_load_external_file('js', $name);
222 21
}
223
224
225
/**
226
 * Request that Elgg load an AMD module onto the page.
227
 *
228
 * @param string $name The AMD module name.
229
 * @return void
230
 * @since 1.9.0
231
 */
232
function elgg_require_js($name) {
233 4
	_elgg_services()->amdConfig->addDependency($name);
234 4
}
235
236
/**
237
 * Cancel a request to load an AMD module onto the page.
238
 *
239
 * @note The elgg, jquery, and jquery-ui modules cannot be cancelled.
240
 *
241
 * @param string $name The AMD module name.
242
 * @return void
243
 * @since 2.1.0
244
 */
245
function elgg_unrequire_js($name) {
246
	_elgg_services()->amdConfig->removeDependency($name);
247
}
248
249
/**
250
 * Get the JavaScript URLs that are loaded
251
 *
252
 * @param string $location 'head' or 'footer'
253
 *
254
 * @return array
255
 * @since 1.8.0
256
 */
257
function elgg_get_loaded_js($location = 'head') {
258 4
	return elgg_get_loaded_external_files('js', $location);
259
}
260
261
/**
262
 * Register a CSS file for inclusion in the HTML head
263
 *
264
 * @param string $name     An identifier for the CSS file
265
 * @param string $url      URL of the CSS file
266
 * @param int    $priority Priority of the CSS file (lower numbers load earlier)
267
 *
268
 * @return bool
269
 * @since 1.8.0
270
 */
271
function elgg_register_css($name, $url, $priority = null) {
272 32
	return elgg_register_external_file('css', $name, $url, 'head', $priority);
273
}
274
275
/**
276
 * Unregister a CSS file
277
 *
278
 * @param string $name The identifier for the CSS file
279
 *
280
 * @return bool
281
 * @since 1.8.0
282
 */
283
function elgg_unregister_css($name) {
284
	return elgg_unregister_external_file('css', $name);
285
}
286
287
/**
288
 * Load a CSS file for this page
289
 *
290
 * This must be called before elgg_view_page(). It can be called before the
291
 * CSS file is registered. If you do not want a CSS file loaded, unregister it.
292
 *
293
 * @param string $name Identifier of the CSS file
294
 *
295
 * @return void
296
 * @since 1.8.0
297
 */
298
function elgg_load_css($name) {
299 21
	elgg_load_external_file('css', $name);
300 21
}
301
302
/**
303
 * Get the loaded CSS URLs
304
 *
305
 * @return array
306
 * @since 1.8.0
307
 */
308
function elgg_get_loaded_css() {
309 1
	return elgg_get_loaded_external_files('css', 'head');
310
}
311
312
/**
313
 * Core registration function for external files
314
 *
315
 * @param string $type     Type of external resource (js or css)
316
 * @param string $name     Identifier used as key
317
 * @param string $url      URL
318
 * @param string $location Location in the page to include the file
319
 * @param int    $priority Loading priority of the file
320
 *
321
 * @return bool
322
 * @since 1.8.0
323
 */
324
function elgg_register_external_file($type, $name, $url, $location, $priority = 500) {
325 36
	return _elgg_services()->externalFiles->register($type, $name, $url, $location, $priority);
326
}
327
328
/**
329
 * Unregister an external file
330
 *
331
 * @param string $type Type of file: js or css
332
 * @param string $name The identifier of the file
333
 *
334
 * @return bool
335
 * @since 1.8.0
336
 */
337
function elgg_unregister_external_file($type, $name) {
338 1
	return _elgg_services()->externalFiles->unregister($type, $name);
339
}
340
341
/**
342
 * Load an external resource for use on this page
343
 *
344
 * @param string $type Type of file: js or css
345
 * @param string $name The identifier for the file
346
 *
347
 * @return void
348
 * @since 1.8.0
349
 */
350
function elgg_load_external_file($type, $name) {
351 23
	_elgg_services()->externalFiles->load($type, $name);
352 23
}
353
354
/**
355
 * Get external resource descriptors
356
 *
357
 * @param string $type     Type of file: js or css
358
 * @param string $location Page location
359
 *
360
 * @return array
361
 * @since 1.8.0
362
 */
363
function elgg_get_loaded_external_files($type, $location) {
364 5
	return _elgg_services()->externalFiles->getLoadedFiles($type, $location);
365
}
366
367
/**
368
 * Returns a list of files in $directory.
369
 *
370
 * Only returns files.  Does not recurse into subdirs.
371
 *
372
 * @param string $directory  Directory to look in
373
 * @param array  $exceptions Array of filenames to ignore
374
 * @param array  $list       Array of files to append to
375
 * @param mixed  $extensions Array of extensions to allow, null for all. Use a dot: array('.php').
376
 *
377
 * @return array Filenames in $directory, in the form $directory/filename.
378
 */
379
function elgg_get_file_list($directory, $exceptions = [], $list = [], $extensions = null) {
380
381
	$directory = \Elgg\Project\Paths::sanitize($directory);
382
	if ($handle = opendir($directory)) {
383
		while (($file = readdir($handle)) !== false) {
384
			if (!is_file($directory . $file) || in_array($file, $exceptions)) {
385
				continue;
386
			}
387
388
			if (is_array($extensions)) {
389
				if (in_array(strrchr($file, '.'), $extensions)) {
390
					$list[] = $directory . $file;
391
				}
392
			} else {
393
				$list[] = $directory . $file;
394
			}
395
		}
396
		closedir($handle);
397
	}
398
399
	return $list;
400
}
401
402
/**
403
 * Counts the number of messages, either globally or in a particular register
404
 *
405
 * @param string $register Optionally, the register
406
 *
407
 * @return integer The number of messages
408
 */
409
function count_messages($register = "") {
410
	return _elgg_services()->systemMessages->count($register);
411
}
412
413
/**
414
 * Display a system message on next page load.
415
 *
416
 * @param string|array $message Message or messages to add
417
 *
418
 * @return bool
419
 */
420
function system_message($message) {
421 16
	_elgg_services()->systemMessages->addSuccessMessage($message);
422 16
	return true;
423
}
424
425
/**
426
 * Display an error on next page load.
427
 *
428
 * @param string|array $error Error or errors to add
429
 *
430
 * @return bool
431
 */
432
function register_error($error) {
433 39
	_elgg_services()->systemMessages->addErrorMessage($error);
434 39
	return true;
435
}
436
437
/**
438
 * Get a copy of the current system messages.
439
 *
440
 * @return \Elgg\SystemMessages\RegisterSet
441
 * @since 2.1
442
 */
443
function elgg_get_system_messages() {
444
	return _elgg_services()->systemMessages->loadRegisters();
445
}
446
447
/**
448
 * Set the system messages. This will overwrite the state of all messages and errors!
449
 *
450
 * @param \Elgg\SystemMessages\RegisterSet $set Set of messages
451
 * @return void
452
 * @since 2.1
453
 */
454
function elgg_set_system_messages(\Elgg\SystemMessages\RegisterSet $set) {
455
	_elgg_services()->systemMessages->saveRegisters($set);
456
}
457
458
/**
459
 * Register a callback as an Elgg event handler.
460
 *
461
 * Events are emitted by Elgg when certain actions occur.  Plugins
462
 * can respond to these events or halt them completely by registering a handler
463
 * as a callback to an event.  Multiple handlers can be registered for
464
 * the same event and will be executed in order of $priority.
465
 *
466
 * For most events, any handler returning false will halt the execution chain and
467
 * cause the event to be "cancelled". For After Events, the return values of the
468
 * handlers will be ignored and all handlers will be called.
469
 *
470
 * This function is called with the event name, event type, and handler callback name.
471
 * Setting the optional $priority allows plugin authors to specify when the
472
 * callback should be run.  Priorities for plugins should be 1-1000.
473
 *
474
 * The callback is passed 3 arguments when called: $event, $type, and optional $params.
475
 *
476
 * $event is the name of event being emitted.
477
 * $type is the type of event or object concerned.
478
 * $params is an optional parameter passed that can include a related object.  See
479
 * specific event documentation for details on which events pass what parameteres.
480
 *
481
 * @tip If a priority isn't specified it is determined by the order the handler was
482
 * registered relative to the event and type.  For plugins, this generally means
483
 * the earlier the plugin is in the load order, the earlier the priorities are for
484
 * any event handlers.
485
 *
486
 * @tip $event and $object_type can use the special keyword 'all'.  Handler callbacks registered
487
 * with $event = all will be called for all events of type $object_type.  Similarly,
488
 * callbacks registered with $object_type = all will be called for all events of type
489
 * $event, regardless of $object_type.  If $event and $object_type both are 'all', the
490
 * handler callback will be called for all events.
491
 *
492
 * @tip Event handler callbacks are considered in the follow order:
493
 *  - Specific registration where 'all' isn't used.
494
 *  - Registration where 'all' is used for $event only.
495
 *  - Registration where 'all' is used for $type only.
496
 *  - Registration where 'all' is used for both.
497
 *
498
 * @warning If you use the 'all' keyword, you must have logic in the handler callback to
499
 * test the passed parameters before taking an action.
500
 *
501
 * @tip When referring to events, the preferred syntax is "event, type".
502
 *
503
 * @param string $event       The event type
504
 * @param string $object_type The object type
505
 * @param string $callback    The handler callback
506
 * @param int    $priority    The priority - 0 is default, negative before, positive after
507
 *
508
 * @return bool
509
 * @example documentation/events/basic.php
510
 * @example documentation/events/advanced.php
511
 * @example documentation/events/all.php
512
 */
513
function elgg_register_event_handler($event, $object_type, $callback, $priority = 500) {
514 105
	return _elgg_services()->hooks->getEvents()->registerHandler($event, $object_type, $callback, $priority);
515
}
516
517
/**
518
 * Unregisters a callback for an event.
519
 *
520
 * @param string $event       The event type
521
 * @param string $object_type The object type
522
 * @param string $callback    The callback. Since 1.11, static method callbacks will match dynamic methods
523
 *
524
 * @return bool true if a handler was found and removed
525
 * @since 1.7
526
 */
527
function elgg_unregister_event_handler($event, $object_type, $callback) {
528 12
	return _elgg_services()->hooks->getEvents()->unregisterHandler($event, $object_type, $callback);
529
}
530
531
/**
532
 * Clears all callback registrations for a event.
533
 *
534
 * @param string $event       The name of the event
535
 * @param string $object_type The objecttype of the event
536
 *
537
 * @return void
538
 * @since 2.3
539
 */
540
function elgg_clear_event_handlers($event, $object_type) {
541
	_elgg_services()->hooks->getEvents()->clearHandlers($event, $object_type);
542
}
543
544
/**
545
 * Trigger an Elgg Event and attempt to run all handler callbacks registered to that
546
 * event, type.
547
 *
548
 * This function attempts to run all handlers registered to $event, $object_type or
549
 * the special keyword 'all' for either or both. If a handler returns false, the
550
 * event will be cancelled (no further handlers will be called, and this function
551
 * will return false).
552
 *
553
 * $event is usually a verb: create, update, delete, annotation.
554
 *
555
 * $object_type is usually a noun: object, group, user, annotation, relationship, metadata.
556
 *
557
 * $object is usually an Elgg* object associated with the event.
558
 *
559
 * @warning Elgg events should only be triggered by core.  Plugin authors should use
560
 * {@link trigger_elgg_plugin_hook()} instead.
561
 *
562
 * @tip When referring to events, the preferred syntax is "event, type".
563
 *
564
 * @note Internal: Only rarely should events be changed, added, or removed in core.
565
 * When making changes to events, be sure to first create a ticket on Github.
566
 *
567
 * @note Internal: @tip Think of $object_type as the primary namespace element, and
568
 * $event as the secondary namespace.
569
 *
570
 * @param string $event       The event type
571
 * @param string $object_type The object type
572
 * @param mixed  $object      The object involved in the event
573
 *
574
 * @return bool False if any handler returned false, otherwise true.
575
 * @example documentation/examples/events/trigger.php
576
 */
577
function elgg_trigger_event($event, $object_type, $object = null) {
578 434
	return _elgg_services()->hooks->getEvents()->trigger($event, $object_type, $object);
579
}
580
581
/**
582
 * Trigger a "Before event" indicating a process is about to begin.
583
 *
584
 * Like regular events, a handler returning false will cancel the process and false
585
 * will be returned.
586
 *
587
 * To register for a before event, append ":before" to the event name when registering.
588
 *
589
 * @param string $event       The event type. The fired event type will be appended with ":before".
590
 * @param string $object_type The object type
591
 * @param mixed  $object      The object involved in the event
592
 *
593
 * @return bool False if any handler returned false, otherwise true
594
 *
595
 * @see elgg_trigger_event()
596
 * @see elgg_trigger_after_event()
597
 */
598
function elgg_trigger_before_event($event, $object_type, $object = null) {
599 107
	return _elgg_services()->hooks->getEvents()->triggerBefore($event, $object_type, $object);
600
}
601
602
/**
603
 * Trigger an "After event" indicating a process has finished.
604
 *
605
 * Unlike regular events, all the handlers will be called, their return values ignored.
606
 *
607
 * To register for an after event, append ":after" to the event name when registering.
608
 *
609
 * @param string $event       The event type. The fired event type will be appended with ":after".
610
 * @param string $object_type The object type
611
 * @param string $object      The object involved in the event
612
 *
613
 * @return true
614
 *
615
 * @see elgg_trigger_before_event()
616
 */
617
function elgg_trigger_after_event($event, $object_type, $object = null) {
618 279
	return _elgg_services()->hooks->getEvents()->triggerAfter($event, $object_type, $object);
619
}
620
621
/**
622
 * Trigger an event normally, but send a notice about deprecated use if any handlers are registered.
623
 *
624
 * @param string $event       The event type
625
 * @param string $object_type The object type
626
 * @param string $object      The object involved in the event
627
 * @param string $message     The deprecation message
628
 * @param string $version     Human-readable *release* version: 1.9, 1.10, ...
629
 *
630
 * @return bool
631
 *
632
 * @see elgg_trigger_event()
633
 */
634
function elgg_trigger_deprecated_event($event, $object_type, $object = null, $message = null, $version = null) {
635
	return _elgg_services()->hooks->getEvents()->triggerDeprecated($event, $object_type, $object, $message, $version);
636
}
637
638
/**
639
 * Register a callback as a plugin hook handler.
640
 *
641
 * Plugin hooks allow developers to losely couple plugins and features by
642
 * responding to and emitting {@link elgg_trigger_plugin_hook()} customizable hooks.
643
 * Handler callbacks can respond to the hook, change the details of the hook, or
644
 * ignore it.
645
 *
646
 * Multiple handlers can be registered for a plugin hook, and each callback
647
 * is called in order of priority.  If the return value of a handler is not
648
 * null, that value is passed to the next callback in the call stack.  When all
649
 * callbacks have been run, the final value is passed back to the caller
650
 * via {@link elgg_trigger_plugin_hook()}.
651
 *
652
 * Similar to Elgg Events, plugin hook handler callbacks are registered by passing
653
 * a hook, a type, and a priority.
654
 *
655
 * The callback is passed 4 arguments when called: $hook, $type, $value, and $params.
656
 *
657
 *  - str $hook The name of the hook.
658
 *  - str $type The type of hook.
659
 *  - mixed $value The return value of the last handler or the default
660
 *  value if no other handlers have been called.
661
 *  - mixed $params An optional array of parameters.  Used to provide additional
662
 *  information to plugins.
663
 *
664
 * @tip Plugin hooks are similar to Elgg Events in that Elgg emits
665
 * a plugin hook when certain actions occur, but a plugin hook allows you to alter the
666
 * parameters, as well as halt execution.
667
 *
668
 * @tip If a priority isn't specified it is determined by the order the handler was
669
 * registered relative to the event and type.  For plugins, this generally means
670
 * the earlier the plugin is in the load order, the earlier the priorities are for
671
 * any event handlers.
672
 *
673
 * @tip Like Elgg Events, $hook and $type can use the special keyword 'all'.
674
 * Handler callbacks registered with $hook = all will be called for all hooks
675
 * of type $type.  Similarly, handlers registered with $type = all will be
676
 * called for all hooks of type $event, regardless of $object_type.  If $hook
677
 * and $type both are 'all', the handler will be called for all hooks.
678
 *
679
 * @tip Plugin hooks are sometimes used to gather lists from plugins.  This is
680
 * usually done by pushing elements into an array passed in $params.  Be sure
681
 * to append to and then return $value so you don't overwrite other plugin's
682
 * values.
683
 *
684
 * @warning Unlike Elgg Events, a handler that returns false will NOT halt the
685
 * execution chain.
686
 *
687
 * @param string   $hook     The name of the hook
688
 * @param string   $type     The type of the hook
689
 * @param callable $callback The name of a valid function or an array with object and method
690
 * @param int      $priority The priority - 500 is default, lower numbers called first
691
 *
692
 * @return bool
693
 *
694
 * @example hooks/register/basic.php Registering for a plugin hook and examining the variables.
695
 * @example hooks/register/advanced.php Registering for a plugin hook and changing the params.
696
 * @since 1.8.0
697
 */
698
function elgg_register_plugin_hook_handler($hook, $type, $callback, $priority = 500) {
699 327
	return _elgg_services()->hooks->registerHandler($hook, $type, $callback, $priority);
700
}
701
702
/**
703
 * Unregister a callback as a plugin hook.
704
 *
705
 * @param string   $hook        The name of the hook
706
 * @param string   $entity_type The name of the type of entity (eg "user", "object" etc)
707
 * @param callable $callback    The PHP callback to be removed. Since 1.11, static method
708
 *                              callbacks will match dynamic methods
709
 *
710
 * @return void
711
 * @since 1.8.0
712
 */
713
function elgg_unregister_plugin_hook_handler($hook, $entity_type, $callback) {
714 238
	_elgg_services()->hooks->unregisterHandler($hook, $entity_type, $callback);
715 238
}
716
717
/**
718
 * Clears all callback registrations for a plugin hook.
719
 *
720
 * @param string $hook The name of the hook
721
 * @param string $type The type of the hook
722
 *
723
 * @return void
724
 * @since 2.0
725
 */
726
function elgg_clear_plugin_hook_handlers($hook, $type) {
727
	_elgg_services()->hooks->clearHandlers($hook, $type);
728
}
729
730
/**
731
 * Trigger a Plugin Hook and run all handler callbacks registered to that hook:type.
732
 *
733
 * This function runs all handlers registered to $hook, $type or
734
 * the special keyword 'all' for either or both.
735
 *
736
 * Use $params to send additional information to the handler callbacks.
737
 *
738
 * $returnvalue is the initial value to pass to the handlers, which can
739
 * change it by returning non-null values. It is useful to use $returnvalue
740
 * to set defaults. If no handlers are registered, $returnvalue is immediately
741
 * returned.
742
 *
743
 * Handlers that return null (or with no explicit return or return value) will
744
 * not change the value of $returnvalue.
745
 *
746
 * $hook is usually a verb: import, get_views, output.
747
 *
748
 * $type is usually a noun: user, ecml, page.
749
 *
750
 * @tip Like Elgg Events, $hook and $type can use the special keyword 'all'.
751
 * Handler callbacks registered with $hook = all will be called for all hooks
752
 * of type $type.  Similarly, handlers registered with $type = all will be
753
 * called for all hooks of type $event, regardless of $object_type.  If $hook
754
 * and $type both are 'all', the handler will be called for all hooks.
755
 *
756
 * @tip It's not possible for a plugin hook to change a non-null $returnvalue
757
 * to null.
758
 *
759
 * @note Internal: The checks for $hook and/or $type not being equal to 'all' is to
760
 * prevent a plugin hook being registered with an 'all' being called more than
761
 * once if the trigger occurs with an 'all'. An example in core of this is in
762
 * actions.php:
763
 * elgg_trigger_plugin_hook('action_gatekeeper:permissions:check', 'all', ...)
764
 *
765
 * @see elgg_register_plugin_hook_handler()
766
 *
767
 * @param string $hook        The name of the hook to trigger ("all" will
768
 *                            trigger for all $types regardless of $hook value)
769
 * @param string $type        The type of the hook to trigger ("all" will
770
 *                            trigger for all $hooks regardless of $type value)
771
 * @param mixed  $params      Additional parameters to pass to the handlers
772
 * @param mixed  $returnvalue An initial return value
773
 *
774
 * @return mixed|null The return value of the last handler callback called
775
 *
776
 * @example hooks/trigger/basic.php    Trigger a hook that determines if execution
777
 *                                     should continue.
778
 * @example hooks/trigger/advanced.php Trigger a hook with a default value and use
779
 *                                     the results to populate a menu.
780
 * @example hooks/basic.php            Trigger and respond to a basic plugin hook.
781
 *
782
 * @since 1.8.0
783
 */
784
function elgg_trigger_plugin_hook($hook, $type, $params = null, $returnvalue = null) {
785 1332
	return _elgg_services()->hooks->trigger($hook, $type, $params, $returnvalue);
786
}
787
788
/**
789
 * Returns an ordered array of hook handlers registered for $hook and $type.
790
 *
791
 * @param string $hook Hook name
792
 * @param string $type Hook type
793
 *
794
 * @return array
795
 *
796
 * @since 2.0.0
797
 */
798
function elgg_get_ordered_hook_handlers($hook, $type) {
799
	return _elgg_services()->hooks->getOrderedHandlers($hook, $type);
800
}
801
802
/**
803
 * Returns an ordered array of event handlers registered for $event and $type.
804
 *
805
 * @param string $event Event name
806
 * @param string $type  Object type
807
 *
808
 * @return array
809
 *
810
 * @since 2.0.0
811
 */
812
function elgg_get_ordered_event_handlers($event, $type) {
813
	return _elgg_services()->hooks->getEvents()->getOrderedHandlers($event, $type);
814
}
815
816
/**
817
 * Log a message.
818
 *
819
 * If $level is >= to the debug setting in {@link $CONFIG->debug}, the
820
 * message will be sent to {@link elgg_dump()}.  Messages with lower
821
 * priority than {@link $CONFIG->debug} are ignored.
822
 *
823
 * @note Use the developers plugin to display logs
824
 *
825
 * @param string $message User message
826
 * @param string $level   NOTICE | WARNING | ERROR
827
 *
828
 * @return bool
829
 * @since 1.7.0
830
 */
831
function elgg_log($message, $level = 'NOTICE') {
832 1081
	static $levels = [
833
		'INFO' => 200,
834
		'NOTICE' => 250,
835
		'WARNING' => 300,
836
		'ERROR' => 400,
837
	];
838
839 1081
	if (!isset($levels[$level])) {
840
		throw new \InvalidArgumentException("Invalid \$level value");
841
	}
842
843 1081
	$level = $levels[$level];
844 1081
	return _elgg_services()->logger->log($message, $level);
845
}
846
847
/**
848
 * Logs $value to PHP's {@link error_log()}
849
 *
850
 * A {@elgg_plugin_hook debug log} is called.  If a handler returns
851
 * false, it will stop the default logging method.
852
 *
853
 * @note Use the developers plugin to display logs
854
 *
855
 * @param mixed $value The value
856
 * @return void
857
 * @since 1.7.0
858
 */
859
function elgg_dump($value) {
860
	_elgg_services()->logger->dump($value);
861
}
862
863
/**
864
 * Get the current Elgg version information
865
 *
866
 * @param bool $human_readable Whether to return a human readable version (default: false)
867
 *
868
 * @return string|false Depending on success
869
 * @since 1.9
870
 */
871
function elgg_get_version($human_readable = false) {
872 34
	static $version, $release;
873
	
874 34
	if (!isset($version) || !isset($release)) {
875
		$path = \Elgg\Application::elggDir()->getPath('version.php');
876
		if (!is_file($path)) {
877
			return false;
878
		}
879
		include $path;
880
	}
881
	
882 34
	return $human_readable ? $release : $version;
883
}
884
885
/**
886
 * Log a notice about deprecated use of a function, view, etc.
887
 *
888
 * @param string $msg             Message to log
889
 * @param string $dep_version     Human-readable *release* version: 1.7, 1.8, ...
890
 * @param int    $backtrace_level How many levels back to display the backtrace.
891
 *                                Useful if calling from functions that are called
892
 *                                from other places (like elgg_view()). Set to -1
893
 *                                for a full backtrace.
894
 *
895
 * @return bool
896
 * @since 1.7.0
897
 */
898
function elgg_deprecated_notice($msg, $dep_version, $backtrace_level = 1) {
899 656
	$backtrace_level += 1;
900 656
	return _elgg_services()->deprecation->sendNotice($msg, $dep_version, $backtrace_level);
901
}
902
903
/**
904
 * Builds a URL from the a parts array like one returned by {@link parse_url()}.
905
 *
906
 * @note If only partial information is passed, a partial URL will be returned.
907
 *
908
 * @param array $parts       Associative array of URL components like parse_url() returns
909
 *                           'user' and 'pass' parts are ignored because of security reasons
910
 * @param bool  $html_encode HTML Encode the url?
911
 *
912
 * @see https://github.com/Elgg/Elgg/pull/8146#issuecomment-91544585
913
 * @return string Full URL
914
 * @since 1.7.0
915
 */
916
function elgg_http_build_url(array $parts, $html_encode = true) {
917
	// build only what's given to us.
918 37
	$scheme = isset($parts['scheme']) ? "{$parts['scheme']}://" : '';
919 37
	$host = isset($parts['host']) ? "{$parts['host']}" : '';
920 37
	$port = isset($parts['port']) ? ":{$parts['port']}" : '';
921 37
	$path = isset($parts['path']) ? "{$parts['path']}" : '';
922 37
	$query = isset($parts['query']) ? "?{$parts['query']}" : '';
923 37
	$fragment = isset($parts['fragment']) ? "#{$parts['fragment']}" : '';
924
925 37
	$string = $scheme . $host . $port . $path . $query . $fragment;
926
927 37
	if ($html_encode) {
928 22
		return htmlspecialchars($string, ENT_QUOTES, 'UTF-8', false);
929
	} else {
930 15
		return $string;
931
	}
932
}
933
934
/**
935
 * Adds action tokens to URL
936
 *
937
 * As of 1.7.0 action tokens are required on all actions.
938
 * Use this function to append action tokens to a URL's GET parameters.
939
 * This will preserve any existing GET parameters.
940
 *
941
 * @note If you are using {@elgg_view input/form} you don't need to
942
 * add tokens to the action.  The form view automatically handles
943
 * tokens.
944
 *
945
 * @param string $url         Full action URL
946
 * @param bool   $html_encode HTML encode the url? (default: false)
947
 *
948
 * @return string URL with action tokens
949
 * @since 1.7.0
950
 */
951
function elgg_add_action_tokens_to_url($url, $html_encode = false) {
952
	$url = elgg_normalize_url($url);
953
	$components = parse_url($url);
954
955
	if (isset($components['query'])) {
956
		$query = elgg_parse_str($components['query']);
957
	} else {
958
		$query = [];
959
	}
960
961
	if (isset($query['__elgg_ts']) && isset($query['__elgg_token'])) {
962
		return $url;
963
	}
964
965
	// append action tokens to the existing query
966
	$query['__elgg_ts'] = time();
967
	$query['__elgg_token'] = generate_action_token($query['__elgg_ts']);
968
	$components['query'] = http_build_query($query);
969
970
	// rebuild the full url
971
	return elgg_http_build_url($components, $html_encode);
972
}
973
974
/**
975
 * Removes an element from a URL's query string.
976
 *
977
 * @note You can send a partial URL string.
978
 *
979
 * @param string $url     Full URL
980
 * @param string $element The element to remove
981
 *
982
 * @return string The new URL with the query element removed.
983
 * @since 1.7.0
984
 */
985
function elgg_http_remove_url_query_element($url, $element) {
986 7
	return elgg_http_add_url_query_elements($url, [$element => null]);
987
}
988
989
/**
990
 * Sets elements in a URL's query string.
991
 *
992
 * @param string $url      The URL
993
 * @param array  $elements Key/value pairs to set in the URL. If the value is null, the
994
 *                         element is removed from the URL.
995
 *
996
 * @return string The new URL with the query strings added
997
 * @since 1.7.0
998
 */
999
function elgg_http_add_url_query_elements($url, array $elements) {
1000 15
	$url_array = parse_url($url);
1001
1002 15
	if (isset($url_array['query'])) {
1003 5
		$query = elgg_parse_str($url_array['query']);
1004
	} else {
1005 10
		$query = [];
1006
	}
1007
1008 15
	foreach ($elements as $k => $v) {
1009 14
		if ($v === null) {
1010 10
			unset($query[$k]);
1011
		} else {
1012 14
			$query[$k] = $v;
1013
		}
1014
	}
1015
1016
	// why check path? A: if no path, this may be a relative URL like "?foo=1". In this case,
1017
	// the output "" would be interpreted the current URL, so in this case we *must* set
1018
	// a query to make sure elements are removed.
1019 15
	if ($query || empty($url_array['path'])) {
1020 14
		$url_array['query'] = http_build_query($query);
1021
	} else {
1022 1
		unset($url_array['query']);
1023
	}
1024 15
	$string = elgg_http_build_url($url_array, false);
1025
1026
	// Restore relative protocol to url if missing and is provided as part of the initial url (see #9874)
1027 15
	if (!isset($url['scheme']) && (substr($url, 0, 2) == '//')) {
1028
		$string = "//{$string}";
1029
	}
1030
	
1031 15
	return $string;
1032
}
1033
1034
/**
1035
 * Test if two URLs are functionally identical.
1036
 *
1037
 * @tip If $ignore_params is used, neither the name nor its value will be considered when comparing.
1038
 *
1039
 * @tip The order of GET params doesn't matter.
1040
 *
1041
 * @param string $url1          First URL
1042
 * @param string $url2          Second URL
1043
 * @param array  $ignore_params GET params to ignore in the comparison
1044
 *
1045
 * @return bool
1046
 * @since 1.8.0
1047
 */
1048
function elgg_http_url_is_identical($url1, $url2, $ignore_params = ['offset', 'limit']) {
1049 13
	$url1 = elgg_normalize_url($url1);
1050 13
	$url2 = elgg_normalize_url($url2);
1051
1052 13
	if ($url1 == $url2) {
1053
		return true;
1054
	}
1055
1056 13
	$url1_info = parse_url($url1);
1057 13
	$url2_info = parse_url($url2);
1058
1059 13
	if (isset($url1_info['path'])) {
1060
		$url1_info['path'] = trim($url1_info['path'], '/');
1061
	}
1062 13
	if (isset($url2_info['path'])) {
1063 13
		$url2_info['path'] = trim($url2_info['path'], '/');
1064
	}
1065
1066
	// compare basic bits
1067 13
	$parts = ['scheme', 'host', 'path'];
1068
1069 13
	foreach ($parts as $part) {
1070 13
		if ((isset($url1_info[$part]) && isset($url2_info[$part]))
1071 13
		&& $url1_info[$part] != $url2_info[$part]) {
1072 5
			return false;
1073 11
		} elseif (isset($url1_info[$part]) && !isset($url2_info[$part])) {
1074
			return false;
1075 11
		} elseif (!isset($url1_info[$part]) && isset($url2_info[$part])) {
1076 11
			return false;
1077
		}
1078
	}
1079
1080
	// quick compare of get params
1081
	if (isset($url1_info['query']) && isset($url2_info['query'])
1082
	&& $url1_info['query'] == $url2_info['query']) {
1083
		return true;
1084
	}
1085
1086
	// compare get params that might be out of order
1087
	$url1_params = [];
1088
	$url2_params = [];
1089
1090
	if (isset($url1_info['query'])) {
1091
		if ($url1_info['query'] = html_entity_decode($url1_info['query'])) {
1092
			$url1_params = elgg_parse_str($url1_info['query']);
1093
		}
1094
	}
1095
1096
	if (isset($url2_info['query'])) {
1097
		if ($url2_info['query'] = html_entity_decode($url2_info['query'])) {
1098
			$url2_params = elgg_parse_str($url2_info['query']);
1099
		}
1100
	}
1101
1102
	// drop ignored params
1103
	foreach ($ignore_params as $param) {
1104
		if (isset($url1_params[$param])) {
1105
			unset($url1_params[$param]);
1106
		}
1107
		if (isset($url2_params[$param])) {
1108
			unset($url2_params[$param]);
1109
		}
1110
	}
1111
1112
	// array_diff_assoc only returns the items in arr1 that aren't in arrN
1113
	// but not the items that ARE in arrN but NOT in arr1
1114
	// if arr1 is an empty array, this function will return 0 no matter what.
1115
	// since we only care if they're different and not how different,
1116
	// add the results together to get a non-zero (ie, different) result
1117
	$diff_count = count(array_diff_assoc($url1_params, $url2_params));
1118
	$diff_count += count(array_diff_assoc($url2_params, $url1_params));
1119
	if ($diff_count > 0) {
1120
		return false;
1121
	}
1122
1123
	return true;
1124
}
1125
1126
/**
1127
 * Signs provided URL with a SHA256 HMAC key
1128
 *
1129
 * @note Signed URLs do not offer CSRF protection and should not be used instead of action tokens.
1130
 *
1131
 * @param string $url     URL to sign
1132
 * @param string $expires Expiration time
1133
 *                        A string suitable for strtotime()
1134
 *                        Falsey values indicate non-expiring URL
1135
 * @return string
1136
 */
1137
function elgg_http_get_signed_url($url, $expires = false) {
1138
	return _elgg_services()->urlSigner->sign($url, $expires);
1139
}
1140
1141
/**
1142
 * Validates if the HMAC signature of the URL is valid
1143
 *
1144
 * @param string $url URL to validate
1145
 * @return bool
1146
 */
1147
function elgg_http_validate_signed_url($url) {
1148
	return _elgg_services()->urlSigner->isValid($url);
1149
}
1150
1151
/**
1152
 * Validates if the HMAC signature of the current request is valid
1153
 * Issues 403 response if signature is inalid
1154
 * @return void
1155
 */
1156
function elgg_signed_request_gatekeeper() {
1157
1158
	switch (php_sapi_name()) {
1159
		case 'cli' :
1160
		case 'phpdbg' :
1161
			return;
1162
1163
		default :
1164
			if (!elgg_http_validate_signed_url(current_page_url())) {
1165
				register_error(elgg_echo('invalid_request_signature'));