diff --git a/lib/classes/url.php b/lib/classes/url.php new file mode 100644 index 00000000000..5e8a6da1022 --- /dev/null +++ b/lib/classes/url.php @@ -0,0 +1,848 @@ +. + +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 = array(); + + /** + * Create new instance of url. + * + * @param url|string $url - url means make a copy of another + * 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 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 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 url with another. + * + * See documentation of constants for an explanation of the comparison flags. + * + * @param url $url The 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(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 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 url + */ + public static function make_file_url($urlbase, $path, $forcedownload = false) { + $params = array(); + if ($forcedownload) { + $params['forcedownload'] = 1; + } + $url = new 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 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; + } +} + +class_alias(url::class, \moodle_url::class); diff --git a/lib/db/legacyclasses.php b/lib/db/legacyclasses.php index 75076d95c32..0cc09322714 100644 --- a/lib/db/legacyclasses.php +++ b/lib/db/legacyclasses.php @@ -38,6 +38,7 @@ $legacyclasses = [ \invalid_response_exception::class => 'exception/invalid_response_exception.php', \invalid_state_exception::class => 'exception/invalid_state_exception.php', \moodle_exception::class => 'exception/moodle_exception.php', + \moodle_url::class => 'url.php', \require_login_exception::class => 'exception/require_login_exception.php', \require_login_session_timeout_exception::class => 'exception/require_login_session_timeout_exception.php', \required_capability_exception::class => 'exception/required_capability_exception.php', diff --git a/lib/weblib.php b/lib/weblib.php index d6351ea2fb2..7e9634c890c 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. @@ -81,6 +79,9 @@ define('URL_MATCH_PARAMS', 1); */ define('URL_MATCH_EXACT', 2); +// TODO MDL-81933 Remove after Moodle 4.5 release. +require_once($CFG->libdir . '/classes/url.php'); + // Functions. /** @@ -242,836 +243,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 *