diff --git a/.upgradenotes/MDL-81960-2024061004043644.yml b/.upgradenotes/MDL-81960-2024061004043644.yml new file mode 100644 index 00000000000..c371a6aff54 --- /dev/null +++ b/.upgradenotes/MDL-81960-2024061004043644.yml @@ -0,0 +1,29 @@ +issueNumber: MDL-81960 +notes: + core: + - message: >- + The following classes have been renamed and now support autoloading. + Existing classes are currently unaffected. + + | Old class name | New class name | + + | --- | --- | + + | `\moodle_url` | `\core\url` | + + | `\progress_trace` | `\core\output\progress_trace` | + + | `\combined_progress_trace` | `\core\output\progress_trace\combined_progress_trace` | + + | `\error_log_progress_trace` | `\core\output\progress_trace\error_log_progress_trace` | + + | `\html_list_progress_trace` | `\core\output\progress_trace\html_list_progress_trace` | + + | `\html_progress_trace` | `\core\output\progress_trace\html_progress_trace` | + + | `\null_progress_trace` | `\core\output\progress_trace\null_progress_trace` | + + | `\progress_trace_buffer` | `\core\output\progress_trace\progress_trace_buffer` | + + | `\text_progress_trace` | `\core\output\progress_trace\text_progress_trace` | + type: improved diff --git a/lib/classes/component.php b/lib/classes/component.php index ade977d6f9e..0c6063c9eca 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -392,6 +392,7 @@ class core_component { \core\output\bootstrap_renderer::class, \core\lang_string::class, \renderable::class, + \core\url::class, ]; foreach ($keyclasses as $classname) { if (!array_key_exists($classname, $cache['classmap'])) { diff --git a/lib/classes/output/progress_trace.php b/lib/classes/output/progress_trace.php new file mode 100644 index 00000000000..8a590b0aebc --- /dev/null +++ b/lib/classes/output/progress_trace.php @@ -0,0 +1,51 @@ +. + +namespace core\output; + +/** + * Progress trace class. + * + * Use this class from long operations where you want to output occasional information about + * what is going on, but don't know if, or in what format, the output should be. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +abstract class progress_trace { + /** + * Output an progress message in whatever format. + * + * @param string $message the message to output. + * @param int $depth indent depth for this message. + */ + abstract public function output( + string $message, + int $depth = 0, + ); + + /** + * Called when the processing is finished. + */ + public function finished() { + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(progress_trace::class, \progress_trace::class); diff --git a/lib/classes/output/progress_trace/combined_progress_trace.php b/lib/classes/output/progress_trace/combined_progress_trace.php new file mode 100644 index 00000000000..9344e7a5afa --- /dev/null +++ b/lib/classes/output/progress_trace/combined_progress_trace.php @@ -0,0 +1,61 @@ +. + +namespace core\output\progress_trace; + +use core\output\progress_trace; + +/** + * Special type of trace that can be used for redirecting to multiple other traces. + * + * @copyright Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +class combined_progress_trace extends progress_trace { + /** + * Constructs a new instance. + * + * @param array $traces multiple traces + */ + public function __construct( + /** @var progress_trace[] The list of traces */ + protected array $traces, + ) { + } + + #[\Override] + public function output( + string $message, + int $depth = 0, + ): void { + foreach ($this->traces as $trace) { + $trace->output($message, $depth); + } + } + + #[\Override] + public function finished(): void { + foreach ($this->traces as $trace) { + $trace->finished(); + } + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(combined_progress_trace::class, \combined_progress_trace::class); diff --git a/lib/classes/output/progress_trace/error_log_progress_trace.php b/lib/classes/output/progress_trace/error_log_progress_trace.php new file mode 100644 index 00000000000..c3f17d87da7 --- /dev/null +++ b/lib/classes/output/progress_trace/error_log_progress_trace.php @@ -0,0 +1,52 @@ +. + +namespace core\output\progress_trace; + +use core\output\progress_trace; + +/** + * This subclass of progress_trace outputs to error log. + * + * @copyright Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +class error_log_progress_trace extends progress_trace { + /** + * Constructor. + * @param string $prefix optional log prefix + */ + public function __construct( + /** @var string The prefix to use in the error_log messages */ + protected string $prefix = '', + ) { + } + + #[\Override] + public function output( + string $message, + int $depth = 0, + ): void { + // phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative + error_log($this->prefix . str_repeat(' ', $depth) . $message); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(error_log_progress_trace::class, \error_log_progress_trace::class); diff --git a/lib/classes/output/progress_trace/html_list_progress_trace.php b/lib/classes/output/progress_trace/html_list_progress_trace.php new file mode 100644 index 00000000000..4c21c79a869 --- /dev/null +++ b/lib/classes/output/progress_trace/html_list_progress_trace.php @@ -0,0 +1,70 @@ +. + +namespace core\output\progress_trace; + +use core\output\progress_trace; + +/** + * HTML List Progress Tree + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +class html_list_progress_trace extends progress_trace { + /** @var int The current depth of the trace*/ + protected int $currentdepth = -1; + + #[\Override] + public function output( + string $message, + int $depth = 0, + ): void { + $samedepth = true; + while ($this->currentdepth > $depth) { + echo "\n\n"; + $this->currentdepth -= 1; + if ($this->currentdepth == $depth) { + echo '
', str_repeat(' ', $depth), htmlspecialchars($message, ENT_COMPAT), "
\n"; + flush(); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(html_progress_trace::class, \html_progress_trace::class); diff --git a/lib/classes/output/progress_trace/null_progress_trace.php b/lib/classes/output/progress_trace/null_progress_trace.php new file mode 100644 index 00000000000..6f9af4e60ef --- /dev/null +++ b/lib/classes/output/progress_trace/null_progress_trace.php @@ -0,0 +1,40 @@ +. + +namespace core\output\progress_trace; + +use core\output\progress_trace; + +/** + * This subclass of progress_trace does not ouput anything. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +class null_progress_trace extends progress_trace { + #[\Override] + public function output( + string $message, + int $depth = 0, + ): void { + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(null_progress_trace::class, \null_progress_trace::class); diff --git a/lib/classes/output/progress_trace/progress_trace_buffer.php b/lib/classes/output/progress_trace/progress_trace_buffer.php new file mode 100644 index 00000000000..85085b9493d --- /dev/null +++ b/lib/classes/output/progress_trace/progress_trace_buffer.php @@ -0,0 +1,94 @@ +. + +namespace core\output\progress_trace; + +use core\output\progress_trace; + +/** + * Special type of trace that can be used for catching of output of other traces. + * + * @copyright Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +class progress_trace_buffer extends progress_trace { + /** @var string output buffer */ + protected string $buffer = ''; + + /** + * Constructor. + * + * @param progress_trace $trace + * @param bool $passthrough true means output and buffer, false means just buffer and no output + */ + public function __construct( + /** @var progress_trace The progress_trace to pass content to */ + protected progress_trace $trace, + /** @var bool Whether we pass output out */ + protected bool $passthrough = true, + ) { + $this->buffer = ''; + } + + #[\Override] + public function output( + string $message, + int $depth = 0, + ): void { + ob_start(); + $this->trace->output($message, $depth); + $this->buffer .= ob_get_contents(); + if ($this->passthrough) { + ob_end_flush(); + } else { + ob_end_clean(); + } + } + + #[\Override] + public function finished(): void { + ob_start(); + $this->trace->finished(); + $this->buffer .= ob_get_contents(); + if ($this->passthrough) { + ob_end_flush(); + } else { + ob_end_clean(); + } + } + + /** + * Reset the internal text buffer. + */ + public function reset_buffer(): void { + $this->buffer = ''; + } + + /** + * Return the internal text buffer. + * + * @return string buffered plain text + */ + public function get_buffer(): string { + return $this->buffer; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(progress_trace_buffer::class, \progress_trace_buffer::class); diff --git a/lib/classes/output/progress_trace/text_progress_trace.php b/lib/classes/output/progress_trace/text_progress_trace.php new file mode 100644 index 00000000000..94652ab489a --- /dev/null +++ b/lib/classes/output/progress_trace/text_progress_trace.php @@ -0,0 +1,41 @@ +. + +namespace core\output\progress_trace; + +use core\output\progress_trace; + +/** + * This subclass of progress_trace outputs to plain text. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +class text_progress_trace extends progress_trace { + #[\Override] + public function output( + string $message, + int $depth = 0, + ): void { + mtrace(str_repeat(' ', $depth) . $message); + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(text_progress_trace::class, \text_progress_trace::class); diff --git a/lib/classes/url.php b/lib/classes/url.php new file mode 100644 index 00000000000..af29e06d832 --- /dev/null +++ b/lib/classes/url.php @@ -0,0 +1,875 @@ +. + +namespace core; + +use core\context\user as context_user; +use core\exception\coding_exception; +use core\exception\moodle_exception; +use Psr\Http\Message\UriInterface; + +/** + * Class for creating and manipulating urls. + * + * It can be used in moodle pages where config.php has been included without any further includes. + * + * It is useful for manipulating urls with long lists of params. + * One situation where it will be useful is a page which links to itself to perform various actions + * and / or to process form data. A url object: + * can be created for a page to refer to itself with all the proper get params being passed from page call to + * page call and methods can be used to output a url including all the params, optionally adding and overriding + * params and can also be used to + * - output the url without any get params + * - and output the params as hidden fields to be output within a form + * + * @copyright 2007 jamiesensei + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + */ +class url { + /** + * Scheme, ex.: http, https + * @var string + */ + protected $scheme = ''; + + /** + * Hostname. + * @var string + */ + protected $host = ''; + + /** + * Port number, empty means default 80 or 443 in case of http. + * @var int + */ + protected $port = ''; + + /** + * Username for http auth. + * @var string + */ + protected $user = ''; + + /** + * Password for http auth. + * @var string + */ + protected $pass = ''; + + /** + * Script path. + * @var string + */ + protected $path = ''; + + /** + * Optional slash argument value. + * @var string + */ + protected $slashargument = ''; + + /** + * Anchor, may be also empty, null means none. + * @var string + */ + protected $anchor = null; + + /** + * Url parameters as associative array. + * @var array + */ + protected $params = []; + + /** + * Create new instance of url. + * + * @param self|string $url - moodle_url means make a copy of another + * moodle_url and change parameters, string means full url or shortened + * form (ex.: '/course/view.php'). It is strongly encouraged to not include + * query string because it may result in double encoded values. Use the + * $params instead. For admin URLs, just use /admin/script.php, this + * class takes care of the $CFG->admin issue. + * @param null|array $params these params override current params or add new + * @param string $anchor The anchor to use as part of the URL if there is one. + * @throws moodle_exception + */ + public function __construct( + $url, + ?array $params = null, + $anchor = null, + ) { + global $CFG; + + if ($url instanceof self) { + $this->scheme = $url->scheme; + $this->host = $url->host; + $this->port = $url->port; + $this->user = $url->user; + $this->pass = $url->pass; + $this->path = $url->path; + $this->slashargument = $url->slashargument; + $this->params = $url->params; + $this->anchor = $url->anchor; + } else { + $url = $url ?? ''; + // Detect if anchor used. + $apos = strpos($url, '#'); + if ($apos !== false) { + $anchor = substr($url, $apos); + $anchor = ltrim($anchor, '#'); + $this->set_anchor($anchor); + $url = substr($url, 0, $apos); + } + + // Normalise shortened form of our url ex.: '/course/view.php'. + if (strpos($url, '/') === 0) { + $url = $CFG->wwwroot . $url; + } + + if ($CFG->admin !== 'admin') { + if (strpos($url, "$CFG->wwwroot/admin/") === 0) { + $url = str_replace("$CFG->wwwroot/admin/", "$CFG->wwwroot/$CFG->admin/", $url); + } + } + + // Parse the $url. + $parts = parse_url($url); + if ($parts === false) { + throw new moodle_exception('invalidurl'); + } + if (isset($parts['query'])) { + // Note: the values may not be correctly decoded, url parameters should be always passed as array. + parse_str(str_replace('&', '&', $parts['query']), $this->params); + } + unset($parts['query']); + foreach ($parts as $key => $value) { + $this->$key = $value; + } + + // Detect slashargument value from path - we do not support directory names ending with .php. + $pos = strpos($this->path, '.php/'); + if ($pos !== false) { + $this->slashargument = substr($this->path, $pos + 4); + $this->path = substr($this->path, 0, $pos + 4); + } + } + + $this->params($params); + if ($anchor !== null) { + $this->anchor = (string)$anchor; + } + } + + /** + * Add an array of params to the params for this url. + * + * The added params override existing ones if they have the same name. + * + * @param null|array $params Defaults to null. If null then returns all params. + * @return array Array of Params for url. + * @throws coding_exception + */ + public function params(?array $params = null) { + $params = (array)$params; + + foreach ($params as $key => $value) { + if (is_int($key)) { + throw new coding_exception('Url parameters can not have numeric keys!'); + } + if (!is_string($value)) { + if (is_array($value)) { + throw new coding_exception('Url parameters values can not be arrays!'); + } + if (is_object($value) && !method_exists($value, '__toString')) { + throw new coding_exception('Url parameters values can not be objects, unless __toString() is defined!'); + } + } + $this->params[$key] = (string)$value; + } + return $this->params; + } + + /** + * Remove all params if no arguments passed. + * Remove selected params if arguments are passed. + * + * Can be called as either remove_params('param1', 'param2') + * or remove_params(array('param1', 'param2')). + * + * @param string[]|string ...$params either an array of param names, or 1..n string params to remove as args. + * @return array url parameters + */ + public function remove_params(...$params) { + if (empty($params)) { + return $this->params; + } + + $firstparam = reset($params); + if (is_array($firstparam)) { + $params = $firstparam; + } + + foreach ($params as $param) { + unset($this->params[$param]); + } + return $this->params; + } + + /** + * Remove all url parameters. + * + * @param array $unused Unused param + */ + public function remove_all_params($unused = null) { + $this->params = []; + $this->slashargument = ''; + } + + /** + * Add a param to the params for this url. + * + * The added param overrides existing one if they have the same name. + * + * @param string $paramname name + * @param string $newvalue Param value. If new value specified current value is overriden or parameter is added + * @return mixed string parameter value, null if parameter does not exist + */ + public function param($paramname, $newvalue = '') { + if (func_num_args() > 1) { + // Set new value. + $this->params([$paramname => $newvalue]); + } + if (isset($this->params[$paramname])) { + return $this->params[$paramname]; + } else { + return null; + } + } + + /** + * Merges parameters and validates them + * + * @param null|array $overrideparams + * @return array merged parameters + * @throws coding_exception + */ + protected function merge_overrideparams(?array $overrideparams = null) { + $overrideparams = (array)$overrideparams; + $params = $this->params; + foreach ($overrideparams as $key => $value) { + if (is_int($key)) { + throw new coding_exception('Overridden parameters can not have numeric keys!'); + } + if (is_array($value)) { + throw new coding_exception('Overridden parameters values can not be arrays!'); + } + if (is_object($value) && !method_exists($value, '__toString')) { + throw new coding_exception('Overridden parameters values can not be objects, unless __toString() is defined!'); + } + $params[$key] = (string)$value; + } + return $params; + } + + /** + * Get the params as as a query string. + * + * This method should not be used outside of this method. + * + * @param bool $escaped Use & as params separator instead of plain & + * @param null|array $overrideparams params to add to the output params, these + * override existing ones with the same name. + * @return string query string that can be added to a url. + */ + public function get_query_string($escaped = true, ?array $overrideparams = null) { + $arr = []; + if ($overrideparams !== null) { + $params = $this->merge_overrideparams($overrideparams); + } else { + $params = $this->params; + } + foreach ($params as $key => $val) { + if (is_array($val)) { + foreach ($val as $index => $value) { + $arr[] = rawurlencode($key . '[' . $index . ']') . "=" . rawurlencode($value); + } + } else { + if (isset($val) && $val !== '') { + $arr[] = rawurlencode($key) . "=" . rawurlencode($val); + } else { + $arr[] = rawurlencode($key); + } + } + } + if ($escaped) { + return implode('&', $arr); + } else { + return implode('&', $arr); + } + } + + /** + * Get the url params as an array of key => value pairs. + * + * This helps in handling cases where url params contain arrays. + * + * @return array params array for templates. + */ + public function export_params_for_template(): array { + $data = []; + foreach ($this->params as $key => $val) { + if (is_array($val)) { + foreach ($val as $index => $value) { + $data[] = ['name' => $key . '[' . $index . ']', 'value' => $value]; + } + } else { + $data[] = ['name' => $key, 'value' => $val]; + } + } + return $data; + } + + /** + * Shortcut for printing of encoded URL. + * + * @return string + */ + public function __toString() { + return $this->out(true); + } + + /** + * Output url. + * + * If you use the returned URL in HTML code, you want the escaped ampersands. If you use + * the returned URL in HTTP headers, you want $escaped=false. + * + * @param bool $escaped Use & as params separator instead of plain & + * @param null|array $overrideparams params to add to the output url, these override existing ones with the same name. + * @return string Resulting URL + */ + public function out($escaped = true, ?array $overrideparams = null) { + + global $CFG; + + if (!is_bool($escaped)) { + debugging('Escape parameter must be of type boolean, ' . gettype($escaped) . ' given instead.'); + } + + $url = $this; + + // Allow url's to be rewritten by a plugin. + if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) { + $class = $CFG->urlrewriteclass; + $pluginurl = $class::url_rewrite($url); + if ($pluginurl instanceof url) { + $url = $pluginurl; + } + } + + return $url->raw_out($escaped, $overrideparams); + } + + /** + * Output url without any rewrites + * + * This is identical in signature and use to out() but doesn't call the rewrite handler. + * + * @param bool $escaped Use & as params separator instead of plain & + * @param null|array $overrideparams params to add to the output url, these override existing ones with the same name. + * @return string Resulting URL + */ + public function raw_out($escaped = true, ?array $overrideparams = null) { + if (!is_bool($escaped)) { + debugging('Escape parameter must be of type boolean, ' . gettype($escaped) . ' given instead.'); + } + + $uri = $this->out_omit_querystring() . $this->slashargument; + + $querystring = $this->get_query_string($escaped, $overrideparams); + if ($querystring !== '') { + $uri .= '?' . $querystring; + } + + $uri .= $this->get_encoded_anchor(); + + return $uri; + } + + /** + * Encode the anchor according to RFC 3986. + * + * @return string The encoded anchor + */ + public function get_encoded_anchor(): string { + if (is_null($this->anchor)) { + return ''; + } + + // RFC 3986 allows the following characters in a fragment without them being encoded: + // pct-encoded: "%" HEXDIG HEXDIG + // unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~" / + // sub-delims: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" / ":" / "@" + // fragment: "/" / "?" + // + // All other characters should be encoded. + // These should not be encoded in the fragment unless they were already encoded. + + // The following characters are allowed in the fragment without encoding. + // In addition to this list is pct-encoded, but we can't easily handle this with a regular expression. + $allowed = 'a-zA-Z0-9\\-._~!$&\'()*+,;=:@\/?'; + $anchor = '#'; + + $remainder = $this->anchor; + do { + // Split the string on any %. + $parts = explode('%', $remainder, 2); + $anchorparts = array_shift($parts); + + // The first part can go through our preg_replace_callback to quote any relevant characters. + $anchor .= preg_replace_callback( + '/[^' . $allowed . ']/', + fn ($matches) => rawurlencode($matches[0]), + $anchorparts, + ); + + // The second part _might_ be a valid pct-encoded character. + if (count($parts) === 0) { + break; + } + + // If the second part is a valid pct-encoded character, append it to the anchor. + $remainder = array_shift($parts); + if (preg_match('/^[a-fA-F0-9]{2}/', $remainder, $matches)) { + $anchor .= "%{$matches[0]}"; + $remainder = substr($remainder, 2); + } else { + // This was not a valid pct-encoded character. Encode the % and continue with the next part. + $anchor .= rawurlencode('%'); + } + } while (strlen($remainder) > 0); + + return $anchor; + } + + /** + * Returns url without parameters, everything before '?'. + * + * @param bool $includeanchor if {@see self::anchor} is defined, should it be returned? + * @return string + */ + public function out_omit_querystring($includeanchor = false) { + + $uri = $this->scheme ? $this->scheme . ':' . ((strtolower($this->scheme) == 'mailto') ? '' : '//') : ''; + $uri .= $this->user ? $this->user . ($this->pass ? ':' . $this->pass : '') . '@' : ''; + $uri .= $this->host ? $this->host : ''; + $uri .= $this->port ? ':' . $this->port : ''; + $uri .= $this->path ? $this->path : ''; + if ($includeanchor) { + $uri .= $this->get_encoded_anchor(); + } + + return $uri; + } + + /** + * Compares this url with another. + * + * See documentation of constants for an explanation of the comparison flags. + * + * @param self $url The moodle_url object to compare + * @param int $matchtype The type of comparison (URL_MATCH_BASE, URL_MATCH_PARAMS, URL_MATCH_EXACT) + * @return bool + */ + public function compare(self $url, $matchtype = URL_MATCH_EXACT) { + + $baseself = $this->out_omit_querystring(); + $baseother = $url->out_omit_querystring(); + + // Append index.php if there is no specific file. + if (substr($baseself, -1) == '/') { + $baseself .= 'index.php'; + } + if (substr($baseother, -1) == '/') { + $baseother .= 'index.php'; + } + + // Compare the two base URLs. + if ($baseself != $baseother) { + return false; + } + + if ($matchtype == URL_MATCH_BASE) { + return true; + } + + $urlparams = $url->params(); + foreach ($this->params() as $param => $value) { + if ($param == 'sesskey') { + continue; + } + if (!array_key_exists($param, $urlparams) || $urlparams[$param] != $value) { + return false; + } + } + + if ($matchtype == URL_MATCH_PARAMS) { + return true; + } + + foreach ($urlparams as $param => $value) { + if ($param == 'sesskey') { + continue; + } + if (!array_key_exists($param, $this->params()) || $this->param($param) != $value) { + return false; + } + } + + if ($url->anchor !== $this->anchor) { + return false; + } + + return true; + } + + /** + * Sets the anchor for the URI (the bit after the hash) + * + * @param string $anchor null means remove previous + */ + public function set_anchor($anchor) { + if (is_null($anchor)) { + // Remove. + $this->anchor = null; + } else { + $this->anchor = $anchor; + } + } + + /** + * Sets the scheme for the URI (the bit before ://) + * + * @param string $scheme + */ + public function set_scheme($scheme) { + // See http://www.ietf.org/rfc/rfc3986.txt part 3.1. + if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*$/', $scheme)) { + $this->scheme = $scheme; + } else { + throw new coding_exception('Bad URL scheme.'); + } + } + + /** + * Sets the url slashargument value. + * + * @param string $path usually file path + * @param string $parameter name of page parameter if slasharguments not supported + * @param bool $supported usually null, then it depends on $CFG->slasharguments, use true or false for other servers + */ + public function set_slashargument($path, $parameter = 'file', $supported = null) { + global $CFG; + if (is_null($supported)) { + $supported = !empty($CFG->slasharguments); + } + + if ($supported) { + $parts = explode('/', $path); + $parts = array_map('rawurlencode', $parts); + $path = implode('/', $parts); + $this->slashargument = $path; + unset($this->params[$parameter]); + } else { + $this->slashargument = ''; + $this->params[$parameter] = $path; + } + } + + // Static factory methods. + + /** + * Create a new url instance from a UriInterface. + * + * @param UriInterface $uri + * @return self + */ + public static function from_uri(UriInterface $uri): self { + $url = new self( + url: $uri->getScheme() . '://' . $uri->getAuthority() . $uri->getPath(), + anchor: $uri->getFragment() ?: null, + ); + + $params = $uri->getQuery(); + foreach (explode('&', $params) as $param) { + $url->param(...explode('=', $param, 2)); + } + + return $url; + } + + /** + * General moodle file url. + * + * @param string $urlbase the script serving the file + * @param string $path + * @param bool $forcedownload + * @return self + */ + public static function make_file_url($urlbase, $path, $forcedownload = false) { + $params = []; + if ($forcedownload) { + $params['forcedownload'] = 1; + } + $url = new self($urlbase, $params); + $url->set_slashargument($path); + return $url; + } + + /** + * Factory method for creation of url pointing to plugin file. + * + * Please note this method can be used only from the plugins to + * create urls of own files, it must not be used outside of plugins! + * + * @param int $contextid + * @param string $component + * @param string $area + * @param ?int $itemid + * @param string $pathname + * @param string $filename + * @param bool $forcedownload + * @param mixed $includetoken Whether to use a user token when displaying this group image. + * True indicates to generate a token for current user, and integer value indicates to generate a token for the + * user whose id is the value indicated. + * If the group picture is included in an e-mail or some other location where the audience is a specific + * user who will not be logged in when viewing, then we use a token to authenticate the user. + * @return url + */ + public static function make_pluginfile_url( + $contextid, + $component, + $area, + $itemid, + $pathname, + $filename, + $forcedownload = false, + $includetoken = false + ) { + global $CFG, $USER; + + $path = []; + + if ($includetoken) { + $urlbase = "$CFG->wwwroot/tokenpluginfile.php"; + $userid = $includetoken === true ? $USER->id : $includetoken; + $token = get_user_key('core_files', $userid); + if ($CFG->slasharguments) { + $path[] = $token; + } + } else { + $urlbase = "$CFG->wwwroot/pluginfile.php"; + } + $path[] = $contextid; + $path[] = $component; + $path[] = $area; + + if ($itemid !== null) { + $path[] = $itemid; + } + + $path = "/" . implode('/', $path) . "{$pathname}{$filename}"; + + $url = self::make_file_url($urlbase, $path, $forcedownload, $includetoken); + if ($includetoken && empty($CFG->slasharguments)) { + $url->param('token', $token); + } + return $url; + } + + /** + * Factory method for creation of url pointing to plugin file. + * This method is the same that make_pluginfile_url but pointing to the webservice pluginfile.php script. + * It should be used only in external functions. + * + * @since 2.8 + * @param int $contextid + * @param string $component + * @param string $area + * @param int $itemid + * @param string $pathname + * @param string $filename + * @param bool $forcedownload + * @return url + */ + public static function make_webservice_pluginfile_url( + $contextid, + $component, + $area, + $itemid, + $pathname, + $filename, + $forcedownload = false + ) { + global $CFG; + $urlbase = "$CFG->wwwroot/webservice/pluginfile.php"; + if ($itemid === null) { + return self::make_file_url($urlbase, "/$contextid/$component/$area" . $pathname . $filename, $forcedownload); + } else { + return self::make_file_url($urlbase, "/$contextid/$component/$area/$itemid" . $pathname . $filename, $forcedownload); + } + } + + /** + * Factory method for creation of url pointing to draft file of current user. + * + * @param int $draftid draft item id + * @param string $pathname + * @param string $filename + * @param bool $forcedownload + * @return url + */ + public static function make_draftfile_url($draftid, $pathname, $filename, $forcedownload = false) { + global $CFG, $USER; + $urlbase = "$CFG->wwwroot/draftfile.php"; + $context = context_user::instance($USER->id); + + return self::make_file_url($urlbase, "/$context->id/user/draft/$draftid" . $pathname . $filename, $forcedownload); + } + + /** + * Factory method for creating of links to legacy course files. + * + * @param int $courseid + * @param string $filepath + * @param bool $forcedownload + * @return url + */ + public static function make_legacyfile_url($courseid, $filepath, $forcedownload = false) { + global $CFG; + + $urlbase = "$CFG->wwwroot/file.php"; + return self::make_file_url($urlbase, '/' . $courseid . '/' . $filepath, $forcedownload); + } + + /** + * Checks if URL is relative to $CFG->wwwroot. + * + * @return bool True if URL is relative to $CFG->wwwroot; otherwise, false. + */ + public function is_local_url(): bool { + global $CFG; + + $url = $this->out(); + // Does URL start with wwwroot? Otherwise, URL isn't relative to wwwroot. + return ( ($url === $CFG->wwwroot) || (strpos($url, $CFG->wwwroot . '/') === 0) ); + } + + /** + * Returns URL as relative path from $CFG->wwwroot + * + * Can be used for passing around urls with the wwwroot stripped + * + * @param boolean $escaped Use & as params separator instead of plain & + * @param ?array $overrideparams params to add to the output url, these override existing ones with the same name. + * @return string Resulting URL + * @throws coding_exception if called on a non-local url + */ + public function out_as_local_url($escaped = true, ?array $overrideparams = null) { + global $CFG; + + // URL should be relative to wwwroot. If not then throw exception. + if ($this->is_local_url()) { + $url = $this->out($escaped, $overrideparams); + $localurl = substr($url, strlen($CFG->wwwroot)); + return !empty($localurl) ? $localurl : ''; + } else { + throw new coding_exception('out_as_local_url called on a non-local URL'); + } + } + + /** + * Returns the 'path' portion of a URL. For example, if the URL is + * http://www.example.org:447/my/file/is/here.txt?really=1 then this will + * return '/my/file/is/here.txt'. + * + * By default the path includes slash-arguments (for example, + * '/myfile.php/extra/arguments') so it is what you would expect from a + * URL path. If you don't want this behaviour, you can opt to exclude the + * slash arguments. (Be careful: if the $CFG variable slasharguments is + * disabled, these URLs will have a different format and you may need to + * look at the 'file' parameter too.) + * + * @param bool $includeslashargument If true, includes slash arguments + * @return string Path of URL + */ + public function get_path($includeslashargument = true) { + return $this->path . ($includeslashargument ? $this->slashargument : ''); + } + + /** + * Returns a given parameter value from the URL. + * + * @param string $name Name of parameter + * @return string Value of parameter or null if not set + */ + public function get_param($name) { + if (array_key_exists($name, $this->params)) { + return $this->params[$name]; + } else { + return null; + } + } + + /** + * Returns the 'scheme' portion of a URL. For example, if the URL is + * http://www.example.org:447/my/file/is/here.txt?really=1 then this will + * return 'http' (without the colon). + * + * @return string Scheme of the URL. + */ + public function get_scheme() { + return $this->scheme; + } + + /** + * Returns the 'host' portion of a URL. For example, if the URL is + * http://www.example.org:447/my/file/is/here.txt?really=1 then this will + * return 'www.example.org'. + * + * @return string Host of the URL. + */ + public function get_host() { + return $this->host; + } + + /** + * Returns the 'port' portion of a URL. For example, if the URL is + * http://www.example.org:447/my/file/is/here.txt?really=1 then this will + * return '447'. + * + * @return string Port of the URL. + */ + public function get_port() { + return $this->port; + } +} + +// Alias this class to the old name. +// This file will be autoloaded by the legacyclasses autoload system. +// In future all uses of this class will be corrected and the legacy references will be removed. +class_alias(url::class, \moodle_url::class); diff --git a/lib/db/legacyclasses.php b/lib/db/legacyclasses.php index 1bb7647a195..9a244e4b5a3 100644 --- a/lib/db/legacyclasses.php +++ b/lib/db/legacyclasses.php @@ -30,12 +30,12 @@ defined('MOODLE_INTERNAL') || die(); // The old class name is the key, the path to the file containing the class is the value. // The array must be called $legacyclasses. $legacyclasses = [ - // Exception API. \bootstrap_renderer::class => 'output/bootstrap_renderer.php', // Core API. \emoticon_manager::class => 'emoticon_manager.php', \lang_string::class => 'lang_string.php', + \moodle_url::class => 'url.php', // Exception API. \coding_exception::class => 'exception/coding_exception.php', @@ -148,4 +148,14 @@ $legacyclasses = [ 'core_table', 'dataformat_export_format.php', ], + + // The progress_trace classes. + \combined_progress_trace::class => 'output/progress_trace/combined_progress_trace.php', + \error_log_progress_trace::class => 'output/progress_trace/error_log_progress_trace.php', + \html_list_progress_trace::class => 'output/progress_trace/html_list_progress_trace.php', + \html_progress_trace::class => 'output/progress_trace/html_progress_trace.php', + \null_progress_trace::class => 'output/progress_trace/null_progress_trace.php', + \progress_trace::class => 'output/progress_trace.php', + \progress_trace_buffer::class => 'output/progress_trace/progress_trace_buffer.php', + \text_progress_trace::class => 'output/progress_trace/text_progress_trace.php', ]; diff --git a/lib/tests/output/progress_trace/html_list_progress_trace_test.php b/lib/tests/output/progress_trace/html_list_progress_trace_test.php new file mode 100644 index 00000000000..f593fe08403 --- /dev/null +++ b/lib/tests/output/progress_trace/html_list_progress_trace_test.php @@ -0,0 +1,40 @@ +. + +namespace core\output\progress_trace; + +/** + * Tests for \core\progress_trace\html_list_progress_trace. + * + * @package core + * @category test + * @copyright 2024 Andrew Lyonsdo
\nre
\nmi
\n"); + } +} diff --git a/lib/tests/output/progress_trace/null_progress_trace_test.php b/lib/tests/output/progress_trace/null_progress_trace_test.php new file mode 100644 index 00000000000..6d34a3214aa --- /dev/null +++ b/lib/tests/output/progress_trace/null_progress_trace_test.php @@ -0,0 +1,42 @@ +. + +namespace core\output\progress_trace; + +/** + * Tests for \core\progress_trace\null_progress_trace. + * + * @package core + * @category test + * @copyright 2024 Andrew Lyonsdo
\nre
\nmi
\n", $output); + $this->assertSame($output, $trace->get_buffer()); + + $trace = new progress_trace_buffer(new html_progress_trace(), false); + $trace->output('do'); + $trace->output('re', 1); + $trace->output('mi', 2); + $trace->finished(); + $this->assertSame("do
\nre
\nmi
\n", $trace->get_buffer()); + $this->assertSame("do
\nre
\nmi
\n", $trace->get_buffer()); + $trace->reset_buffer(); + $this->assertSame('', $trace->get_buffer()); + $this->expectOutputString(''); + } +} diff --git a/lib/tests/output/progress_trace/test_combined_progress_trace_test.php b/lib/tests/output/progress_trace/test_combined_progress_trace_test.php new file mode 100644 index 00000000000..663bac655ff --- /dev/null +++ b/lib/tests/output/progress_trace/test_combined_progress_trace_test.php @@ -0,0 +1,45 @@ +. + +namespace core\output\progress_trace; + +/** + * Tests for \core\progress_trace\test_combined_progress_trace. + * + * @package core + * @category test + * @copyright 2024 Andrew Lyonsdo
\nre
\nmi
\n", $trace1->get_buffer()); + $this->assertSame("do\n re\n mi\n", $trace2->get_buffer()); + $this->expectOutputString(''); + } +} diff --git a/lib/tests/output/progress_trace/text_progress_trace_test.php b/lib/tests/output/progress_trace/text_progress_trace_test.php new file mode 100644 index 00000000000..fd8d3fe8ebd --- /dev/null +++ b/lib/tests/output/progress_trace/text_progress_trace_test.php @@ -0,0 +1,42 @@ +. + +namespace core\output\progress_trace; + +/** + * Tests for \core\progress_trace\text_progress_trace. + * + * @package core + * @category test + * @copyright 2024 Andrew Lyonsdo
\nre
\nmi
\n"); - } - - /** - * @covers \html_list_progress_trace - */ - public function test_html_list_progress_trace(): void { - $this->resetAfterTest(false); - - $trace = new html_list_progress_trace(); - $trace->output('do'); - $trace->output('re', 1); - $trace->output('mi', 2); - $trace->finished(); - $this->expectOutputString("do
\nre
\nmi
\n", $output); - $this->assertSame($output, $trace->get_buffer()); - - $trace = new progress_trace_buffer(new html_progress_trace(), false); - $trace->output('do'); - $trace->output('re', 1); - $trace->output('mi', 2); - $trace->finished(); - $this->assertSame("do
\nre
\nmi
\n", $trace->get_buffer()); - $this->assertSame("do
\nre
\nmi
\n", $trace->get_buffer()); - $trace->reset_buffer(); - $this->assertSame('', $trace->get_buffer()); - $this->expectOutputString(''); - } - - /** - * @covers \combined_progress_trace - */ - public function test_combined_progress_trace(): void { - $this->resetAfterTest(false); - - $trace1 = new progress_trace_buffer(new html_progress_trace(), false); - $trace2 = new progress_trace_buffer(new text_progress_trace(), false); - - $trace = new combined_progress_trace(array($trace1, $trace2)); - $trace->output('do'); - $trace->output('re', 1); - $trace->output('mi', 2); - $trace->finished(); - $this->assertSame("do
\nre
\nmi
\n", $trace1->get_buffer()); - $this->assertSame("do\n re\n mi\n", $trace2->get_buffer()); - $this->expectOutputString(''); - } - /** * @covers ::set_debugging */ @@ -1199,136 +1093,6 @@ EXPECTED; $this->assertEquals($expected, get_html_lang_attribute_value($langcode)); } - /** - * Test the coding exceptions when returning URL as relative path from $CFG->wwwroot. - * - * @param moodle_url $url The URL pointing to a web resource. - * @param string $exmessage The expected output URL. - * @throws coding_exception If called on a non-local URL. - * @see \moodle_url::out_as_local_url() - * @covers \moodle_url::out_as_local_url - * @dataProvider out_as_local_url_coding_exception_provider - */ - public function test_out_as_local_url_coding_exception(\moodle_url $url, string $exmessage): void { - $this->expectException(\coding_exception::class); - $this->expectExceptionMessage($exmessage); - $localurl = $url->out_as_local_url(); - } - - /** - * Data provider for throwing coding exceptions in \moodle_url::out_as_local_url(). - * - * @return array - * @throws moodle_exception On seriously malformed URLs (parse_url). - * @see \moodle_url::out_as_local_url() - * @see parse_url() - */ - public function out_as_local_url_coding_exception_provider() { - return [ - 'Google Maps CDN (HTTPS)' => [ - new \moodle_url('https://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']), - 'Coding error detected, it must be fixed by a programmer: out_as_local_url called on a non-local URL' - ], - 'Google Maps CDN (HTTP)' => [ - new \moodle_url('http://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']), - 'Coding error detected, it must be fixed by a programmer: out_as_local_url called on a non-local URL' - ], - ]; - } - - /** - * Test URL as relative path from $CFG->wwwroot. - * - * @param moodle_url $url The URL pointing to a web resource. - * @param string $expected The expected local URL. - * @throws coding_exception If called on a non-local URL. - * @see \moodle_url::out_as_local_url() - * @covers \moodle_url::out_as_local_url - * @dataProvider out_as_local_url_provider - */ - public function test_out_as_local_url(\moodle_url $url, string $expected): void { - $this->assertEquals($expected, $url->out_as_local_url(false)); - } - - /** - * Data provider for returning local paths via \moodle_url::out_as_local_url(). - * - * @return array - * @throws moodle_exception On seriously malformed URLs (parse_url). - * @see \moodle_url::out_as_local_url() - * @see parse_url() - */ - public function out_as_local_url_provider() { - global $CFG; - $wwwroot = rtrim($CFG->wwwroot, '/'); - - return [ - 'Environment XML file' => [ - new \moodle_url('/admin/environment.xml'), - '/admin/environment.xml' - ], - 'H5P JS internal resource' => [ - new \moodle_url('/h5p/js/embed.js'), - '/h5p/js/embed.js' - ], - 'A Moodle JS resource using the full path including the proper JS Handler' => [ - new \moodle_url($wwwroot . '/lib/javascript.php/1/lib/editor/tiny/js/tinymce/tinymce.js'), - '/lib/javascript.php/1/lib/editor/tiny/js/tinymce/tinymce.js' - ], - ]; - } - - /** - * Test URL as relative path from $CFG->wwwroot. - * - * @param moodle_url $url The URL pointing to a web resource. - * @param bool $expected The expected result. - * @see \moodle_url::is_local_url() - * @covers \moodle_url::is_local_url - * @dataProvider is_local_url_provider - */ - public function test_is_local_url(\moodle_url $url, bool $expected): void { - $this->assertEquals($expected, $url->is_local_url(), "'{$url}' is not a local URL!"); - } - - /** - * Data provider for testing \moodle_url::is_local_url(). - * - * @return array - * @see \moodle_url::is_local_url() - */ - public function is_local_url_provider() { - global $CFG; - $wwwroot = rtrim($CFG->wwwroot, '/'); - - return [ - 'Google Maps CDN (HTTPS)' => [ - new \moodle_url('https://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']), - false - ], - 'Google Maps CDN (HTTP)' => [ - new \moodle_url('http://maps.googleapis.com/maps/api/js', ['key' => 'googlemapkey3', 'sensor' => 'false']), - false - ], - 'wwwroot' => [ - new \moodle_url($wwwroot), - true - ], - 'wwwroot/' => [ - new \moodle_url($wwwroot . '/'), - true - ], - 'Environment XML file' => [ - new \moodle_url('/admin/environment.xml'), - true - ], - 'H5P JS internal resource' => [ - new \moodle_url('/h5p/js/embed.js'), - true - ], - ]; - } - /** * Data provider for strip_querystring tests. * diff --git a/lib/weblib.php b/lib/weblib.php index d6351ea2fb2..e1a0bd345af 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -30,8 +30,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -use Psr\Http\Message\UriInterface; - defined('MOODLE_INTERNAL') || die(); // Constants. @@ -242,836 +240,6 @@ function get_local_referer($stripquery = true) { } } -/** - * Class for creating and manipulating urls. - * - * It can be used in moodle pages where config.php has been included without any further includes. - * - * It is useful for manipulating urls with long lists of params. - * One situation where it will be useful is a page which links to itself to perform various actions - * and / or to process form data. A moodle_url object : - * can be created for a page to refer to itself with all the proper get params being passed from page call to - * page call and methods can be used to output a url including all the params, optionally adding and overriding - * params and can also be used to - * - output the url without any get params - * - and output the params as hidden fields to be output within a form - * - * @copyright 2007 jamiesensei - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @package core - */ -class moodle_url { - - /** - * Scheme, ex.: http, https - * @var string - */ - protected $scheme = ''; - - /** - * Hostname. - * @var string - */ - protected $host = ''; - - /** - * Port number, empty means default 80 or 443 in case of http. - * @var int - */ - protected $port = ''; - - /** - * Username for http auth. - * @var string - */ - protected $user = ''; - - /** - * Password for http auth. - * @var string - */ - protected $pass = ''; - - /** - * Script path. - * @var string - */ - protected $path = ''; - - /** - * Optional slash argument value. - * @var string - */ - protected $slashargument = ''; - - /** - * Anchor, may be also empty, null means none. - * @var string - */ - protected $anchor = null; - - /** - * Url parameters as associative array. - * @var array - */ - protected $params = array(); - - /** - * Create new instance of moodle_url. - * - * @param moodle_url|string $url - moodle_url means make a copy of another - * moodle_url and change parameters, string means full url or shortened - * form (ex.: '/course/view.php'). It is strongly encouraged to not include - * query string because it may result in double encoded values. Use the - * $params instead. For admin URLs, just use /admin/script.php, this - * class takes care of the $CFG->admin issue. - * @param array $params these params override current params or add new - * @param string $anchor The anchor to use as part of the URL if there is one. - * @throws moodle_exception - */ - public function __construct($url, array $params = null, $anchor = null) { - global $CFG; - - if ($url instanceof moodle_url) { - $this->scheme = $url->scheme; - $this->host = $url->host; - $this->port = $url->port; - $this->user = $url->user; - $this->pass = $url->pass; - $this->path = $url->path; - $this->slashargument = $url->slashargument; - $this->params = $url->params; - $this->anchor = $url->anchor; - - } else { - $url = $url ?? ''; - // Detect if anchor used. - $apos = strpos($url, '#'); - if ($apos !== false) { - $anchor = substr($url, $apos); - $anchor = ltrim($anchor, '#'); - $this->set_anchor($anchor); - $url = substr($url, 0, $apos); - } - - // Normalise shortened form of our url ex.: '/course/view.php'. - if (strpos($url, '/') === 0) { - $url = $CFG->wwwroot.$url; - } - - if ($CFG->admin !== 'admin') { - if (strpos($url, "$CFG->wwwroot/admin/") === 0) { - $url = str_replace("$CFG->wwwroot/admin/", "$CFG->wwwroot/$CFG->admin/", $url); - } - } - - // Parse the $url. - $parts = parse_url($url); - if ($parts === false) { - throw new moodle_exception('invalidurl'); - } - if (isset($parts['query'])) { - // Note: the values may not be correctly decoded, url parameters should be always passed as array. - parse_str(str_replace('&', '&', $parts['query']), $this->params); - } - unset($parts['query']); - foreach ($parts as $key => $value) { - $this->$key = $value; - } - - // Detect slashargument value from path - we do not support directory names ending with .php. - $pos = strpos($this->path, '.php/'); - if ($pos !== false) { - $this->slashargument = substr($this->path, $pos + 4); - $this->path = substr($this->path, 0, $pos + 4); - } - } - - $this->params($params); - if ($anchor !== null) { - $this->anchor = (string)$anchor; - } - } - - /** - * Add an array of params to the params for this url. - * - * The added params override existing ones if they have the same name. - * - * @param array $params Defaults to null. If null then returns all params. - * @return array Array of Params for url. - * @throws coding_exception - */ - public function params(array $params = null) { - $params = (array)$params; - - foreach ($params as $key => $value) { - if (is_int($key)) { - throw new coding_exception('Url parameters can not have numeric keys!'); - } - if (!is_string($value)) { - if (is_array($value)) { - throw new coding_exception('Url parameters values can not be arrays!'); - } - if (is_object($value) and !method_exists($value, '__toString')) { - throw new coding_exception('Url parameters values can not be objects, unless __toString() is defined!'); - } - } - $this->params[$key] = (string)$value; - } - return $this->params; - } - - /** - * Remove all params if no arguments passed. - * Remove selected params if arguments are passed. - * - * Can be called as either remove_params('param1', 'param2') - * or remove_params(array('param1', 'param2')). - * - * @param string[]|string $params,... either an array of param names, or 1..n string params to remove as args. - * @return array url parameters - */ - public function remove_params($params = null) { - if (!is_array($params)) { - $params = func_get_args(); - } - foreach ($params as $param) { - unset($this->params[$param]); - } - return $this->params; - } - - /** - * Remove all url parameters. - * - * @todo remove the unused param. - * @param array $params Unused param - * @return void - */ - public function remove_all_params($params = null) { - $this->params = array(); - $this->slashargument = ''; - } - - /** - * Add a param to the params for this url. - * - * The added param overrides existing one if they have the same name. - * - * @param string $paramname name - * @param string $newvalue Param value. If new value specified current value is overriden or parameter is added - * @return mixed string parameter value, null if parameter does not exist - */ - public function param($paramname, $newvalue = '') { - if (func_num_args() > 1) { - // Set new value. - $this->params(array($paramname => $newvalue)); - } - if (isset($this->params[$paramname])) { - return $this->params[$paramname]; - } else { - return null; - } - } - - /** - * Merges parameters and validates them - * - * @param array $overrideparams - * @return array merged parameters - * @throws coding_exception - */ - protected function merge_overrideparams(array $overrideparams = null) { - $overrideparams = (array)$overrideparams; - $params = $this->params; - foreach ($overrideparams as $key => $value) { - if (is_int($key)) { - throw new coding_exception('Overridden parameters can not have numeric keys!'); - } - if (is_array($value)) { - throw new coding_exception('Overridden parameters values can not be arrays!'); - } - if (is_object($value) and !method_exists($value, '__toString')) { - throw new coding_exception('Overridden parameters values can not be objects, unless __toString() is defined!'); - } - $params[$key] = (string)$value; - } - return $params; - } - - /** - * Get the params as as a query string. - * - * This method should not be used outside of this method. - * - * @param bool $escaped Use & as params separator instead of plain & - * @param array $overrideparams params to add to the output params, these - * override existing ones with the same name. - * @return string query string that can be added to a url. - */ - public function get_query_string($escaped = true, array $overrideparams = null) { - $arr = array(); - if ($overrideparams !== null) { - $params = $this->merge_overrideparams($overrideparams); - } else { - $params = $this->params; - } - foreach ($params as $key => $val) { - if (is_array($val)) { - foreach ($val as $index => $value) { - $arr[] = rawurlencode($key.'['.$index.']')."=".rawurlencode($value); - } - } else { - if (isset($val) && $val !== '') { - $arr[] = rawurlencode($key)."=".rawurlencode($val); - } else { - $arr[] = rawurlencode($key); - } - } - } - if ($escaped) { - return implode('&', $arr); - } else { - return implode('&', $arr); - } - } - - /** - * Get the url params as an array of key => value pairs. - * - * This helps in handling cases where url params contain arrays. - * - * @return array params array for templates. - */ - public function export_params_for_template(): array { - $data = []; - foreach ($this->params as $key => $val) { - if (is_array($val)) { - foreach ($val as $index => $value) { - $data[] = ['name' => $key.'['.$index.']', 'value' => $value]; - } - } else { - $data[] = ['name' => $key, 'value' => $val]; - } - } - return $data; - } - - /** - * Shortcut for printing of encoded URL. - * - * @return string - */ - public function __toString() { - return $this->out(true); - } - - /** - * Output url. - * - * If you use the returned URL in HTML code, you want the escaped ampersands. If you use - * the returned URL in HTTP headers, you want $escaped=false. - * - * @param bool $escaped Use & as params separator instead of plain & - * @param array $overrideparams params to add to the output url, these override existing ones with the same name. - * @return string Resulting URL - */ - public function out($escaped = true, array $overrideparams = null) { - - global $CFG; - - if (!is_bool($escaped)) { - debugging('Escape parameter must be of type boolean, '.gettype($escaped).' given instead.'); - } - - $url = $this; - - // Allow url's to be rewritten by a plugin. - if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) { - $class = $CFG->urlrewriteclass; - $pluginurl = $class::url_rewrite($url); - if ($pluginurl instanceof moodle_url) { - $url = $pluginurl; - } - } - - return $url->raw_out($escaped, $overrideparams); - - } - - /** - * Output url without any rewrites - * - * This is identical in signature and use to out() but doesn't call the rewrite handler. - * - * @param bool $escaped Use & as params separator instead of plain & - * @param array $overrideparams params to add to the output url, these override existing ones with the same name. - * @return string Resulting URL - */ - public function raw_out($escaped = true, array $overrideparams = null) { - if (!is_bool($escaped)) { - debugging('Escape parameter must be of type boolean, '.gettype($escaped).' given instead.'); - } - - $uri = $this->out_omit_querystring().$this->slashargument; - - $querystring = $this->get_query_string($escaped, $overrideparams); - if ($querystring !== '') { - $uri .= '?' . $querystring; - } - - $uri .= $this->get_encoded_anchor(); - - return $uri; - } - - /** - * Encode the anchor according to RFC 3986. - * - * @return string The encoded anchor - */ - public function get_encoded_anchor(): string { - if (is_null($this->anchor)) { - return ''; - } - - // RFC 3986 allows the following characters in a fragment without them being encoded: - // pct-encoded: "%" HEXDIG HEXDIG - // unreserved: ALPHA / DIGIT / "-" / "." / "_" / "~" / - // sub-delims: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" / ":" / "@" - // fragment: "/" / "?" - // - // All other characters should be encoded. - // These should not be encoded in the fragment unless they were already encoded. - - // The following characters are allowed in the fragment without encoding. - // In addition to this list is pct-encoded, but we can't easily handle this with a regular expression. - $allowed = 'a-zA-Z0-9\\-._~!$&\'()*+,;=:@\/?'; - $anchor = '#'; - - $remainder = $this->anchor; - do { - // Split the string on any %. - $parts = explode('%', $remainder, 2); - $anchorparts = array_shift($parts); - - // The first part can go through our preg_replace_callback to quote any relevant characters. - $anchor .= preg_replace_callback( - '/[^' . $allowed . ']/', - fn ($matches) => rawurlencode($matches[0]), - $anchorparts, - ); - - // The second part _might_ be a valid pct-encoded character. - if (count($parts) === 0) { - break; - } - - // If the second part is a valid pct-encoded character, append it to the anchor. - $remainder = array_shift($parts); - if (preg_match('/^[a-fA-F0-9]{2}/', $remainder, $matches)) { - $anchor .= "%{$matches[0]}"; - $remainder = substr($remainder, 2); - } else { - // This was not a valid pct-encoded character. Encode the % and continue with the next part. - $anchor .= rawurlencode('%'); - } - } while (strlen($remainder) > 0); - - return $anchor; - } - - /** - * Returns url without parameters, everything before '?'. - * - * @param bool $includeanchor if {@link self::anchor} is defined, should it be returned? - * @return string - */ - public function out_omit_querystring($includeanchor = false) { - - $uri = $this->scheme ? $this->scheme.':'.((strtolower($this->scheme) == 'mailto') ? '':'//'): ''; - $uri .= $this->user ? $this->user.($this->pass? ':'.$this->pass:'').'@':''; - $uri .= $this->host ? $this->host : ''; - $uri .= $this->port ? ':'.$this->port : ''; - $uri .= $this->path ? $this->path : ''; - if ($includeanchor) { - $uri .= $this->get_encoded_anchor(); - } - - return $uri; - } - - /** - * Compares this moodle_url with another. - * - * See documentation of constants for an explanation of the comparison flags. - * - * @param moodle_url $url The moodle_url object to compare - * @param int $matchtype The type of comparison (URL_MATCH_BASE, URL_MATCH_PARAMS, URL_MATCH_EXACT) - * @return bool - */ - public function compare(moodle_url $url, $matchtype = URL_MATCH_EXACT) { - - $baseself = $this->out_omit_querystring(); - $baseother = $url->out_omit_querystring(); - - // Append index.php if there is no specific file. - if (substr($baseself, -1) == '/') { - $baseself .= 'index.php'; - } - if (substr($baseother, -1) == '/') { - $baseother .= 'index.php'; - } - - // Compare the two base URLs. - if ($baseself != $baseother) { - return false; - } - - if ($matchtype == URL_MATCH_BASE) { - return true; - } - - $urlparams = $url->params(); - foreach ($this->params() as $param => $value) { - if ($param == 'sesskey') { - continue; - } - if (!array_key_exists($param, $urlparams) || $urlparams[$param] != $value) { - return false; - } - } - - if ($matchtype == URL_MATCH_PARAMS) { - return true; - } - - foreach ($urlparams as $param => $value) { - if ($param == 'sesskey') { - continue; - } - if (!array_key_exists($param, $this->params()) || $this->param($param) != $value) { - return false; - } - } - - if ($url->anchor !== $this->anchor) { - return false; - } - - return true; - } - - /** - * Sets the anchor for the URI (the bit after the hash) - * - * @param string $anchor null means remove previous - */ - public function set_anchor($anchor) { - if (is_null($anchor)) { - // Remove. - $this->anchor = null; - } else { - $this->anchor = $anchor; - } - } - - /** - * Sets the scheme for the URI (the bit before ://) - * - * @param string $scheme - */ - public function set_scheme($scheme) { - // See http://www.ietf.org/rfc/rfc3986.txt part 3.1. - if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*$/', $scheme)) { - $this->scheme = $scheme; - } else { - throw new coding_exception('Bad URL scheme.'); - } - } - - /** - * Sets the url slashargument value. - * - * @param string $path usually file path - * @param string $parameter name of page parameter if slasharguments not supported - * @param bool $supported usually null, then it depends on $CFG->slasharguments, use true or false for other servers - * @return void - */ - public function set_slashargument($path, $parameter = 'file', $supported = null) { - global $CFG; - if (is_null($supported)) { - $supported = !empty($CFG->slasharguments); - } - - if ($supported) { - $parts = explode('/', $path); - $parts = array_map('rawurlencode', $parts); - $path = implode('/', $parts); - $this->slashargument = $path; - unset($this->params[$parameter]); - - } else { - $this->slashargument = ''; - $this->params[$parameter] = $path; - } - } - - // Static factory methods. - - /** - * Create a new moodle_url instance from a UriInterface. - * - * @param UriInterface $uri - * @return self - */ - public static function from_uri(UriInterface $uri): self { - $url = new self( - url: $uri->getScheme() . '://' . $uri->getAuthority() . $uri->getPath(), - anchor: $uri->getFragment() ?: null, - ); - - $params = $uri->getQuery(); - foreach (explode('&', $params) as $param) { - $url->param(...explode('=', $param, 2)); - } - - return $url; - } - - /** - * General moodle file url. - * - * @param string $urlbase the script serving the file - * @param string $path - * @param bool $forcedownload - * @return moodle_url - */ - public static function make_file_url($urlbase, $path, $forcedownload = false) { - $params = array(); - if ($forcedownload) { - $params['forcedownload'] = 1; - } - $url = new moodle_url($urlbase, $params); - $url->set_slashargument($path); - return $url; - } - - /** - * Factory method for creation of url pointing to plugin file. - * - * Please note this method can be used only from the plugins to - * create urls of own files, it must not be used outside of plugins! - * - * @param int $contextid - * @param string $component - * @param string $area - * @param ?int $itemid - * @param string $pathname - * @param string $filename - * @param bool $forcedownload - * @param mixed $includetoken Whether to use a user token when displaying this group image. - * True indicates to generate a token for current user, and integer value indicates to generate a token for the - * user whose id is the value indicated. - * If the group picture is included in an e-mail or some other location where the audience is a specific - * user who will not be logged in when viewing, then we use a token to authenticate the user. - * @return moodle_url - */ - public static function make_pluginfile_url($contextid, $component, $area, $itemid, $pathname, $filename, - $forcedownload = false, $includetoken = false) { - global $CFG, $USER; - - $path = []; - - if ($includetoken) { - $urlbase = "$CFG->wwwroot/tokenpluginfile.php"; - $userid = $includetoken === true ? $USER->id : $includetoken; - $token = get_user_key('core_files', $userid); - if ($CFG->slasharguments) { - $path[] = $token; - } - } else { - $urlbase = "$CFG->wwwroot/pluginfile.php"; - } - $path[] = $contextid; - $path[] = $component; - $path[] = $area; - - if ($itemid !== null) { - $path[] = $itemid; - } - - $path = "/" . implode('/', $path) . "{$pathname}{$filename}"; - - $url = self::make_file_url($urlbase, $path, $forcedownload, $includetoken); - if ($includetoken && empty($CFG->slasharguments)) { - $url->param('token', $token); - } - return $url; - } - - /** - * Factory method for creation of url pointing to plugin file. - * This method is the same that make_pluginfile_url but pointing to the webservice pluginfile.php script. - * It should be used only in external functions. - * - * @since 2.8 - * @param int $contextid - * @param string $component - * @param string $area - * @param int $itemid - * @param string $pathname - * @param string $filename - * @param bool $forcedownload - * @return moodle_url - */ - public static function make_webservice_pluginfile_url($contextid, $component, $area, $itemid, $pathname, $filename, - $forcedownload = false) { - global $CFG; - $urlbase = "$CFG->wwwroot/webservice/pluginfile.php"; - if ($itemid === null) { - return self::make_file_url($urlbase, "/$contextid/$component/$area".$pathname.$filename, $forcedownload); - } else { - return self::make_file_url($urlbase, "/$contextid/$component/$area/$itemid".$pathname.$filename, $forcedownload); - } - } - - /** - * Factory method for creation of url pointing to draft file of current user. - * - * @param int $draftid draft item id - * @param string $pathname - * @param string $filename - * @param bool $forcedownload - * @return moodle_url - */ - public static function make_draftfile_url($draftid, $pathname, $filename, $forcedownload = false) { - global $CFG, $USER; - $urlbase = "$CFG->wwwroot/draftfile.php"; - $context = context_user::instance($USER->id); - - return self::make_file_url($urlbase, "/$context->id/user/draft/$draftid".$pathname.$filename, $forcedownload); - } - - /** - * Factory method for creating of links to legacy course files. - * - * @param int $courseid - * @param string $filepath - * @param bool $forcedownload - * @return moodle_url - */ - public static function make_legacyfile_url($courseid, $filepath, $forcedownload = false) { - global $CFG; - - $urlbase = "$CFG->wwwroot/file.php"; - return self::make_file_url($urlbase, '/'.$courseid.'/'.$filepath, $forcedownload); - } - - /** - * Checks if URL is relative to $CFG->wwwroot. - * - * @return bool True if URL is relative to $CFG->wwwroot; otherwise, false. - */ - public function is_local_url(): bool { - global $CFG; - - $url = $this->out(); - // Does URL start with wwwroot? Otherwise, URL isn't relative to wwwroot. - return ( ($url === $CFG->wwwroot) || (strpos($url, $CFG->wwwroot.'/') === 0) ); - } - - /** - * Returns URL as relative path from $CFG->wwwroot - * - * Can be used for passing around urls with the wwwroot stripped - * - * @param boolean $escaped Use & as params separator instead of plain & - * @param array $overrideparams params to add to the output url, these override existing ones with the same name. - * @return string Resulting URL - * @throws coding_exception if called on a non-local url - */ - public function out_as_local_url($escaped = true, array $overrideparams = null) { - global $CFG; - - // URL should be relative to wwwroot. If not then throw exception. - if ($this->is_local_url()) { - $url = $this->out($escaped, $overrideparams); - $localurl = substr($url, strlen($CFG->wwwroot)); - return !empty($localurl) ? $localurl : ''; - } else { - throw new coding_exception('out_as_local_url called on a non-local URL'); - } - } - - /** - * Returns the 'path' portion of a URL. For example, if the URL is - * http://www.example.org:447/my/file/is/here.txt?really=1 then this will - * return '/my/file/is/here.txt'. - * - * By default the path includes slash-arguments (for example, - * '/myfile.php/extra/arguments') so it is what you would expect from a - * URL path. If you don't want this behaviour, you can opt to exclude the - * slash arguments. (Be careful: if the $CFG variable slasharguments is - * disabled, these URLs will have a different format and you may need to - * look at the 'file' parameter too.) - * - * @param bool $includeslashargument If true, includes slash arguments - * @return string Path of URL - */ - public function get_path($includeslashargument = true) { - return $this->path . ($includeslashargument ? $this->slashargument : ''); - } - - /** - * Returns a given parameter value from the URL. - * - * @param string $name Name of parameter - * @return string Value of parameter or null if not set - */ - public function get_param($name) { - if (array_key_exists($name, $this->params)) { - return $this->params[$name]; - } else { - return null; - } - } - - /** - * Returns the 'scheme' portion of a URL. For example, if the URL is - * http://www.example.org:447/my/file/is/here.txt?really=1 then this will - * return 'http' (without the colon). - * - * @return string Scheme of the URL. - */ - public function get_scheme() { - return $this->scheme; - } - - /** - * Returns the 'host' portion of a URL. For example, if the URL is - * http://www.example.org:447/my/file/is/here.txt?really=1 then this will - * return 'www.example.org'. - * - * @return string Host of the URL. - */ - public function get_host() { - return $this->host; - } - - /** - * Returns the 'port' portion of a URL. For example, if the URL is - * http://www.example.org:447/my/file/is/here.txt?really=1 then this will - * return '447'. - * - * @return string Port of the URL. - */ - public function get_port() { - return $this->port; - } -} - /** * Determine if there is data waiting to be processed from a form * @@ -3420,295 +2588,6 @@ function is_in_popup() { return ($inpopup); } -/** - * Progress trace class. - * - * Use this class from long operations where you want to output occasional information about - * what is going on, but don't know if, or in what format, the output should be. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @package core - */ -abstract class progress_trace { - /** - * Output an progress message in whatever format. - * - * @param string $message the message to output. - * @param integer $depth indent depth for this message. - */ - abstract public function output($message, $depth = 0); - - /** - * Called when the processing is finished. - */ - public function finished() { - } -} - -/** - * This subclass of progress_trace does not ouput anything. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @package core - */ -class null_progress_trace extends progress_trace { - /** - * Does Nothing - * - * @param string $message - * @param int $depth - * @return void Does Nothing - */ - public function output($message, $depth = 0) { - } -} - -/** - * This subclass of progress_trace outputs to plain text. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @package core - */ -class text_progress_trace extends progress_trace { - /** - * Output the trace message. - * - * @param string $message - * @param int $depth - * @return void Output is echo'd - */ - public function output($message, $depth = 0) { - mtrace(str_repeat(' ', $depth) . $message); - } -} - -/** - * This subclass of progress_trace outputs as HTML. - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @package core - */ -class html_progress_trace extends progress_trace { - /** - * Output the trace message. - * - * @param string $message - * @param int $depth - * @return void Output is echo'd - */ - public function output($message, $depth = 0) { - echo '', str_repeat(' ', $depth), htmlspecialchars($message, ENT_COMPAT), "
\n"; - flush(); - } -} - -/** - * HTML List Progress Tree - * - * @copyright 2009 Tim Hunt - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @package core - */ -class html_list_progress_trace extends progress_trace { - /** @var int */ - protected $currentdepth = -1; - - /** - * Echo out the list - * - * @param string $message The message to display - * @param int $depth - * @return void Output is echoed - */ - public function output($message, $depth = 0) { - $samedepth = true; - while ($this->currentdepth > $depth) { - echo "