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