. /** * Library of functions for web output * * Library of all general-purpose Moodle PHP functions and constants * that produce HTML output * * Other main libraries: * - datalib.php - functions that access the database. * - moodlelib.php - general-purpose Moodle functions. * * @package core * @subpackage lib * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); // Constants. // Define text formatting types ... eventually we can add Wiki, BBcode etc. /** * Does all sorts of transformations and filtering. */ define('FORMAT_MOODLE', '0'); /** * Plain HTML (with some tags stripped). */ define('FORMAT_HTML', '1'); /** * Plain text (even tags are printed in full). */ define('FORMAT_PLAIN', '2'); /** * Wiki-formatted text. * Deprecated: left here just to note that '3' is not used (at the moment) * and to catch any latent wiki-like text (which generates an error) * @deprecated since 2005! */ define('FORMAT_WIKI', '3'); /** * Markdown-formatted text http://daringfireball.net/projects/markdown/ */ define('FORMAT_MARKDOWN', '4'); /** * A moodle_url comparison using this flag will return true if the base URLs match, params are ignored. */ define('URL_MATCH_BASE', 0); /** * A moodle_url comparison using this flag will return true if the base URLs match and the params of url1 are part of url2. */ define('URL_MATCH_PARAMS', 1); /** * A moodle_url comparison using this flag will return true if the two URLs are identical, except for the order of the params. */ define('URL_MATCH_EXACT', 2); // Functions. /** * Add quotes to HTML characters. * * Returns $var with HTML characters (like "<", ">", etc.) properly quoted. * Related function {@link p()} simply prints the output of this function. * * @param string $var the string potentially containing HTML characters * @return string */ function s($var) { if ($var === false) { return '0'; } if ($var === null || $var === '') { return ''; } return preg_replace( '/&#(\d+|x[0-9a-f]+);/i', '$1;', htmlspecialchars($var, ENT_QUOTES | ENT_HTML401 | ENT_SUBSTITUTE) ); } /** * Add quotes to HTML characters. * * Prints $var with HTML characters (like "<", ">", etc.) properly quoted. * This function simply calls & displays {@link s()}. * @see s() * * @param string $var the string potentially containing HTML characters */ function p($var) { echo s($var); } /** * Does proper javascript quoting. * * Do not use addslashes anymore, because it does not work when magic_quotes_sybase is enabled. * * @param mixed $var String, Array, or Object to add slashes to * @return mixed quoted result */ function addslashes_js($var) { if (is_string($var)) { $var = str_replace('\\', '\\\\', $var); $var = str_replace(array('\'', '"', "\n", "\r", "\0"), array('\\\'', '\\"', '\\n', '\\r', '\\0'), $var); $var = str_replace('', '<\/', $var); // XHTML compliance. } else if (is_array($var)) { $var = array_map('addslashes_js', $var); } else if (is_object($var)) { $a = get_object_vars($var); foreach ($a as $key => $value) { $a[$key] = addslashes_js($value); } $var = (object)$a; } return $var; } /** * Remove query string from url. * * Takes in a URL and returns it without the querystring portion. * * @param string $url the url which may have a query string attached. * @return string The remaining URL. */ function strip_querystring($url) { if ($url === null || $url === '') { return ''; } if ($commapos = strpos($url, '?')) { return substr($url, 0, $commapos); } else { return $url; } } /** * Returns the name of the current script, WITH the querystring portion. * * This function is necessary because PHP_SELF and REQUEST_URI and SCRIPT_NAME * return different things depending on a lot of things like your OS, Web * server, and the way PHP is compiled (ie. as a CGI, module, ISAPI, etc.) * NOTE: This function returns false if the global variables needed are not set. * * @return mixed String or false if the global variables needed are not set. */ function me() { global $ME; return $ME; } /** * Guesses the full URL of the current script. * * This function is using $PAGE->url, but may fall back to $FULLME which * is constructed from PHP_SELF and REQUEST_URI or SCRIPT_NAME * * @return mixed full page URL string or false if unknown */ function qualified_me() { global $FULLME, $PAGE, $CFG; if (isset($PAGE) and $PAGE->has_set_url()) { // This is the only recommended way to find out current page. return $PAGE->url->out(false); } else { if ($FULLME === null) { // CLI script most probably. return false; } if (!empty($CFG->sslproxy)) { // Return only https links when using SSL proxy. return preg_replace('/^http:/', 'https:', $FULLME, 1); } else { return $FULLME; } } } /** * Determines whether or not the Moodle site is being served over HTTPS. * * This is done simply by checking the value of $CFG->wwwroot, which seems * to be the only reliable method. * * @return boolean True if site is served over HTTPS, false otherwise. */ function is_https() { global $CFG; return (strpos($CFG->wwwroot, 'https://') === 0); } /** * Returns the cleaned local URL of the HTTP_REFERER less the URL query string parameters if required. * * @param bool $stripquery if true, also removes the query part of the url. * @return string The resulting referer or empty string. */ function get_local_referer($stripquery = true) { if (isset($_SERVER['HTTP_REFERER'])) { $referer = clean_param($_SERVER['HTTP_REFERER'], PARAM_LOCALURL); if ($stripquery) { return strip_querystring($referer); } else { return $referer; } } else { return ''; } } /** * Determine if there is data waiting to be processed from a form * * Used on most forms in Moodle to check for data * Returns the data as an object, if it's found. * This object can be used in foreach loops without * casting because it's cast to (array) automatically * * Checks that submitted POST data exists and returns it as object. * * @return mixed false or object */ function data_submitted() { if (empty($_POST)) { return false; } else { return (object)fix_utf8($_POST); } } /** * Given some normal text this function will break up any * long words to a given size by inserting the given character * * It's multibyte savvy and doesn't change anything inside html tags. * * @param string $string the string to be modified * @param int $maxsize maximum length of the string to be returned * @param string $cutchar the string used to represent word breaks * @return string */ function break_up_long_words($string, $maxsize=20, $cutchar=' ') { // First of all, save all the tags inside the text to skip them. $tags = array(); filter_save_tags($string, $tags); // Process the string adding the cut when necessary. $output = ''; $length = core_text::strlen($string); $wordlength = 0; for ($i=0; $i<$length; $i++) { $char = core_text::substr($string, $i, 1); if ($char == ' ' or $char == "\t" or $char == "\n" or $char == "\r" or $char == "<" or $char == ">") { $wordlength = 0; } else { $wordlength++; if ($wordlength > $maxsize) { $output .= $cutchar; $wordlength = 0; } } $output .= $char; } // Finally load the tags back again. if (!empty($tags)) { $output = str_replace(array_keys($tags), $tags, $output); } return $output; } /** * Try and close the current window using JavaScript, either immediately, or after a delay. * * Echo's out the resulting XHTML & javascript * * @param integer $delay a delay in seconds before closing the window. Default 0. * @param boolean $reloadopener if true, we will see if this window was a pop-up, and try * to reload the parent window before this one closes. */ function close_window($delay = 0, $reloadopener = false) { global $PAGE, $OUTPUT; if (!$PAGE->headerprinted) { $PAGE->set_title(get_string('closewindow')); echo $OUTPUT->header(); } else { $OUTPUT->container_end_all(false); } if ($reloadopener) { // Trigger the reload immediately, even if the reload is after a delay. $PAGE->requires->js_function_call('window.opener.location.reload', array(true)); } $OUTPUT->notification(get_string('windowclosing'), 'notifysuccess'); $PAGE->requires->js_function_call('close_window', array(new stdClass()), false, $delay); echo $OUTPUT->footer(); exit; } /** * Returns a string containing a link to the user documentation for the current page. * * Also contains an icon by default. Shown to teachers and admin only. * * @param string $text The text to be displayed for the link * @return string The link to user documentation for this current page */ function page_doc_link($text='') { global $OUTPUT, $PAGE; $path = page_get_doc_link_path($PAGE); if (!$path) { return ''; } return $OUTPUT->doc_link($path, $text); } /** * Returns the path to use when constructing a link to the docs. * * @since Moodle 2.5.1 2.6 * @param moodle_page $page * @return string */ function page_get_doc_link_path(moodle_page $page) { global $CFG; if (empty($CFG->docroot) || during_initial_install()) { return ''; } if (!has_capability('moodle/site:doclinks', $page->context)) { return ''; } $path = $page->docspath; if (!$path) { return ''; } return $path; } /** * Validates an email to make sure it makes sense. * * @param string $address The email address to validate. * @return boolean */ function validate_email($address) { global $CFG; if ($address === null || $address === false || $address === '') { return false; } require_once("{$CFG->libdir}/phpmailer/moodle_phpmailer.php"); return moodle_phpmailer::validateAddress($address ?? '') && !preg_match('/[<>]/', $address); } /** * Extracts file argument either from file parameter or PATH_INFO * * Note: $scriptname parameter is not needed anymore * * @return string file path (only safe characters) */ function get_file_argument() { global $SCRIPT; $relativepath = false; $hasforcedslashargs = false; if (isset($_SERVER['REQUEST_URI']) && !empty($_SERVER['REQUEST_URI'])) { // Checks whether $_SERVER['REQUEST_URI'] contains '/pluginfile.php/' // instead of '/pluginfile.php?', when serving a file from e.g. mod_imscp or mod_scorm. if ((strpos($_SERVER['REQUEST_URI'], '/pluginfile.php/') !== false) && isset($_SERVER['PATH_INFO']) && !empty($_SERVER['PATH_INFO'])) { // Exclude edge cases like '/pluginfile.php/?file='. $args = explode('/', ltrim($_SERVER['PATH_INFO'], '/')); $hasforcedslashargs = (count($args) > 2); // Always at least: context, component and filearea. } } if (!$hasforcedslashargs) { $relativepath = optional_param('file', false, PARAM_PATH); } if ($relativepath !== false and $relativepath !== '') { return $relativepath; } $relativepath = false; // Then try extract file from the slasharguments. if (stripos($_SERVER['SERVER_SOFTWARE'], 'iis') !== false) { // NOTE: IIS tends to convert all file paths to single byte DOS encoding, // we can not use other methods because they break unicode chars, // the only ways are to use URL rewriting // OR // to properly set the 'FastCGIUtf8ServerVariables' registry key. if (isset($_SERVER['PATH_INFO']) and $_SERVER['PATH_INFO'] !== '') { // Check that PATH_INFO works == must not contain the script name. if (strpos($_SERVER['PATH_INFO'], $SCRIPT) === false) { $relativepath = clean_param(urldecode($_SERVER['PATH_INFO']), PARAM_PATH); } } } else { // All other apache-like servers depend on PATH_INFO. if (isset($_SERVER['PATH_INFO'])) { if (isset($_SERVER['SCRIPT_NAME']) and strpos($_SERVER['PATH_INFO'], $_SERVER['SCRIPT_NAME']) === 0) { $relativepath = substr($_SERVER['PATH_INFO'], strlen($_SERVER['SCRIPT_NAME'])); } else { $relativepath = $_SERVER['PATH_INFO']; } $relativepath = clean_param($relativepath, PARAM_PATH); } } return $relativepath; } /** * Just returns an array of text formats suitable for a popup menu * * @return array */ function format_text_menu() { return array (FORMAT_MOODLE => get_string('formattext'), FORMAT_HTML => get_string('formathtml'), FORMAT_PLAIN => get_string('formatplain'), FORMAT_MARKDOWN => get_string('formatmarkdown')); } /** * Given text in a variety of format codings, this function returns the text as safe HTML. * * This function should mainly be used for long strings like posts, * answers, glossary items etc. For short strings {@link format_string()}. * *
* Options: * trusted : If true the string won't be cleaned. Default false required noclean=true. * noclean : If true the string won't be cleaned, unless $CFG->forceclean is set. Default false required trusted=true. * filter : If true the string will be run through applicable filters as well. Default true. * para : If true then the returned string will be wrapped in div tags. Default true. * newlines : If true then lines newline breaks will be converted to HTML newline breaks. Default true. * context : The context that will be used for filtering. * overflowdiv : If set to true the formatted text will be encased in a div * with the class no-overflow before being returned. Default false. * allowid : If true then id attributes will not be removed, even when * using htmlpurifier. Default false. * blanktarget : If true all tags will have target="_blank" added unless target is explicitly specified. ** * @param string $text The text to be formatted. This is raw text originally from user input. * @param int $format Identifier of the text format to be used * [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_MARKDOWN] * @param stdClass|array $options text formatting options * @param int $courseiddonotuse deprecated course id, use context option instead * @return string */ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseiddonotuse = null) { global $CFG; // Manually include the formatting class for now until after the release after 4.5 LTS. require_once("{$CFG->libdir}/classes/formatting.php"); if ($format === FORMAT_WIKI) { // This format was deprecated in Moodle 1.5. throw new \coding_exception( 'Wiki-like formatting is not supported.' ); } if ($options instanceof \core\context) { // A common mistake has been to call this function with a context object. // This has never been expected, or nor supported. debugging( 'The options argument should not be a context object directly. ' . ' Please pass an array with a context key instead.', DEBUG_DEVELOPER, ); $params['context'] = $options; $options = []; } if ($options) { $options = (array) $options; } if (empty($CFG->version) || $CFG->version < 2013051400 || during_initial_install()) { // Do not filter anything during installation or before upgrade completes. $params['context'] = null; } else if ($options && isset($options['context'])) { // First by explicit passed context option. if (is_numeric($options['context'])) { // A contextid was passed. $params['context'] = \core\context::instance_by_id($options['context']); } else if ($options['context'] instanceof \core\context) { $params['context'] = $options['context']; } else { debugging( 'Unknown context passed to format_text(). Content will not be filtered.', DEBUG_DEVELOPER, ); } // Unset the context from $options to prevent it overriding the configured value. unset($options['context']); } else if ($courseiddonotuse) { // Legacy courseid. $params['context'] = \core\context\course::instance($courseiddonotuse); debugging( "Passing a courseid to format_text() is deprecated, please pass a context instead.", DEBUG_DEVELOPER, ); } $params['text'] = $text; if ($options) { // The smiley option was deprecated in Moodle 2.0. if (array_key_exists('smiley', $options)) { unset($options['smiley']); debugging( 'The smiley option is deprecated and no longer used.', DEBUG_DEVELOPER, ); } // The nocache option was deprecated in Moodle 2.3 in MDL-34347. if (array_key_exists('nocache', $options)) { unset($options['nocache']); debugging( 'The nocache option is deprecated and no longer used.', DEBUG_DEVELOPER, ); } $validoptions = [ 'text', 'format', 'context', 'trusted', 'clean', 'filter', 'para', 'newlines', 'overflowdiv', 'blanktarget', 'allowid', 'noclean', ]; $invalidoptions = array_diff(array_keys($options), $validoptions); if ($invalidoptions) { debugging(sprintf( 'The following options are not valid: %s', implode(', ', $invalidoptions), ), DEBUG_DEVELOPER); foreach ($invalidoptions as $option) { unset($options[$option]); } } foreach ($options as $option => $value) { $params[$option] = $value; } // The noclean option has been renamed to clean. if (array_key_exists('noclean', $params)) { $params['clean'] = !$params['noclean']; unset($params['noclean']); } } if ($format !== null) { $params['format'] = $format; } return \core\di::get(\core\formatting::class)->format_text(...$params); } /** * Resets some data related to filters, called during upgrade or when general filter settings change. * * @param bool $phpunitreset true means called from our PHPUnit integration test reset * @return void */ function reset_text_filters_cache($phpunitreset = false) { global $CFG, $DB; if ($phpunitreset) { // HTMLPurifier does not change, DB is already reset to defaults, // nothing to do here, the dataroot was cleared too. return; } // The purge_all_caches() deals with cachedir and localcachedir purging, // the individual filter caches are invalidated as necessary elsewhere. // Update $CFG->filterall cache flag. if (empty($CFG->stringfilters)) { set_config('filterall', 0); return; } $installedfilters = core_component::get_plugin_list('filter'); $filters = explode(',', $CFG->stringfilters); foreach ($filters as $filter) { if (isset($installedfilters[$filter])) { set_config('filterall', 1); return; } } set_config('filterall', 0); } /** * Given a simple string, this function returns the string * processed by enabled string filters if $CFG->filterall is enabled * * This function should be used to print short strings (non html) that * need filter processing e.g. activity titles, post subjects, * glossary concepts. * * @staticvar bool $strcache * @param string $string The string to be filtered. Should be plain text, expect * possibly for multilang tags. * @param ?bool $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713 * @param array $options options array/object or courseid * @return string */ function format_string($string, $striplinks = true, $options = null) { global $CFG; // Manually include the formatting class for now until after the release after 4.5 LTS. require_once("{$CFG->libdir}/classes/formatting.php"); $params = [ 'string' => $string, 'striplinks' => (bool) $striplinks, ]; // This method only expects either: // - an array of options; // - a stdClass of options to be cast to an array; or // - an integer courseid. if ($options instanceof \core\context) { // A common mistake has been to call this function with a context object. // This has never been expected, or nor supported. debugging( 'The options argument should not be a context object directly. ' . ' Please pass an array with a context key instead.', DEBUG_DEVELOPER, ); $params['context'] = $options; $options = []; } else if (is_numeric($options)) { // Legacy courseid usage. $params['context'] = \core\context\course::instance($options); $options = []; } else if (is_array($options) || is_a($options, \stdClass::class)) { $options = (array) $options; if (isset($options['context'])) { if (is_numeric($options['context'])) { // A contextid was passed usage. $params['context'] = \core\context::instance_by_id($options['context']); } else if ($options['context'] instanceof \core\context) { $params['context'] = $options['context']; } else { debugging( 'An invalid value for context was provided.', DEBUG_DEVELOPER, ); } } } else if ($options !== null) { // Something else was passed, so we'll just use an empty array. debugging(sprintf( 'The options argument should be an Array, or stdclass. %s passed.', gettype($options), ), DEBUG_DEVELOPER); // Attempt to cast to array since we always used to, but throw in some debugging. $options = array_filter( (array) $options, fn ($key) => !is_numeric($key), ARRAY_FILTER_USE_KEY, ); } if (isset($options['filter'])) { $params['filter'] = (bool) $options['filter']; } else { $params['filter'] = true; } if (isset($options['escape'])) { $params['escape'] = (bool) $options['escape']; } else { $params['escape'] = true; } $validoptions = [ 'string', 'striplinks', 'context', 'filter', 'escape', ]; if ($options) { $invalidoptions = array_diff(array_keys($options), $validoptions); if ($invalidoptions) { debugging(sprintf( 'The following options are not valid: %s', implode(', ', $invalidoptions), ), DEBUG_DEVELOPER); } } return \core\di::get(\core\formatting::class)->format_string( ...$params, ); } /** * Given a string, performs a negative lookahead looking for any ampersand character * that is not followed by a proper HTML entity. If any is found, it is replaced * by &. The string is then returned. * * @param string $string * @return string */ function replace_ampersands_not_followed_by_entity($string) { return preg_replace("/\&(?![a-zA-Z0-9#]{1,8};)/", "&", $string ?? ''); } /** * Given a string, replaces all .* by .* and returns the string. * * @param string $string * @return string */ function strip_links($string) { return preg_replace('/(]+?>)(.+?)(<\/a>)/is', '$2', $string); } /** * This expression turns links into something nice in a text format. (Russell Jungwirth) * * @param string $string * @return string */ function wikify_links($string) { return preg_replace('~(]*>([^<]*))~i', '$3 [ $2 ]', $string); } /** * Given text in a variety of format codings, this function returns the text as plain text suitable for plain email. * * @param string $text The text to be formatted. This is raw text originally from user input. * @param int $format Identifier of the text format to be used * [FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN, FORMAT_WIKI, FORMAT_MARKDOWN] * @return string */ function format_text_email($text, $format) { switch ($format) { case FORMAT_PLAIN: return $text; break; case FORMAT_WIKI: // There should not be any of these any more! $text = wikify_links($text); return core_text::entities_to_utf8(strip_tags($text), true); break; case FORMAT_HTML: return html_to_text($text); break; case FORMAT_MOODLE: case FORMAT_MARKDOWN: default: $text = wikify_links($text); return core_text::entities_to_utf8(strip_tags($text), true); break; } } /** * Formats activity intro text * * @param string $module name of module * @param object $activity instance of activity * @param int $cmid course module id * @param bool $filter filter resulting html text * @return string */ function format_module_intro($module, $activity, $cmid, $filter=true) { global $CFG; require_once("$CFG->libdir/filelib.php"); $context = context_module::instance($cmid); $options = array('noclean' => true, 'para' => false, 'filter' => $filter, 'context' => $context, 'overflowdiv' => true); $intro = file_rewrite_pluginfile_urls($activity->intro, 'pluginfile.php', $context->id, 'mod_'.$module, 'intro', null); return trim(format_text($intro, $activity->introformat, $options, null)); } /** * Removes the usage of Moodle files from a text. * * In some rare cases we need to re-use a text that already has embedded links * to some files hosted within Moodle. But the new area in which we will push * this content does not support files... therefore we need to remove those files. * * @param string $source The text * @return string The stripped text */ function strip_pluginfile_content($source) { $baseurl = '@@PLUGINFILE@@'; // Looking for something like < .* "@@pluginfile@@.*" .* > $pattern = '$<[^<>]+["\']' . $baseurl . '[^"\']*["\'][^<>]*>$'; $stripped = preg_replace($pattern, '', $source); // Use purify html to rebalence potentially mismatched tags and generally cleanup. return purify_html($stripped); } /** * Legacy function, used for cleaning of old forum and glossary text only. * * @param string $text text that may contain legacy TRUSTTEXT marker * @return string text without legacy TRUSTTEXT marker */ function trusttext_strip($text) { if (!is_string($text)) { // This avoids the potential for an endless loop below. throw new coding_exception('trusttext_strip parameter must be a string'); } while (true) { // Removing nested TRUSTTEXT. $orig = $text; $text = str_replace('#####TRUSTTEXT#####', '', $text); if (strcmp($orig, $text) === 0) { return $text; } } } /** * Must be called before editing of all texts with trust flag. Removes all XSS nasties from texts stored in database if needed. * * @param stdClass $object data object with xxx, xxxformat and xxxtrust fields * @param string $field name of text field * @param context $context active context * @return stdClass updated $object */ function trusttext_pre_edit($object, $field, $context) { $trustfield = $field.'trust'; $formatfield = $field.'format'; if ($object->$formatfield == FORMAT_MARKDOWN) { // We do not have a way to sanitise Markdown texts, // luckily editors for this format should not have XSS problems. return $object; } if (!$object->$trustfield or !trusttext_trusted($context)) { $object->$field = clean_text($object->$field, $object->$formatfield); } return $object; } /** * Is current user trusted to enter no dangerous XSS in this context? * * Please note the user must be in fact trusted everywhere on this server!! * * @param context $context * @return bool true if user trusted */ function trusttext_trusted($context) { return (trusttext_active() and has_capability('moodle/site:trustcontent', $context)); } /** * Is trusttext feature active? * * @return bool */ function trusttext_active() { global $CFG; return !empty($CFG->enabletrusttext); } /** * Cleans raw text removing nasties. * * Given raw text (eg typed in by a user) this function cleans it up and removes any nasty tags that could mess up * Moodle pages through XSS attacks. * * The result must be used as a HTML text fragment, this function can not cleanup random * parts of html tags such as url or src attributes. * * NOTE: the format parameter was deprecated because we can safely clean only HTML. * * @param string $text The text to be cleaned * @param int|string $format deprecated parameter, should always contain FORMAT_HTML or FORMAT_MOODLE * @param array $options Array of options; currently only option supported is 'allowid' (if true, * does not remove id attributes when cleaning) * @return string The cleaned up text */ function clean_text($text, $format = FORMAT_HTML, $options = array()) { $text = (string)$text; if ($format != FORMAT_HTML and $format != FORMAT_HTML) { // TODO: we need to standardise cleanup of text when loading it into editor first. // debugging('clean_text() is designed to work only with html');. } if ($format == FORMAT_PLAIN) { return $text; } if (is_purify_html_necessary($text)) { $text = purify_html($text, $options); } // Originally we tried to neutralise some script events here, it was a wrong approach because // it was trivial to work around that (for example using style based XSS exploits). // We must not give false sense of security here - all developers MUST understand how to use // rawurlencode(), htmlentities(), htmlspecialchars(), p(), s(), moodle_url, html_writer and friends!!! return $text; } /** * Is it necessary to use HTMLPurifier? * * @private * @param string $text * @return bool false means html is safe and valid, true means use HTMLPurifier */ function is_purify_html_necessary($text) { if ($text === '') { return false; } if ($text === (string)((int)$text)) { return false; } if (strpos($text, '&') !== false or preg_match('|<[^pesb/]|', $text)) { // We need to normalise entities or other tags except p, em, strong and br present. return true; } $altered = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8', true); if ($altered === $text) { // No < > or other special chars means this must be safe. return false; } // Let's try to convert back some safe html tags. $altered = preg_replace('|<p>(.*?)</p>|m', '
$1
', $altered); if ($altered === $text) { return false; } $altered = preg_replace('|<em>([^<>]+?)</em>|m', '$1', $altered); if ($altered === $text) { return false; } $altered = preg_replace('|<strong>([^<>]+?)</strong>|m', '$1', $altered); if ($altered === $text) { return false; } $altered = str_replace('<br />', '
* if (!isset($CFG->additionalhtmlhead)) {
* $CFG->additionalhtmlhead = '';
* }
* $CFG->additionalhtmlhead .= '';
* header('X-UA-Compatible: IE=8');
* echo $OUTPUT->header();
*
*
* Please note the $CFG->additionalhtmlhead alone might not work,
* you should send the IE compatibility header() too.
*
* @param string $contenttype
* @param bool $cacheable Can this page be cached on back?
* @return void, sends HTTP headers
*/
function send_headers($contenttype, $cacheable = true) {
global $CFG;
@header('Content-Type: ' . $contenttype);
@header('Content-Script-Type: text/javascript');
@header('Content-Style-Type: text/css');
if (empty($CFG->additionalhtmlhead) or stripos($CFG->additionalhtmlhead, 'X-UA-Compatible') === false) {
@header('X-UA-Compatible: IE=edge');
}
if ($cacheable) {
// Allow caching on "back" (but not on normal clicks).
@header('Cache-Control: private, pre-check=0, post-check=0, max-age=0, no-transform');
@header('Pragma: no-cache');
@header('Expires: ');
} else {
// Do everything we can to always prevent clients and proxies caching.
@header('Cache-Control: no-store, no-cache, must-revalidate');
@header('Cache-Control: post-check=0, pre-check=0, no-transform', false);
@header('Pragma: no-cache');
@header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
@header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
}
@header('Accept-Ranges: none');
// The Moodle app must be allowed to embed content always.
if (empty($CFG->allowframembedding) && !core_useragent::is_moodle_app()) {
@header('X-Frame-Options: sameorigin');
}
// If referrer policy is set, add a referrer header.
if (!empty($CFG->referrerpolicy) && ($CFG->referrerpolicy !== 'default')) {
@header('Referrer-Policy: ' . $CFG->referrerpolicy);
}
}
/**
* Return the right arrow with text ('next'), and optionally embedded in a link.
*
* @param string $text HTML/plain text label (set to blank only for breadcrumb separator cases).
* @param string $url An optional link to use in a surrounding HTML anchor.
* @param bool $accesshide True if text should be hidden (for screen readers only).
* @param string $addclass Additional class names for the link, or the arrow character.
* @return string HTML string.
*/
function link_arrow_right($text, $url='', $accesshide=false, $addclass='', $addparams = []) {
global $OUTPUT; // TODO: move to output renderer.
$arrowclass = 'arrow ';
if (!$url) {
$arrowclass .= $addclass;
}
$arrow = ' ';
$htmltext = '';
if ($text) {
$htmltext = ''.$text.' ';
if ($accesshide) {
$htmltext = get_accesshide($htmltext);
}
}
if ($url) {
$class = 'arrow_link';
if ($addclass) {
$class .= ' '.$addclass;
}
$linkparams = [
'class' => $class,
'href' => $url,
'title' => preg_replace('/<.*?>/', '', $text),
];
$linkparams += $addparams;
return html_writer::link($url, $htmltext . $arrow, $linkparams);
}
return $htmltext.$arrow;
}
/**
* Return the left arrow with text ('previous'), and optionally embedded in a link.
*
* @param string $text HTML/plain text label (set to blank only for breadcrumb separator cases).
* @param string $url An optional link to use in a surrounding HTML anchor.
* @param bool $accesshide True if text should be hidden (for screen readers only).
* @param string $addclass Additional class names for the link, or the arrow character.
* @return string HTML string.
*/
function link_arrow_left($text, $url='', $accesshide=false, $addclass='', $addparams = []) {
global $OUTPUT; // TODO: move to utput renderer.
$arrowclass = 'arrow ';
if (! $url) {
$arrowclass .= $addclass;
}
$arrow = ' ';
$htmltext = '';
if ($text) {
$htmltext = ' '.$text.'';
if ($accesshide) {
$htmltext = get_accesshide($htmltext);
}
}
if ($url) {
$class = 'arrow_link';
if ($addclass) {
$class .= ' '.$addclass;
}
$linkparams = [
'class' => $class,
'href' => $url,
'title' => preg_replace('/<.*?>/', '', $text),
];
$linkparams += $addparams;
return html_writer::link($url, $arrow . $htmltext, $linkparams);
}
return $arrow.$htmltext;
}
/**
* Return a HTML element with the class "accesshide", for accessibility.
*
* Please use cautiously - where possible, text should be visible!
*
* @param string $text Plain text.
* @param string $elem Lowercase element name, default "span".
* @param string $class Additional classes for the element.
* @param string $attrs Additional attributes string in the form, "name='value' name2='value2'"
* @return string HTML string.
*/
function get_accesshide($text, $elem='span', $class='', $attrs='') {
return "<$elem class=\"accesshide $class\" $attrs>$text$elem>";
}
/**
* Return the breadcrumb trail navigation separator.
*
* @return string HTML string.
*/
function get_separator() {
// Accessibility: the 'hidden' slash is preferred for screen readers.
return ' '.link_arrow_right($text='/', $url='', $accesshide=true, 'sep').' ';
}
/**
* Print (or return) a collapsible region, that has a caption that can be clicked to expand or collapse the region.
*
* If JavaScript is off, then the region will always be expanded.
*
* @param string $contents the contents of the box.
* @param string $classes class names added to the div that is output.
* @param string $id id added to the div that is output. Must not be blank.
* @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract.
* @param string $userpref the name of the user preference that stores the user's preferred default state.
* (May be blank if you do not wish the state to be persisted.
* @param boolean $default Initial collapsed state to use if the user_preference it not set.
* @param boolean $return if true, return the HTML as a string, rather than printing it.
* @return string|void If $return is false, returns nothing, otherwise returns a string of HTML.
*/
function print_collapsible_region($contents, $classes, $id, $caption, $userpref = '', $default = false, $return = false) {
$output = print_collapsible_region_start($classes, $id, $caption, $userpref, $default, true);
$output .= $contents;
$output .= print_collapsible_region_end(true);
if ($return) {
return $output;
} else {
echo $output;
}
}
/**
* Print (or return) the start of a collapsible region
*
* The collapsibleregion has a caption that can be clicked to expand or collapse the region. If JavaScript is off, then the region
* will always be expanded.
*
* @param string $classes class names added to the div that is output.
* @param string $id id added to the div that is output. Must not be blank.
* @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract.
* @param string $userpref the name of the user preference that stores the user's preferred default state.
* (May be blank if you do not wish the state to be persisted.
* @param boolean $default Initial collapsed state to use if the user_preference it not set.
* @param boolean $return if true, return the HTML as a string, rather than printing it.
* @param string $extracontent the extra content will show next to caption, eg.Help icon.
* @return string|void if $return is false, returns nothing, otherwise returns a string of HTML.
*/
function print_collapsible_region_start($classes, $id, $caption, $userpref = '', $default = false, $return = false,
$extracontent = null) {
global $PAGE;
// Work out the initial state.
if (!empty($userpref) and is_string($userpref)) {
$collapsed = get_user_preferences($userpref, $default);
} else {
$collapsed = $default;
$userpref = false;
}
if ($collapsed) {
$classes .= ' collapsed';
}
$output = '';
$output .= 'print_location_comment(__FILE__, __LINE__);
*
* @param string $file
* @param integer $line
* @param boolean $return Whether to return or print the comment
* @return string|void Void unless true given as third parameter
*/
function print_location_comment($file, $line, $return = false) {
if ($return) {
return "\n";
} else {
echo "\n";
}
}
/**
* Returns true if the user is using a right-to-left language.
*
* @return boolean true if the current language is right-to-left (Hebrew, Arabic etc)
*/
function right_to_left() {
return (get_string('thisdirection', 'langconfig') === 'rtl');
}
/**
* Returns swapped left<=> right if in RTL environment.
*
* Part of RTL Moodles support.
*
* @param string $align align to check
* @return string
*/
function fix_align_rtl($align) {
if (!right_to_left()) {
return $align;
}
if ($align == 'left') {
return 'right';
}
if ($align == 'right') {
return 'left';
}
return $align;
}
/**
* Returns true if the page is displayed in a popup window.
*
* Gets the information from the URL parameter inpopup.
*
* @todo Use a central function to create the popup calls all over Moodle and
* In the moment only works with resources and probably questions.
*
* @return boolean
*/
function is_in_popup() {
$inpopup = optional_param('inpopup', '', PARAM_BOOL);
return ($inpopup);
}
/**
* Returns a localized sentence in the current language summarizing the current password policy
*
* @todo this should be handled by a function/method in the language pack library once we have a support for it
* @uses $CFG
* @return string
*/
function print_password_policy() {
global $CFG;
$message = '';
if (!empty($CFG->passwordpolicy)) {
$messages = array();
if (!empty($CFG->minpasswordlength)) {
$messages[] = get_string('informminpasswordlength', 'auth', $CFG->minpasswordlength);
}
if (!empty($CFG->minpassworddigits)) {
$messages[] = get_string('informminpassworddigits', 'auth', $CFG->minpassworddigits);
}
if (!empty($CFG->minpasswordlower)) {
$messages[] = get_string('informminpasswordlower', 'auth', $CFG->minpasswordlower);
}
if (!empty($CFG->minpasswordupper)) {
$messages[] = get_string('informminpasswordupper', 'auth', $CFG->minpasswordupper);
}
if (!empty($CFG->minpasswordnonalphanum)) {
$messages[] = get_string('informminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum);
}
// Fire any additional password policy functions from plugins.
// Callbacks must return an array of message strings.
$pluginsfunction = get_plugins_with_function('print_password_policy');
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $pluginfunction) {
$messages = array_merge($messages, $pluginfunction());
}
}
$messages = join(', ', $messages); // This is ugly but we do not have anything better yet...
// Check if messages is empty before outputting any text.
if ($messages != '') {
$message = get_string('informpasswordpolicy', 'auth', $messages);
}
}
return $message;
}
/**
* Get the value of a help string fully prepared for display in the current language.
*
* @param string $identifier The identifier of the string to search for.
* @param string $component The module the string is associated with.
* @param boolean $ajax Whether this help is called from an AJAX script.
* This is used to influence text formatting and determines
* which format to output the doclink in.
* @param string|object|array $a An object, string or number that can be used
* within translation strings
* @return stdClass An object containing:
* - heading: Any heading that there may be for this help string.
* - text: The wiki-formatted help string.
* - doclink: An object containing a link, the linktext, and any additional
* CSS classes to apply to that link. Only present if $ajax = false.
* - completedoclink: A text representation of the doclink. Only present if $ajax = true.
*/
function get_formatted_help_string($identifier, $component, $ajax = false, $a = null) {
global $CFG, $OUTPUT;
$sm = get_string_manager();
// Do not rebuild caches here!
// Devs need to learn to purge all caches after any change or disable $CFG->langstringcache.
$data = new stdClass();
if ($sm->string_exists($identifier, $component)) {
$data->heading = format_string(get_string($identifier, $component));
} else {
// Gracefully fall back to an empty string.
$data->heading = '';
}
if ($sm->string_exists($identifier . '_help', $component)) {
$options = new stdClass();
$options->trusted = false;
$options->noclean = false;
$options->filter = false;
$options->para = true;
$options->newlines = false;
$options->overflowdiv = !$ajax;
// Should be simple wiki only MDL-21695.
$data->text = format_text(get_string($identifier.'_help', $component, $a), FORMAT_MARKDOWN, $options);
$helplink = $identifier . '_link';
if ($sm->string_exists($helplink, $component)) { // Link to further info in Moodle docs.
$link = get_string($helplink, $component);
$linktext = get_string('morehelp');
$data->doclink = new stdClass();
$url = new moodle_url(get_docs_url($link));
if ($ajax) {
$data->doclink->link = $url->out();
$data->doclink->linktext = $linktext;
$data->doclink->class = ($CFG->doctonewwindow) ? 'helplinkpopup' : '';
} else {
$data->completedoclink = html_writer::tag('div', $OUTPUT->doc_link($link, $linktext),
array('class' => 'helpdoclink'));
}
}
} else {
$data->text = html_writer::tag('p',
html_writer::tag('strong', 'TODO') . ": missing help string [{$identifier}_help, {$component}]");
}
return $data;
}