Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/lib/ -> moodlelib.php (source)
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * moodlelib.php - Moodle main library
 *
 * Main library file of miscellaneous general-purpose Moodle functions.
 * Other main libraries:
 *  - weblib.php      - functions that produce web output
 *  - datalib.php     - functions that access the database
 *
 * @package    core
 * @subpackage lib
 * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

defined('MOODLE_INTERNAL') || die();

// CONSTANTS (Encased in phpdoc proper comments).

// Date and time constants.
/**
 * Time constant - the number of seconds in a year
 */
define('YEARSECS', 31536000);

/**
 * Time constant - the number of seconds in a week
 */
define('WEEKSECS', 604800);

/**
 * Time constant - the number of seconds in a day
 */
define('DAYSECS', 86400);

/**
 * Time constant - the number of seconds in an hour
 */
define('HOURSECS', 3600);

/**
 * Time constant - the number of seconds in a minute
 */
define('MINSECS', 60);

/**
 * Time constant - the number of minutes in a day
 */
define('DAYMINS', 1440);

/**
 * Time constant - the number of minutes in an hour
 */
define('HOURMINS', 60);

// Parameter constants - every call to optional_param(), required_param()
// or clean_param() should have a specified type of parameter.

/**
 * PARAM_ALPHA - contains only English ascii letters [a-zA-Z].
 */
define('PARAM_ALPHA',    'alpha');

/**
 * PARAM_ALPHAEXT the same contents as PARAM_ALPHA (English ascii letters [a-zA-Z]) plus the chars in quotes: "_-" allowed
 * NOTE: originally this allowed "/" too, please use PARAM_SAFEPATH if "/" needed
 */
define('PARAM_ALPHAEXT', 'alphaext');

/**
 * PARAM_ALPHANUM - expected numbers 0-9 and English ascii letters [a-zA-Z] only.
 */
define('PARAM_ALPHANUM', 'alphanum');

/**
 * PARAM_ALPHANUMEXT - expected numbers 0-9, letters (English ascii letters [a-zA-Z]) and _- only.
 */
define('PARAM_ALPHANUMEXT', 'alphanumext');

/**
 * PARAM_AUTH - actually checks to make sure the string is a valid auth plugin
 */
define('PARAM_AUTH',  'auth');

/**
 * PARAM_BASE64 - Base 64 encoded format
 */
define('PARAM_BASE64',   'base64');

/**
 * PARAM_BOOL - converts input into 0 or 1, use for switches in forms and urls.
 */
define('PARAM_BOOL',     'bool');

/**
 * PARAM_CAPABILITY - A capability name, like 'moodle/role:manage'. Actually
 * checked against the list of capabilities in the database.
 */
define('PARAM_CAPABILITY',   'capability');

/**
 * PARAM_CLEANHTML - cleans submitted HTML code. Note that you almost never want
 * to use this. The normal mode of operation is to use PARAM_RAW when receiving
 * the input (required/optional_param or formslib) and then sanitise the HTML
 * using format_text on output. This is for the rare cases when you want to
 * sanitise the HTML on input. This cleaning may also fix xhtml strictness.
 */
define('PARAM_CLEANHTML', 'cleanhtml');

/**
 * PARAM_EMAIL - an email address following the RFC
 */
define('PARAM_EMAIL',   'email');

/**
 * PARAM_FILE - safe file name, all dangerous chars are stripped, protects against XSS, SQL injections and directory traversals
 */
define('PARAM_FILE',   'file');

/**
 * PARAM_FLOAT - a real/floating point number.
 *
 * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
 * It does not work for languages that use , as a decimal separator.
 * Use PARAM_LOCALISEDFLOAT instead.
 */
define('PARAM_FLOAT',  'float');

/**
 * PARAM_LOCALISEDFLOAT - a localised real/floating point number.
 * This is preferred over PARAM_FLOAT for numbers typed in by the user.
 * Cleans localised numbers to computer readable numbers; false for invalid numbers.
 */
define('PARAM_LOCALISEDFLOAT',  'localisedfloat');

/**
 * PARAM_HOST - expected fully qualified domain name (FQDN) or an IPv4 dotted quad (IP address)
 */
define('PARAM_HOST',     'host');

/**
 * PARAM_INT - integers only, use when expecting only numbers.
 */
define('PARAM_INT',      'int');

/**
 * PARAM_LANG - checks to see if the string is a valid installed language in the current site.
 */
define('PARAM_LANG',  'lang');

/**
 * PARAM_LOCALURL - expected properly formatted URL as well as one that refers to the local server itself. (NOT orthogonal to the
 * others! Implies PARAM_URL!)
 */
define('PARAM_LOCALURL', 'localurl');

/**
 * PARAM_NOTAGS - all html tags are stripped from the text. Do not abuse this type.
 */
define('PARAM_NOTAGS',   'notags');

/**
 * PARAM_PATH - safe relative path name, all dangerous chars are stripped, protects against XSS, SQL injections and directory
 * traversals note: the leading slash is not removed, window drive letter is not allowed
 */
define('PARAM_PATH',     'path');

/**
 * PARAM_PEM - Privacy Enhanced Mail format
 */
define('PARAM_PEM',      'pem');

/**
 * PARAM_PERMISSION - A permission, one of CAP_INHERIT, CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT.
 */
define('PARAM_PERMISSION',   'permission');

/**
 * PARAM_RAW specifies a parameter that is not cleaned/processed in any way except the discarding of the invalid utf-8 characters
 */
define('PARAM_RAW', 'raw');

/**
 * PARAM_RAW_TRIMMED like PARAM_RAW but leading and trailing whitespace is stripped.
 */
define('PARAM_RAW_TRIMMED', 'raw_trimmed');

/**
 * PARAM_SAFEDIR - safe directory name, suitable for include() and require()
 */
define('PARAM_SAFEDIR',  'safedir');

/**
 * PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
 * and other references to Moodle code files.
 *
 * This is NOT intended to be used for absolute paths or any user uploaded files.
 */
define('PARAM_SAFEPATH',  'safepath');

/**
 * PARAM_SEQUENCE - expects a sequence of numbers like 8 to 1,5,6,4,6,8,9.  Numbers and comma only.
 */
define('PARAM_SEQUENCE',  'sequence');

/**
 * PARAM_TAG - one tag (interests, blogs, etc.) - mostly international characters and space, <> not supported
 */
define('PARAM_TAG',   'tag');

/**
 * PARAM_TAGLIST - list of tags separated by commas (interests, blogs, etc.)
 */
define('PARAM_TAGLIST',   'taglist');

/**
 * PARAM_TEXT - general plain text compatible with multilang filter, no other html tags. Please note '<', or '>' are allowed here.
 */
define('PARAM_TEXT',  'text');

/**
 * PARAM_THEME - Checks to see if the string is a valid theme name in the current site
 */
define('PARAM_THEME',  'theme');

/**
 * PARAM_URL - expected properly formatted URL. Please note that domain part is required, http://localhost/ is not accepted but
 * http://localhost.localdomain/ is ok.
 */
define('PARAM_URL',      'url');

/**
 * PARAM_USERNAME - Clean username to only contains allowed characters. This is to be used ONLY when manually creating user
 * accounts, do NOT use when syncing with external systems!!
 */
define('PARAM_USERNAME',    'username');

/**
 * PARAM_STRINGID - used to check if the given string is valid string identifier for get_string()
 */
define('PARAM_STRINGID',    'stringid');

// DEPRECATED PARAM TYPES OR ALIASES - DO NOT USE FOR NEW CODE.
/**
 * PARAM_CLEAN - obsoleted, please use a more specific type of parameter.
 * It was one of the first types, that is why it is abused so much ;-)
 * @deprecated since 2.0
 */
define('PARAM_CLEAN',    'clean');

/**
 * PARAM_INTEGER - deprecated alias for PARAM_INT
 * @deprecated since 2.0
 */
define('PARAM_INTEGER',  'int');

/**
 * PARAM_NUMBER - deprecated alias of PARAM_FLOAT
 * @deprecated since 2.0
 */
define('PARAM_NUMBER',  'float');

/**
 * PARAM_ACTION - deprecated alias for PARAM_ALPHANUMEXT, use for various actions in forms and urls
 * NOTE: originally alias for PARAM_APLHA
 * @deprecated since 2.0
 */
define('PARAM_ACTION',   'alphanumext');

/**
 * PARAM_FORMAT - deprecated alias for PARAM_ALPHANUMEXT, use for names of plugins, formats, etc.
 * NOTE: originally alias for PARAM_APLHA
 * @deprecated since 2.0
 */
define('PARAM_FORMAT',   'alphanumext');

/**
 * PARAM_MULTILANG - deprecated alias of PARAM_TEXT.
 * @deprecated since 2.0
 */
define('PARAM_MULTILANG',  'text');

/**
 * PARAM_TIMEZONE - expected timezone. Timezone can be int +-(0-13) or float +-(0.5-12.5) or
 * string separated by '/' and can have '-' &/ '_' (eg. America/North_Dakota/New_Salem
 * America/Port-au-Prince)
 */
define('PARAM_TIMEZONE', 'timezone');

/**
 * PARAM_CLEANFILE - deprecated alias of PARAM_FILE; originally was removing regional chars too
 */
define('PARAM_CLEANFILE', 'file');

/**
 * PARAM_COMPONENT is used for full component names (aka frankenstyle) such as 'mod_forum', 'core_rating', 'auth_ldap'.
 * Short legacy subsystem names and module names are accepted too ex: 'forum', 'rating', 'user'.
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 * NOTE: numbers and underscores are strongly discouraged in plugin names!
 */
define('PARAM_COMPONENT', 'component');

/**
 * PARAM_AREA is a name of area used when addressing files, comments, ratings, etc.
 * It is usually used together with context id and component.
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 */
define('PARAM_AREA', 'area');

/**
 * PARAM_PLUGIN is used for plugin names such as 'forum', 'glossary', 'ldap', 'paypal', 'completionstatus'.
 * Only lowercase ascii letters, numbers and underscores are allowed, it has to start with a letter.
 * NOTE: numbers and underscores are strongly discouraged in plugin names! Underscores are forbidden in module names.
 */
define('PARAM_PLUGIN', 'plugin');


// Web Services.

/**
 * VALUE_REQUIRED - if the parameter is not supplied, there is an error
 */
define('VALUE_REQUIRED', 1);

/**
 * VALUE_OPTIONAL - if the parameter is not supplied, then the param has no value
 */
define('VALUE_OPTIONAL', 2);

/**
 * VALUE_DEFAULT - if the parameter is not supplied, then the default value is used
 */
define('VALUE_DEFAULT', 0);

/**
 * NULL_NOT_ALLOWED - the parameter can not be set to null in the database
 */
define('NULL_NOT_ALLOWED', false);

/**
 * NULL_ALLOWED - the parameter can be set to null in the database
 */
define('NULL_ALLOWED', true);

// Page types.

/**
 * PAGE_COURSE_VIEW is a definition of a page type. For more information on the page class see moodle/lib/pagelib.php.
 */
define('PAGE_COURSE_VIEW', 'course-view');

/** Get remote addr constant */
define('GETREMOTEADDR_SKIP_HTTP_CLIENT_IP', '1');
/** Get remote addr constant */
define('GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR', '2');
/**
 * GETREMOTEADDR_SKIP_DEFAULT defines the default behavior remote IP address validation.
 */
define('GETREMOTEADDR_SKIP_DEFAULT', GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP);

// Blog access level constant declaration.
define ('BLOG_USER_LEVEL', 1);
define ('BLOG_GROUP_LEVEL', 2);
define ('BLOG_COURSE_LEVEL', 3);
define ('BLOG_SITE_LEVEL', 4);
define ('BLOG_GLOBAL_LEVEL', 5);


// Tag constants.
/**
 * To prevent problems with multibytes strings,Flag updating in nav not working on the review page. this should not exceed the
 * length of "varchar(255) / 3 (bytes / utf-8 character) = 85".
 * TODO: this is not correct, varchar(255) are 255 unicode chars ;-)
 *
 * @todo define(TAG_MAX_LENGTH) this is not correct, varchar(255) are 255 unicode chars ;-)
 */
define('TAG_MAX_LENGTH', 50);

// Password policy constants.
define ('PASSWORD_LOWER', 'abcdefghijklmnopqrstuvwxyz');
define ('PASSWORD_UPPER', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
define ('PASSWORD_DIGITS', '0123456789');
define ('PASSWORD_NONALPHANUM', '.,;:!?_-+/*@#&$');

> /** // Feature constants. > * Required password pepper entropy. // Used for plugin_supports() to report features that are, or are not, supported by a module. > */ > define ('PEPPER_ENTROPY', 112); /** True if module can provide a grade */ >
define('FEATURE_GRADE_HAS_GRADE', 'grade_has_grade'); /** True if module supports outcomes */ define('FEATURE_GRADE_OUTCOMES', 'outcomes'); /** True if module supports advanced grading methods */ define('FEATURE_ADVANCED_GRADING', 'grade_advanced_grading'); /** True if module controls the grade visibility over the gradebook */ define('FEATURE_CONTROLS_GRADE_VISIBILITY', 'controlsgradevisbility'); /** True if module supports plagiarism plugins */ define('FEATURE_PLAGIARISM', 'plagiarism'); /** True if module has code to track whether somebody viewed it */ define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views'); /** True if module has custom completion rules */ define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules'); /** True if module has no 'view' page (like label) */ define('FEATURE_NO_VIEW_LINK', 'viewlink'); /** True (which is default) if the module wants support for setting the ID number for grade calculation purposes. */ define('FEATURE_IDNUMBER', 'idnumber'); /** True if module supports groups */ define('FEATURE_GROUPS', 'groups'); /** True if module supports groupings */ define('FEATURE_GROUPINGS', 'groupings'); /** * True if module supports groupmembersonly (which no longer exists) * @deprecated Since Moodle 2.8 */ define('FEATURE_GROUPMEMBERSONLY', 'groupmembersonly'); /** Type of module */ define('FEATURE_MOD_ARCHETYPE', 'mod_archetype'); /** True if module supports intro editor */ define('FEATURE_MOD_INTRO', 'mod_intro'); /** True if module has default completion */ define('FEATURE_MODEDIT_DEFAULT_COMPLETION', 'modedit_default_completion'); define('FEATURE_COMMENT', 'comment'); define('FEATURE_RATE', 'rate'); /** True if module supports backup/restore of moodle2 format */ define('FEATURE_BACKUP_MOODLE2', 'backup_moodle2'); /** True if module can show description on course main page */ define('FEATURE_SHOW_DESCRIPTION', 'showdescription'); /** True if module uses the question bank */ define('FEATURE_USES_QUESTIONS', 'usesquestions'); /** * Maximum filename char size */ define('MAX_FILENAME_SIZE', 100); /** Unspecified module archetype */ define('MOD_ARCHETYPE_OTHER', 0); /** Resource-like type module */ define('MOD_ARCHETYPE_RESOURCE', 1); /** Assignment module archetype */ define('MOD_ARCHETYPE_ASSIGNMENT', 2); /** System (not user-addable) module archetype */ define('MOD_ARCHETYPE_SYSTEM', 3);
> /** Type of module */ /** > define('FEATURE_MOD_PURPOSE', 'mod_purpose'); * Security token used for allowing access > /** Module purpose administration */ * from external application such as web services. > define('MOD_PURPOSE_ADMINISTRATION', 'administration'); * Scripts do not use any session, performance is relatively > /** Module purpose assessment */ * low because we need to load access info in each request. > define('MOD_PURPOSE_ASSESSMENT', 'assessment'); * Scripts are executed in parallel. > /** Module purpose communication */ */ > define('MOD_PURPOSE_COLLABORATION', 'collaboration'); define('EXTERNAL_TOKEN_PERMANENT', 0); > /** Module purpose communication */ > define('MOD_PURPOSE_COMMUNICATION', 'communication'); /** > /** Module purpose content */ * Security token used for allowing access > define('MOD_PURPOSE_CONTENT', 'content'); * of embedded applications, the code is executed in the > /** Module purpose interface */ * active user session. Token is invalidated after user logs out. > define('MOD_PURPOSE_INTERFACE', 'interface'); * Scripts are executed serially - normal session locking is used. > /** Module purpose other */ */ > define('MOD_PURPOSE_OTHER', 'other'); define('EXTERNAL_TOKEN_EMBEDDED', 1); >
/** * The home page should be the site home */ define('HOMEPAGE_SITE', 0); /** * The home page should be the users my page */ define('HOMEPAGE_MY', 1); /** * The home page can be chosen by the user */ define('HOMEPAGE_USER', 2);
> /** > * The home page should be the users my courses page /** > */ * URL of the Moodle sites registration portal. > define('HOMEPAGE_MYCOURSES', 3);
*/ defined('HUB_MOODLEORGHUBURL') || define('HUB_MOODLEORGHUBURL', 'https://stats.moodle.org'); /** * URL of the statistic server public key. */ defined('HUB_STATSPUBLICKEY') || define('HUB_STATSPUBLICKEY', 'https://moodle.org/static/statspubkey.pem'); /** * Moodle mobile app service name */ define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app'); /** * Indicates the user has the capabilities required to ignore activity and course file size restrictions */ define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1); /** * Course display settings: display all sections on one page. */ define('COURSE_DISPLAY_SINGLEPAGE', 0); /** * Course display settings: split pages into a page per section. */ define('COURSE_DISPLAY_MULTIPAGE', 1); /** * Authentication constant: String used in password field when password is not stored. */ define('AUTH_PASSWORD_NOT_CACHED', 'not cached'); /** * Email from header to never include via information. */ define('EMAIL_VIA_NEVER', 0); /** * Email from header to always include via information. */ define('EMAIL_VIA_ALWAYS', 1); /** * Email from header to only include via information if the address is no-reply. */ define('EMAIL_VIA_NO_REPLY_ONLY', 2);
> /** // PARAMETER HANDLING. > * Contact site support form/link disabled. > */ /** > define('CONTACT_SUPPORT_DISABLED', 0); * Returns a particular value for the named variable, taken from > * POST or GET. If the parameter doesn't exist then an error is > /** * thrown because we require this variable. > * Contact site support form/link only available to authenticated users. * > */ * This function should be used to initialise all required values > define('CONTACT_SUPPORT_AUTHENTICATED', 1); * in a script that are based on parameters. Usually it will be > * used like this: > /** * $id = required_param('id', PARAM_INT); > * Contact site support form/link available to anyone visiting the site. * > */ * Please note the $type parameter is now required and the value can not be array. > define('CONTACT_SUPPORT_ANYONE', 2); * > * @param string $parname the name of the page parameter we want > /** * @param string $type expected type of parameter > * Maximum number of characters for password. * @return mixed > */ * @throws coding_exception > define('MAX_PASSWORD_CHARACTERS', 128); */ >
function required_param($parname, $type) { if (func_num_args() != 2 or empty($parname) or empty($type)) { throw new coding_exception('required_param() requires $parname and $type to be specified (parameter: '.$parname.')'); } // POST has precedence. if (isset($_POST[$parname])) { $param = $_POST[$parname]; } else if (isset($_GET[$parname])) { $param = $_GET[$parname]; } else {
< print_error('missingparam', '', '', $parname);
> throw new \moodle_exception('missingparam', '', '', $parname);
} if (is_array($param)) { debugging('Invalid array parameter detected in required_param(): '.$parname); // TODO: switch to fatal error in Moodle 2.3. return required_param_array($parname, $type); } return clean_param($param, $type); } /** * Returns a particular array value for the named variable, taken from * POST or GET. If the parameter doesn't exist then an error is * thrown because we require this variable. * * This function should be used to initialise all required values * in a script that are based on parameters. Usually it will be * used like this: * $ids = required_param_array('ids', PARAM_INT); * * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported * * @param string $parname the name of the page parameter we want * @param string $type expected type of parameter * @return array * @throws coding_exception */ function required_param_array($parname, $type) { if (func_num_args() != 2 or empty($parname) or empty($type)) { throw new coding_exception('required_param_array() requires $parname and $type to be specified (parameter: '.$parname.')'); } // POST has precedence. if (isset($_POST[$parname])) { $param = $_POST[$parname]; } else if (isset($_GET[$parname])) { $param = $_GET[$parname]; } else {
< print_error('missingparam', '', '', $parname);
> throw new \moodle_exception('missingparam', '', '', $parname);
} if (!is_array($param)) {
< print_error('missingparam', '', '', $parname);
> throw new \moodle_exception('missingparam', '', '', $parname);
} $result = array(); foreach ($param as $key => $value) { if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { debugging('Invalid key name in required_param_array() detected: '.$key.', parameter: '.$parname); continue; } $result[$key] = clean_param($value, $type); } return $result; } /** * Returns a particular value for the named variable, taken from * POST or GET, otherwise returning a given default. * * This function should be used to initialise all optional values * in a script that are based on parameters. Usually it will be * used like this: * $name = optional_param('name', 'Fred', PARAM_TEXT); * * Please note the $type parameter is now required and the value can not be array. * * @param string $parname the name of the page parameter we want * @param mixed $default the default value to return if nothing is found * @param string $type expected type of parameter * @return mixed * @throws coding_exception */ function optional_param($parname, $default, $type) { if (func_num_args() != 3 or empty($parname) or empty($type)) { throw new coding_exception('optional_param requires $parname, $default + $type to be specified (parameter: '.$parname.')'); } // POST has precedence. if (isset($_POST[$parname])) { $param = $_POST[$parname]; } else if (isset($_GET[$parname])) { $param = $_GET[$parname]; } else { return $default; } if (is_array($param)) { debugging('Invalid array parameter detected in required_param(): '.$parname); // TODO: switch to $default in Moodle 2.3. return optional_param_array($parname, $default, $type); } return clean_param($param, $type); } /** * Returns a particular array value for the named variable, taken from * POST or GET, otherwise returning a given default. * * This function should be used to initialise all optional values * in a script that are based on parameters. Usually it will be * used like this: * $ids = optional_param('id', array(), PARAM_INT); * * Note: arrays of arrays are not supported, only alphanumeric keys with _ and - are supported * * @param string $parname the name of the page parameter we want * @param mixed $default the default value to return if nothing is found * @param string $type expected type of parameter * @return array * @throws coding_exception */ function optional_param_array($parname, $default, $type) { if (func_num_args() != 3 or empty($parname) or empty($type)) { throw new coding_exception('optional_param_array requires $parname, $default + $type to be specified (parameter: '.$parname.')'); } // POST has precedence. if (isset($_POST[$parname])) { $param = $_POST[$parname]; } else if (isset($_GET[$parname])) { $param = $_GET[$parname]; } else { return $default; } if (!is_array($param)) { debugging('optional_param_array() expects array parameters only: '.$parname); return $default; } $result = array(); foreach ($param as $key => $value) { if (!preg_match('/^[a-z0-9_-]+$/i', $key)) { debugging('Invalid key name in optional_param_array() detected: '.$key.', parameter: '.$parname); continue; } $result[$key] = clean_param($value, $type); } return $result; } /** * Strict validation of parameter values, the values are only converted * to requested PHP type. Internally it is using clean_param, the values * before and after cleaning must be equal - otherwise * an invalid_parameter_exception is thrown. * Objects and classes are not accepted. * * @param mixed $param * @param string $type PARAM_ constant * @param bool $allownull are nulls valid value? * @param string $debuginfo optional debug information * @return mixed the $param value converted to PHP type * @throws invalid_parameter_exception if $param is not of given type */ function validate_param($param, $type, $allownull=NULL_NOT_ALLOWED, $debuginfo='') { if (is_null($param)) { if ($allownull == NULL_ALLOWED) { return null; } else { throw new invalid_parameter_exception($debuginfo); } } if (is_array($param) or is_object($param)) { throw new invalid_parameter_exception($debuginfo); } $cleaned = clean_param($param, $type); if ($type == PARAM_FLOAT) { // Do not detect precision loss here. if (is_float($param) or is_int($param)) { // These always fit. } else if (!is_numeric($param) or !preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', (string)$param)) { throw new invalid_parameter_exception($debuginfo); } } else if ((string)$param !== (string)$cleaned) { // Conversion to string is usually lossless. throw new invalid_parameter_exception($debuginfo); } return $cleaned; } /** * Makes sure array contains only the allowed types, this function does not validate array key names! * * <code> * $options = clean_param($options, PARAM_INT); * </code> * * @param array|null $param the variable array we are cleaning * @param string $type expected format of param after cleaning. * @param bool $recursive clean recursive arrays * @return array * @throws coding_exception */ function clean_param_array(?array $param, $type, $recursive = false) { // Convert null to empty array. $param = (array)$param; foreach ($param as $key => $value) { if (is_array($value)) { if ($recursive) { $param[$key] = clean_param_array($value, $type, true); } else { throw new coding_exception('clean_param_array can not process multidimensional arrays when $recursive is false.'); } } else { $param[$key] = clean_param($value, $type); } } return $param; } /** * Used by {@link optional_param()} and {@link required_param()} to * clean the variables and/or cast to specific types, based on * an options field. * <code> * $course->format = clean_param($course->format, PARAM_ALPHA); * $selectedgradeitem = clean_param($selectedgradeitem, PARAM_INT); * </code> * * @param mixed $param the variable we are cleaning * @param string $type expected format of param after cleaning. * @return mixed * @throws coding_exception */ function clean_param($param, $type) { global $CFG; if (is_array($param)) { throw new coding_exception('clean_param() can not process arrays, please use clean_param_array() instead.'); } else if (is_object($param)) { if (method_exists($param, '__toString')) { $param = $param->__toString(); } else { throw new coding_exception('clean_param() can not process objects, please use clean_param_array() instead.'); } } switch ($type) { case PARAM_RAW: // No cleaning at all. $param = fix_utf8($param); return $param; case PARAM_RAW_TRIMMED: // No cleaning, but strip leading and trailing whitespace.
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
return trim($param); case PARAM_CLEAN: // General HTML cleaning, try to use more specific type if possible this is deprecated! // Please use more specific type instead. if (is_numeric($param)) { return $param; } $param = fix_utf8($param); // Sweep for scripts, etc. return clean_text($param); case PARAM_CLEANHTML: // Clean html fragment.
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
// Sweep for scripts, etc. $param = clean_text($param, FORMAT_HTML); return trim($param); case PARAM_INT: // Convert to integer. return (int)$param; case PARAM_FLOAT: // Convert to float. return (float)$param; case PARAM_LOCALISEDFLOAT: // Convert to float. return unformat_float($param, true); case PARAM_ALPHA: // Remove everything not `a-z`.
< return preg_replace('/[^a-zA-Z]/i', '', $param);
> return preg_replace('/[^a-zA-Z]/i', '', (string)$param);
case PARAM_ALPHAEXT: // Remove everything not `a-zA-Z_-` (originally allowed "/" too).
< return preg_replace('/[^a-zA-Z_-]/i', '', $param);
> return preg_replace('/[^a-zA-Z_-]/i', '', (string)$param);
case PARAM_ALPHANUM: // Remove everything not `a-zA-Z0-9`.
< return preg_replace('/[^A-Za-z0-9]/i', '', $param);
> return preg_replace('/[^A-Za-z0-9]/i', '', (string)$param);
case PARAM_ALPHANUMEXT: // Remove everything not `a-zA-Z0-9_-`.
< return preg_replace('/[^A-Za-z0-9_-]/i', '', $param);
> return preg_replace('/[^A-Za-z0-9_-]/i', '', (string)$param);
case PARAM_SEQUENCE: // Remove everything not `0-9,`.
< return preg_replace('/[^0-9,]/i', '', $param);
> return preg_replace('/[^0-9,]/i', '', (string)$param);
case PARAM_BOOL: // Convert to 1 or 0.
< $tempstr = strtolower($param);
> $tempstr = strtolower((string)$param);
if ($tempstr === 'on' or $tempstr === 'yes' or $tempstr === 'true') { $param = 1; } else if ($tempstr === 'off' or $tempstr === 'no' or $tempstr === 'false') { $param = 0; } else { $param = empty($param) ? 0 : 1; } return $param; case PARAM_NOTAGS: // Strip all tags. $param = fix_utf8($param);
< return strip_tags($param);
> return strip_tags((string)$param);
case PARAM_TEXT: // Leave only tags needed for multilang. $param = fix_utf8($param); // If the multilang syntax is not correct we strip all tags because it would break xhtml strict which is required // for accessibility standards please note this cleaning does not strip unbalanced '>' for BC compatibility reasons. do {
< if (strpos($param, '</lang>') !== false) {
> if (strpos((string)$param, '</lang>') !== false) {
// Old and future mutilang syntax. $param = strip_tags($param, '<lang>'); if (!preg_match_all('/<.*>/suU', $param, $matches)) { break; } $open = false; foreach ($matches[0] as $match) { if ($match === '</lang>') { if ($open) { $open = false; continue; } else { break 2; } } if (!preg_match('/^<lang lang="[a-zA-Z0-9_-]+"\s*>$/u', $match)) { break 2; } else { $open = true; } } if ($open) { break; } return $param;
< } else if (strpos($param, '</span>') !== false) {
> } else if (strpos((string)$param, '</span>') !== false) {
// Current problematic multilang syntax. $param = strip_tags($param, '<span>'); if (!preg_match_all('/<.*>/suU', $param, $matches)) { break; } $open = false; foreach ($matches[0] as $match) { if ($match === '</span>') { if ($open) { $open = false; continue; } else { break 2; } } if (!preg_match('/^<span(\s+lang="[a-zA-Z0-9_-]+"|\s+class="multilang"){2}\s*>$/u', $match)) { break 2; } else { $open = true; } } if ($open) { break; } return $param; } } while (false); // Easy, just strip all tags, if we ever want to fix orphaned '&' we have to do that in format_string().
< return strip_tags($param);
> return strip_tags((string)$param);
case PARAM_COMPONENT: // We do not want any guessing here, either the name is correct or not // please note only normalised component names are accepted.
> $param = (string)$param;
if (!preg_match('/^[a-z][a-z0-9]*(_[a-z][a-z0-9_]*)?[a-z0-9]+$/', $param)) { return ''; } if (strpos($param, '__') !== false) { return ''; } if (strpos($param, 'mod_') === 0) { // Module names must not contain underscores because we need to differentiate them from invalid plugin types. if (substr_count($param, '_') != 1) { return ''; } } return $param; case PARAM_PLUGIN: case PARAM_AREA: // We do not want any guessing here, either the name is correct or not. if (!is_valid_plugin_name($param)) { return ''; } return $param; case PARAM_SAFEDIR: // Remove everything not a-zA-Z0-9_- .
< return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
> return preg_replace('/[^a-zA-Z0-9_-]/i', '', (string)$param);
case PARAM_SAFEPATH: // Remove everything not a-zA-Z0-9/_- .
< return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
> return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', (string)$param);
case PARAM_FILE: // Strip all suspicious characters from filename.
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
$param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param); if ($param === '.' || $param === '..') { $param = ''; } return $param; case PARAM_PATH: // Strip all suspicious characters from file path.
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
$param = str_replace('\\', '/', $param); // Explode the path and clean each element using the PARAM_FILE rules. $breadcrumb = explode('/', $param); foreach ($breadcrumb as $key => $crumb) { if ($crumb === '.' && $key === 0) { // Special condition to allow for relative current path such as ./currentdirfile.txt. } else { $crumb = clean_param($crumb, PARAM_FILE); } $breadcrumb[$key] = $crumb; } $param = implode('/', $breadcrumb); // Remove multiple current path (./././) and multiple slashes (///). $param = preg_replace('~//+~', '/', $param); $param = preg_replace('~/(\./)+~', '/', $param); return $param; case PARAM_HOST: // Allow FQDN or IPv4 dotted quad.
< $param = preg_replace('/[^\.\d\w-]/', '', $param );
> $param = preg_replace('/[^\.\d\w-]/', '', (string)$param );
// Match ipv4 dotted quad. if (preg_match('/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/', $param, $match)) { // Confirm values are ok. if ( $match[0] > 255 || $match[1] > 255 || $match[3] > 255 || $match[4] > 255 ) { // Hmmm, what kind of dotted quad is this? $param = ''; } } else if ( preg_match('/^[\w\d\.-]+$/', $param) // Dots, hyphens, numbers. && !preg_match('/^[\.-]/', $param) // No leading dots/hyphens. && !preg_match('/[\.-]$/', $param) // No trailing dots/hyphens. ) { // All is ok - $param is respected. } else { // All is not ok... $param=''; } return $param; case PARAM_URL: // Allow safe urls.
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
include_once($CFG->dirroot . '/lib/validateurlsyntax.php'); if (!empty($param) && validateUrlSyntax($param, 's?H?S?F?E-u-P-a?I?p?f?q?r?')) { // All is ok, param is respected. } else { // Not really ok. $param =''; } return $param; case PARAM_LOCALURL: // Allow http absolute, root relative and relative URLs within wwwroot. $param = clean_param($param, PARAM_URL); if (!empty($param)) { if ($param === $CFG->wwwroot) { // Exact match; } else if (preg_match(':^/:', $param)) { // Root-relative, ok! } else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) { // Absolute, and matches our wwwroot. } else { // Relative - let's make sure there are no tricks. if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) { // Looks ok. } else { $param = ''; } } } return $param; case PARAM_PEM:
< $param = trim($param);
> $param = trim((string)$param);
// PEM formatted strings may contain letters/numbers and the symbols: // forward slash: / // plus sign: + // equal sign: = // , surrounded by BEGIN and END CERTIFICATE prefix and suffixes. if (preg_match('/^-----BEGIN CERTIFICATE-----([\s\w\/\+=]+)-----END CERTIFICATE-----$/', trim($param), $matches)) { list($wholething, $body) = $matches; unset($wholething, $matches); $b64 = clean_param($body, PARAM_BASE64); if (!empty($b64)) { return "-----BEGIN CERTIFICATE-----\n$b64\n-----END CERTIFICATE-----\n"; } else { return ''; } } return ''; case PARAM_BASE64: if (!empty($param)) { // PEM formatted strings may contain letters/numbers and the symbols // forward slash: / // plus sign: + // equal sign: =. if (0 >= preg_match('/^([\s\w\/\+=]+)$/', trim($param))) { return ''; } $lines = preg_split('/[\s]+/', $param, -1, PREG_SPLIT_NO_EMPTY); // Each line of base64 encoded data must be 64 characters in length, except for the last line which may be less // than (or equal to) 64 characters long. for ($i=0, $j=count($lines); $i < $j; $i++) { if ($i + 1 == $j) { if (64 < strlen($lines[$i])) { return ''; } continue; } if (64 != strlen($lines[$i])) { return ''; } } return implode("\n", $lines); } else { return ''; } case PARAM_TAG:
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
// Please note it is not safe to use the tag name directly anywhere, // it must be processed with s(), urlencode() before embedding anywhere. // Remove some nasties. $param = preg_replace('~[[:cntrl:]]|[<>`]~u', '', $param); // Convert many whitespace chars into one. $param = preg_replace('/\s+/u', ' ', $param); $param = core_text::substr(trim($param), 0, TAG_MAX_LENGTH); return $param; case PARAM_TAGLIST:
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
$tags = explode(',', $param); $result = array(); foreach ($tags as $tag) { $res = clean_param($tag, PARAM_TAG); if ($res !== '') { $result[] = $res; } } if ($result) { return implode(',', $result); } else { return ''; } case PARAM_CAPABILITY: if (get_capability_info($param)) { return $param; } else { return ''; } case PARAM_PERMISSION: $param = (int)$param; if (in_array($param, array(CAP_INHERIT, CAP_ALLOW, CAP_PREVENT, CAP_PROHIBIT))) { return $param; } else { return CAP_INHERIT; } case PARAM_AUTH: $param = clean_param($param, PARAM_PLUGIN); if (empty($param)) { return ''; } else if (exists_auth_plugin($param)) { return $param; } else { return ''; } case PARAM_LANG: $param = clean_param($param, PARAM_SAFEDIR); if (get_string_manager()->translation_exists($param)) { return $param; } else { // Specified language is not installed or param malformed. return ''; } case PARAM_THEME: $param = clean_param($param, PARAM_PLUGIN); if (empty($param)) { return ''; } else if (file_exists("$CFG->dirroot/theme/$param/config.php")) { return $param; } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$param/config.php")) { return $param; } else { // Specified theme is not installed. return ''; } case PARAM_USERNAME:
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
$param = trim($param); // Convert uppercase to lowercase MDL-16919. $param = core_text::strtolower($param); if (empty($CFG->extendedusernamechars)) { $param = str_replace(" " , "", $param); // Regular expression, eliminate all chars EXCEPT: // alphanum, dash (-), underscore (_), at sign (@) and period (.) characters. $param = preg_replace('/[^-\.@_a-z0-9]/', '', $param); } return $param; case PARAM_EMAIL: $param = fix_utf8($param);
< if (validate_email($param)) {
> if (validate_email($param ?? '')) {
return $param; } else { return ''; } case PARAM_STRINGID:
< if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', $param)) {
> if (preg_match('|^[a-zA-Z][a-zA-Z0-9\.:/_-]*$|', (string)$param)) {
return $param; } else { return ''; } case PARAM_TIMEZONE: // Can be int, float(with .5 or .0) or string seperated by '/' and can have '-_'.
< $param = fix_utf8($param);
> $param = (string)fix_utf8($param);
$timezonepattern = '/^(([+-]?(0?[0-9](\.[5|0])?|1[0-3](\.0)?|1[0-2]\.5))|(99)|[[:alnum:]]+(\/?[[:alpha:]_-])+)$/'; if (preg_match($timezonepattern, $param)) { return $param; } else { return ''; } default: // Doh! throw error, switched parameters in optional_param or another serious problem.
< print_error("unknownparamtype", '', '', $type);
> throw new \moodle_exception("unknownparamtype", '', '', $type);
} } /** * Whether the PARAM_* type is compatible in RTL. * * Being compatible with RTL means that the data they contain can flow * from right-to-left or left-to-right without compromising the user experience. * * Take URLs for example, they are not RTL compatible as they should always * flow from the left to the right. This also applies to numbers, email addresses, * configuration snippets, base64 strings, etc... * * This function tries to best guess which parameters can contain localised strings. * * @param string $paramtype Constant PARAM_*. * @return bool */ function is_rtl_compatible($paramtype) { return $paramtype == PARAM_TEXT || $paramtype == PARAM_NOTAGS; } /** * Makes sure the data is using valid utf8, invalid characters are discarded. * * Note: this function is not intended for full objects with methods and private properties. * * @param mixed $value * @return mixed with proper utf-8 encoding */ function fix_utf8($value) { if (is_null($value) or $value === '') { return $value; } else if (is_string($value)) { if ((string)(int)$value === $value) { // Shortcut. return $value; }
< // No null bytes expected in our data, so let's remove it. < $value = str_replace("\0", '', $value);
> > // Remove null bytes or invalid Unicode sequences from value. > $value = str_replace(["\0", "\xef\xbf\xbe", "\xef\xbf\xbf"], '', $value);
// Note: this duplicates min_fix_utf8() intentionally. static $buggyiconv = null; if ($buggyiconv === null) { $buggyiconv = (!function_exists('iconv') or @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€'); } if ($buggyiconv) { if (function_exists('mb_convert_encoding')) { $subst = mb_substitute_character(); mb_substitute_character('none'); $result = mb_convert_encoding($value, 'utf-8', 'utf-8'); mb_substitute_character($subst); } else { // Warn admins on admin/index.php page. $result = $value; } } else { $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value); } return $result; } else if (is_array($value)) { foreach ($value as $k => $v) { $value[$k] = fix_utf8($v); } return $value; } else if (is_object($value)) { // Do not modify original. $value = clone($value); foreach ($value as $k => $v) { $value->$k = fix_utf8($v); } return $value; } else { // This is some other type, no utf-8 here. return $value; } } /** * Return true if given value is integer or string with integer value * * @param mixed $value String or Int * @return bool true if number, false if not */ function is_number($value) { if (is_int($value)) { return true; } else if (is_string($value)) { return ((string)(int)$value) === $value; } else { return false; } } /** * Returns host part from url. * * @param string $url full url * @return string host, null if not found */ function get_host_from_url($url) { preg_match('|^[a-z]+://([a-zA-Z0-9-.]+)|i', $url, $matches); if ($matches) { return $matches[1]; } return null; } /** * Tests whether anything was returned by text editor * * This function is useful for testing whether something you got back from * the HTML editor actually contains anything. Sometimes the HTML editor * appear to be empty, but actually you get back a <br> tag or something. * * @param string $string a string containing HTML. * @return boolean does the string contain any actual content - that is text, * images, objects, etc. */ function html_is_blank($string) {
< return trim(strip_tags($string, '<img><object><applet><input><select><textarea><hr>')) == '';
> return trim(strip_tags((string)$string, '<img><object><applet><input><select><textarea><hr>')) == '';
} /** * Set a key in global configuration * * Set a key/value pair in both this session's {@link $CFG} global variable * and in the 'config' database table for future sessions. * * Can also be used to update keys for plugin-scoped configs in config_plugin table. * In that case it doesn't affect $CFG. * * A NULL value will delete the entry. * * NOTE: this function is called from lib/db/upgrade.php * * @param string $name the key to set * @param string $value the value to set (without magic quotes) * @param string $plugin (optional) the plugin scope, default null * @return bool true or exception */ function set_config($name, $value, $plugin=null) { global $CFG, $DB;
< if (empty($plugin)) { < if (!array_key_exists($name, $CFG->config_php_settings)) { < // So it's defined for this invocation at least. < if (is_null($value)) { < unset($CFG->$name); < } else { < // Settings from db are always strings. < $CFG->$name = (string)$value; < } < } < < if ($DB->get_field('config', 'name', array('name' => $name))) {
> // Redirect to appropriate handler when value is null.
if ($value === null) {
< $DB->delete_records('config', array('name' => $name)); < } else { < $DB->set_field('config', 'value', $value, array('name' => $name));
> return unset_config($name, $plugin);
}
< } else { < if ($value !== null) {
> > // Set variables determining conditions and where to store the new config. > // Plugin config goes to {config_plugins}, core config goes to {config}. > $iscore = empty($plugin); > if ($iscore) { > // If it's for core config. > $table = 'config'; > $conditions = ['name' => $name]; > $invalidatecachekey = 'core'; > } else { > // If it's a plugin. > $table = 'config_plugins'; > $conditions = ['name' => $name, 'plugin' => $plugin]; > $invalidatecachekey = $plugin; > } > > // DB handling - checks for existing config, updating or inserting only if necessary. > $invalidatecache = true; > $inserted = false; > $record = $DB->get_record($table, $conditions, 'id, value'); > if ($record === false) { > // Inserts a new config record.
$config = new stdClass(); $config->name = $name; $config->value = $value;
< $DB->insert_record('config', $config, false);
> if (!$iscore) { > $config->plugin = $plugin; > } > $inserted = $DB->insert_record($table, $config, false); > } else if ($invalidatecache = ($record->value !== $value)) { > // Record exists - Check and only set new value if it has changed. > $DB->set_field($table, 'value', $value, ['id' => $record->id]); > } > > if ($iscore && !isset($CFG->config_php_settings[$name])) { > // So it's defined for this invocation at least. > // Settings from db are always strings. > $CFG->$name = (string) $value;
}
>
// When setting config during a Behat test (in the CLI script, not in the web browser // requests), remember which ones are set so that we can clear them later.
< if (defined('BEHAT_TEST')) { < if (!property_exists($CFG, 'behat_cli_added_config')) { < $CFG->behat_cli_added_config = []; < }
> if ($iscore && $inserted && defined('BEHAT_TEST')) {
$CFG->behat_cli_added_config[$name] = true; }
< } < if ($name === 'siteidentifier') {
> > // Update siteidentifier cache, if required. > if ($iscore && $name === 'siteidentifier') {
cache_helper::update_site_identifier($value); }
< cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); < } else { < // Plugin scope. < if ($id = $DB->get_field('config_plugins', 'id', array('name' => $name, 'plugin' => $plugin))) { < if ($value===null) { < $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); < } else { < $DB->set_field('config_plugins', 'value', $value, array('id' => $id)); < } < } else { < if ($value !== null) { < $config = new stdClass(); < $config->plugin = $plugin; < $config->name = $name; < $config->value = $value; < $DB->insert_record('config_plugins', $config, false); < } < } < cache_helper::invalidate_by_definition('core', 'config', array(), $plugin);
> > // Invalidate cache, if required. > if ($invalidatecache) { > cache_helper::invalidate_by_definition('core', 'config', [], $invalidatecachekey);
} return true; } /** * Get configuration values from the global config table * or the config_plugins table. * * If called with one parameter, it will load all the config * variables for one plugin, and return them as an object. * * If called with 2 parameters it will return a string single * value or false if the value is not found. * * NOTE: this function is called from lib/db/upgrade.php *
< * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so < * that we need only fetch it once per request.
* @param string $plugin full component name * @param string $name default null * @return mixed hash-like object or single value, return false no config found * @throws dml_exception */ function get_config($plugin, $name = null) { global $CFG, $DB;
< static $siteidentifier = null; <
if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) { $forced =& $CFG->config_php_settings; $iscore = true; $plugin = 'core'; } else { if (array_key_exists($plugin, $CFG->forced_plugin_settings)) { $forced =& $CFG->forced_plugin_settings[$plugin]; } else { $forced = array(); } $iscore = false; }
< if ($siteidentifier === null) {
> if (!isset($CFG->siteidentifier)) {
try {
< // This may fail during installation. < // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to < // install the database. < $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
> // This may throw an exception during installation, which is how we detect the > // need to install the database. For more details see {@see initialise_cfg()}. > $CFG->siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
} catch (dml_exception $ex) { // Set siteidentifier to false. We don't want to trip this continually. $siteidentifier = false; throw $ex; } } if (!empty($name)) { if (array_key_exists($name, $forced)) { return (string)$forced[$name]; } else if ($name === 'siteidentifier' && $plugin == 'core') {
< return $siteidentifier;
> return $CFG->siteidentifier;
} } $cache = cache::make('core', 'config'); $result = $cache->get($plugin); if ($result === false) { // The user is after a recordset. if (!$iscore) { $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value'); } else { // This part is not really used any more, but anyway... $result = $DB->get_records_menu('config', array(), '', 'name,value');; } $cache->set($plugin, $result); } if (!empty($name)) { if (array_key_exists($name, $result)) { return $result[$name]; } return false; } if ($plugin === 'core') {
< $result['siteidentifier'] = $siteidentifier;
> $result['siteidentifier'] = $CFG->siteidentifier;
} foreach ($forced as $key => $value) { if (is_null($value) or is_array($value) or is_object($value)) { // We do not want any extra mess here, just real settings that could be saved in db. unset($result[$key]); } else { // Convert to string as if it went through the DB. $result[$key] = (string)$value; } } return (object)$result; } /** * Removes a key from global configuration. * * NOTE: this function is called from lib/db/upgrade.php * * @param string $name the key to set * @param string $plugin (optional) the plugin scope * @return boolean whether the operation succeeded. */ function unset_config($name, $plugin=null) { global $CFG, $DB; if (empty($plugin)) { unset($CFG->$name); $DB->delete_records('config', array('name' => $name)); cache_helper::invalidate_by_definition('core', 'config', array(), 'core'); } else { $DB->delete_records('config_plugins', array('name' => $name, 'plugin' => $plugin)); cache_helper::invalidate_by_definition('core', 'config', array(), $plugin); } return true; } /** * Remove all the config variables for a given plugin. * * NOTE: this function is called from lib/db/upgrade.php * * @param string $plugin a plugin, for example 'quiz' or 'qtype_multichoice'; * @return boolean whether the operation succeeded. */ function unset_all_config_for_plugin($plugin) { global $DB; // Delete from the obvious config_plugins first. $DB->delete_records('config_plugins', array('plugin' => $plugin)); // Next delete any suspect settings from config. $like = $DB->sql_like('name', '?', true, true, false, '|'); $params = array($DB->sql_like_escape($plugin.'_', '|') . '%'); $DB->delete_records_select('config', $like, $params); // Finally clear both the plugin cache and the core cache (suspect settings now removed from core). cache_helper::invalidate_by_definition('core', 'config', array(), array('core', $plugin)); return true; } /** * Use this function to get a list of users from a config setting of type admin_setting_users_with_capability. * * All users are verified if they still have the necessary capability. * * @param string $value the value of the config setting. * @param string $capability the capability - must match the one passed to the admin_setting_users_with_capability constructor. * @param bool $includeadmins include administrators. * @return array of user objects. */ function get_users_from_config($value, $capability, $includeadmins = true) { if (empty($value) or $value === '$@NONE@$') { return array(); } // We have to make sure that users still have the necessary capability, // it should be faster to fetch them all first and then test if they are present // instead of validating them one-by-one. $users = get_users_by_capability(context_system::instance(), $capability); if ($includeadmins) { $admins = get_admins(); foreach ($admins as $admin) { $users[$admin->id] = $admin; } } if ($value === '$@ALL@$') { return $users; } $result = array(); // Result in correct order. $allowed = explode(',', $value); foreach ($allowed as $uid) { if (isset($users[$uid])) { $user = $users[$uid]; $result[$user->id] = $user; } } return $result; } /** * Invalidates browser caches and cached data in temp. * * @return void */ function purge_all_caches() { purge_caches(); } /** * Selectively invalidate different types of cache. * * Purges the cache areas specified. By default, this will purge all caches but can selectively purge specific * areas alone or in combination. * * @param bool[] $options Specific parts of the cache to purge. Valid options are: * 'muc' Purge MUC caches? * 'theme' Purge theme cache? * 'lang' Purge language string cache? * 'js' Purge javascript cache? * 'filter' Purge text filter cache? * 'other' Purge all other caches? */ function purge_caches($options = []) { $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false); if (empty(array_filter($options))) { $options = array_fill_keys(array_keys($defaults), true); // Set all options to true. } else { $options = array_merge($defaults, array_intersect_key($options, $defaults)); // Override defaults with specified options. } if ($options['muc']) { cache_helper::purge_all(); } if ($options['theme']) { theme_reset_all_caches(); } if ($options['lang']) { get_string_manager()->reset_caches(); } if ($options['js']) { js_reset_all_caches(); } if ($options['template']) { template_reset_all_caches(); } if ($options['filter']) { reset_text_filters_cache(); } if ($options['other']) { purge_other_caches(); } } /** * Purge all non-MUC caches not otherwise purged in purge_caches. * * IMPORTANT - If you are adding anything here to do with the cache directory you should also have a look at * {@link phpunit_util::reset_dataroot()} */ function purge_other_caches() { global $DB, $CFG;
< core_text::reset_caches();
if (class_exists('core_plugin_manager')) { core_plugin_manager::reset_caches(); } // Bump up cacherev field for all courses. try { increment_revision_number('course', 'cacherev', ''); } catch (moodle_exception $e) { // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet. } $DB->reset_caches(); // Purge all other caches: rss, simplepie, etc. clearstatcache(); remove_dir($CFG->cachedir.'', true); // Make sure cache dir is writable, throws exception if not. make_cache_directory(''); // This is the only place where we purge local caches, we are only adding files there. // The $CFG->localcachedirpurged flag forces local directories to be purged on cluster nodes. remove_dir($CFG->localcachedir, true); set_config('localcachedirpurged', time()); make_localcache_directory('', true); \core\task\manager::clear_static_caches(); } /** * Get volatile flags * * @param string $type * @param int $changedsince default null * @return array records array */ function get_cache_flags($type, $changedsince = null) { global $DB; $params = array('type' => $type, 'expiry' => time()); $sqlwhere = "flagtype = :type AND expiry >= :expiry"; if ($changedsince !== null) { $params['changedsince'] = $changedsince; $sqlwhere .= " AND timemodified > :changedsince"; } $cf = array(); if ($flags = $DB->get_records_select('cache_flags', $sqlwhere, $params, '', 'name,value')) { foreach ($flags as $flag) { $cf[$flag->name] = $flag->value; } } return $cf; } /** * Get volatile flags * * @param string $type * @param string $name * @param int $changedsince default null * @return string|false The cache flag value or false */ function get_cache_flag($type, $name, $changedsince=null) { global $DB; $params = array('type' => $type, 'name' => $name, 'expiry' => time()); $sqlwhere = "flagtype = :type AND name = :name AND expiry >= :expiry"; if ($changedsince !== null) { $params['changedsince'] = $changedsince; $sqlwhere .= " AND timemodified > :changedsince"; } return $DB->get_field_select('cache_flags', 'value', $sqlwhere, $params); } /** * Set a volatile flag * * @param string $type the "type" namespace for the key * @param string $name the key to set * @param string $value the value to set (without magic quotes) - null will remove the flag * @param int $expiry (optional) epoch indicating expiry - defaults to now()+ 24hs * @return bool Always returns true */ function set_cache_flag($type, $name, $value, $expiry = null) { global $DB; $timemodified = time(); if ($expiry === null || $expiry < $timemodified) { $expiry = $timemodified + 24 * 60 * 60; } else { $expiry = (int)$expiry; } if ($value === null) { unset_cache_flag($type, $name); return true; } if ($f = $DB->get_record('cache_flags', array('name' => $name, 'flagtype' => $type), '*', IGNORE_MULTIPLE)) { // This is a potential problem in DEBUG_DEVELOPER. if ($f->value == $value and $f->expiry == $expiry and $f->timemodified == $timemodified) { return true; // No need to update. } $f->value = $value; $f->expiry = $expiry; $f->timemodified = $timemodified; $DB->update_record('cache_flags', $f); } else { $f = new stdClass(); $f->flagtype = $type; $f->name = $name; $f->value = $value; $f->expiry = $expiry; $f->timemodified = $timemodified; $DB->insert_record('cache_flags', $f); } return true; } /** * Removes a single volatile flag * * @param string $type the "type" namespace for the key * @param string $name the key to set * @return bool */ function unset_cache_flag($type, $name) { global $DB; $DB->delete_records('cache_flags', array('name' => $name, 'flagtype' => $type)); return true; } /** * Garbage-collect volatile flags * * @return bool Always returns true */ function gc_cache_flags() { global $DB; $DB->delete_records_select('cache_flags', 'expiry < ?', array(time())); return true; } // USER PREFERENCE API. /** * Refresh user preference cache. This is used most often for $USER * object that is stored in session, but it also helps with performance in cron script. * * Preferences for each user are loaded on first use on every page, then again after the timeout expires. * * @package core * @category preference * @access public * @param stdClass $user User object. Preferences are preloaded into 'preference' property * @param int $cachelifetime Cache life time on the current page (in seconds) * @throws coding_exception * @return null */ function check_user_preferences_loaded(stdClass $user, $cachelifetime = 120) { global $DB; // Static cache, we need to check on each page load, not only every 2 minutes. static $loadedusers = array(); if (!isset($user->id)) { throw new coding_exception('Invalid $user parameter in check_user_preferences_loaded() call, missing id field'); } if (empty($user->id) or isguestuser($user->id)) { // No permanent storage for not-logged-in users and guest. if (!isset($user->preference)) { $user->preference = array(); } return; } $timenow = time(); if (isset($loadedusers[$user->id]) and isset($user->preference) and isset($user->preference['_lastloaded'])) { // Already loaded at least once on this page. Are we up to date? if ($user->preference['_lastloaded'] + $cachelifetime > $timenow) { // No need to reload - we are on the same page and we loaded prefs just a moment ago. return; } else if (!get_cache_flag('userpreferenceschanged', $user->id, $user->preference['_lastloaded'])) { // No change since the lastcheck on this page. $user->preference['_lastloaded'] = $timenow; return; } } // OK, so we have to reload all preferences. $loadedusers[$user->id] = true; $user->preference = $DB->get_records_menu('user_preferences', array('userid' => $user->id), '', 'name,value'); // All values. $user->preference['_lastloaded'] = $timenow; } /** * Called from set/unset_user_preferences, so that the prefs can be correctly reloaded in different sessions. * * NOTE: internal function, do not call from other code. * * @package core * @access private * @param integer $userid the user whose prefs were changed. */ function mark_user_preferences_changed($userid) { global $CFG; if (empty($userid) or isguestuser($userid)) { // No cache flags for guest and not-logged-in users. return; } set_cache_flag('userpreferenceschanged', $userid, 1, time() + $CFG->sessiontimeout); } /** * Sets a preference for the specified user. * * If a $user object is submitted it's 'preference' property is used for the preferences cache. * * When additional validation/permission check is needed it is better to use {@see useredit_update_user_preference()} * * @package core * @category preference * @access public * @param string $name The key to set as preference for the specified user * @param string $value The value to set for the $name key in the specified user's * record, null means delete current value. * @param stdClass|int|null $user A moodle user object or id, null means current user * @throws coding_exception * @return bool Always true or exception */ function set_user_preference($name, $value, $user = null) { global $USER, $DB; if (empty($name) or is_numeric($name) or $name === '_lastloaded') { throw new coding_exception('Invalid preference name in set_user_preference() call'); } if (is_null($value)) { // Null means delete current. return unset_user_preference($name, $user); } else if (is_object($value)) { throw new coding_exception('Invalid value in set_user_preference() call, objects are not allowed'); } else if (is_array($value)) { throw new coding_exception('Invalid value in set_user_preference() call, arrays are not allowed'); } // Value column maximum length is 1333 characters. $value = (string)$value; if (core_text::strlen($value) > 1333) { throw new coding_exception('Invalid value in set_user_preference() call, value is is too long for the value column'); } if (is_null($user)) { $user = $USER; } else if (isset($user->id)) { // It is a valid object. } else if (is_numeric($user)) { $user = (object)array('id' => (int)$user); } else { throw new coding_exception('Invalid $user parameter in set_user_preference() call'); } check_user_preferences_loaded($user); if (empty($user->id) or isguestuser($user->id)) { // No permanent storage for not-logged-in users and guest. $user->preference[$name] = $value; return true; } if ($preference = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => $name))) { if ($preference->value === $value and isset($user->preference[$name]) and $user->preference[$name] === $value) { // Preference already set to this value. return true; } $DB->set_field('user_preferences', 'value', $value, array('id' => $preference->id)); } else { $preference = new stdClass(); $preference->userid = $user->id; $preference->name = $name; $preference->value = $value; $DB->insert_record('user_preferences', $preference); } // Update value in cache. $user->preference[$name] = $value; // Update the $USER in case where we've not a direct reference to $USER. if ($user !== $USER && $user->id == $USER->id) { $USER->preference[$name] = $value; } // Set reload flag for other sessions. mark_user_preferences_changed($user->id); return true; } /** * Sets a whole array of preferences for the current user * * If a $user object is submitted it's 'preference' property is used for the preferences cache. * * @package core * @category preference * @access public * @param array $prefarray An array of key/value pairs to be set * @param stdClass|int|null $user A moodle user object or id, null means current user * @return bool Always true or exception */ function set_user_preferences(array $prefarray, $user = null) { foreach ($prefarray as $name => $value) { set_user_preference($name, $value, $user); } return true; } /** * Unsets a preference completely by deleting it from the database * * If a $user object is submitted it's 'preference' property is used for the preferences cache. * * @package core * @category preference * @access public * @param string $name The key to unset as preference for the specified user * @param stdClass|int|null $user A moodle user object or id, null means current user * @throws coding_exception * @return bool Always true or exception */ function unset_user_preference($name, $user = null) { global $USER, $DB; if (empty($name) or is_numeric($name) or $name === '_lastloaded') { throw new coding_exception('Invalid preference name in unset_user_preference() call'); } if (is_null($user)) { $user = $USER; } else if (isset($user->id)) { // It is a valid object. } else if (is_numeric($user)) { $user = (object)array('id' => (int)$user); } else { throw new coding_exception('Invalid $user parameter in unset_user_preference() call'); } check_user_preferences_loaded($user); if (empty($user->id) or isguestuser($user->id)) { // No permanent storage for not-logged-in user and guest. unset($user->preference[$name]); return true; } // Delete from DB. $DB->delete_records('user_preferences', array('userid' => $user->id, 'name' => $name)); // Delete the preference from cache. unset($user->preference[$name]); // Update the $USER in case where we've not a direct reference to $USER. if ($user !== $USER && $user->id == $USER->id) { unset($USER->preference[$name]); } // Set reload flag for other sessions. mark_user_preferences_changed($user->id); return true; } /** * Used to fetch user preference(s) * * If no arguments are supplied this function will return * all of the current user preferences as an array. * * If a name is specified then this function * attempts to return that particular preference value. If * none is found, then the optional value $default is returned, * otherwise null. * * If a $user object is submitted it's 'preference' property is used for the preferences cache. * * @package core * @category preference * @access public * @param string $name Name of the key to use in finding a preference value * @param mixed|null $default Value to be returned if the $name key is not set in the user preferences * @param stdClass|int|null $user A moodle user object or id, null means current user * @throws coding_exception * @return string|mixed|null A string containing the value of a single preference. An * array with all of the preferences or null */ function get_user_preferences($name = null, $default = null, $user = null) { global $USER; if (is_null($name)) { // All prefs. } else if (is_numeric($name) or $name === '_lastloaded') { throw new coding_exception('Invalid preference name in get_user_preferences() call'); } if (is_null($user)) { $user = $USER; } else if (isset($user->id)) { // Is a valid object. } else if (is_numeric($user)) { if ($USER->id == $user) { $user = $USER; } else { $user = (object)array('id' => (int)$user); } } else { throw new coding_exception('Invalid $user parameter in get_user_preferences() call'); } check_user_preferences_loaded($user); if (empty($name)) { // All values. return $user->preference; } else if (isset($user->preference[$name])) { // The single string value. return $user->preference[$name]; } else { // Default value (null if not specified). return $default; } } // FUNCTIONS FOR HANDLING TIME. /** * Given Gregorian date parts in user time produce a GMT timestamp. * * @package core * @category time * @param int $year The year part to create timestamp of * @param int $month The month part to create timestamp of * @param int $day The day part to create timestamp of * @param int $hour The hour part to create timestamp of * @param int $minute The minute part to create timestamp of * @param int $second The second part to create timestamp of * @param int|float|string $timezone Timezone modifier, used to calculate GMT time offset.
< * if 99 then default user's timezone is used {@link http://docs.moodle.org/dev/Time_API#Timezone}
> * if 99 then default user's timezone is used {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
* @param bool $applydst Toggle Daylight Saving Time, default true, will be * applied only if timezone is 99 or string. * @return int GMT timestamp */ function make_timestamp($year, $month=1, $day=1, $hour=0, $minute=0, $second=0, $timezone=99, $applydst=true) { $date = new DateTime('now', core_date::get_user_timezone_object($timezone)); $date->setDate((int)$year, (int)$month, (int)$day); $date->setTime((int)$hour, (int)$minute, (int)$second); $time = $date->getTimestamp(); if ($time === false) { throw new coding_exception('getTimestamp() returned false, please ensure you have passed correct values.'. ' This can fail if year is more than 2038 and OS is 32 bit windows'); } // Moodle BC DST stuff. if (!$applydst) { $time += dst_offset_on($time, $timezone); } return $time; } /** * Format a date/time (seconds) as weeks, days, hours etc as needed * * Given an amount of time in seconds, returns string * formatted nicely as years, days, hours etc as needed * * @package core * @category time * @uses MINSECS * @uses HOURSECS * @uses DAYSECS * @uses YEARSECS * @param int $totalsecs Time in seconds * @param stdClass $str Should be a time object * @return string A nicely formatted date/time string */ function format_time($totalsecs, $str = null) { $totalsecs = abs($totalsecs); if (!$str) { // Create the str structure the slow way. $str = new stdClass(); $str->day = get_string('day'); $str->days = get_string('days'); $str->hour = get_string('hour'); $str->hours = get_string('hours'); $str->min = get_string('min'); $str->mins = get_string('mins'); $str->sec = get_string('sec'); $str->secs = get_string('secs'); $str->year = get_string('year'); $str->years = get_string('years'); } $years = floor($totalsecs/YEARSECS); $remainder = $totalsecs - ($years*YEARSECS); $days = floor($remainder/DAYSECS); $remainder = $totalsecs - ($days*DAYSECS); $hours = floor($remainder/HOURSECS); $remainder = $remainder - ($hours*HOURSECS); $mins = floor($remainder/MINSECS); $secs = $remainder - ($mins*MINSECS); $ss = ($secs == 1) ? $str->sec : $str->secs; $sm = ($mins == 1) ? $str->min : $str->mins; $sh = ($hours == 1) ? $str->hour : $str->hours; $sd = ($days == 1) ? $str->day : $str->days; $sy = ($years == 1) ? $str->year : $str->years; $oyears = ''; $odays = ''; $ohours = ''; $omins = ''; $osecs = ''; if ($years) { $oyears = $years .' '. $sy; } if ($days) { $odays = $days .' '. $sd; } if ($hours) { $ohours = $hours .' '. $sh; } if ($mins) { $omins = $mins .' '. $sm; } if ($secs) { $osecs = $secs .' '. $ss; } if ($years) { return trim($oyears .' '. $odays); } if ($days) { return trim($odays .' '. $ohours); } if ($hours) { return trim($ohours .' '. $omins); } if ($mins) { return trim($omins .' '. $osecs); } if ($secs) { return $osecs; } return get_string('now'); } /** * Returns a formatted string that represents a date in user time. * * @package core * @category time * @param int $date the timestamp in UTC, as obtained from the database. * @param string $format strftime format. You should probably get this using * get_string('strftime...', 'langconfig'); * @param int|float|string $timezone by default, uses the user's time zone. if numeric and * not 99 then daylight saving will not be added.
< * {@link http://docs.moodle.org/dev/Time_API#Timezone}
> * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
* @param bool $fixday If true (default) then the leading zero from %d is removed. * If false then the leading zero is maintained. * @param bool $fixhour If true (default) then the leading zero from %I is removed. * @return string the formatted date/time. */ function userdate($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); return $calendartype->timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour); } /** * Returns a html "time" tag with both the exact user date with timezone information * as a datetime attribute in the W3C format, and the user readable date and time as text. * * @package core * @category time * @param int $date the timestamp in UTC, as obtained from the database. * @param string $format strftime format. You should probably get this using * get_string('strftime...', 'langconfig'); * @param int|float|string $timezone by default, uses the user's time zone. if numeric and * not 99 then daylight saving will not be added.
< * {@link http://docs.moodle.org/dev/Time_API#Timezone}
> * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
* @param bool $fixday If true (default) then the leading zero from %d is removed. * If false then the leading zero is maintained. * @param bool $fixhour If true (default) then the leading zero from %I is removed. * @return string the formatted date/time. */ function userdate_htmltime($date, $format = '', $timezone = 99, $fixday = true, $fixhour = true) { $userdatestr = userdate($date, $format, $timezone, $fixday, $fixhour); if (CLI_SCRIPT && !PHPUNIT_TEST) { return $userdatestr; } $machinedate = new DateTime(); $machinedate->setTimestamp(intval($date)); $machinedate->setTimezone(core_date::get_user_timezone_object()); return html_writer::tag('time', $userdatestr, ['datetime' => $machinedate->format(DateTime::W3C)]); } /** * Returns a formatted date ensuring it is UTF-8. * * If we are running under Windows convert to Windows encoding and then back to UTF-8 * (because it's impossible to specify UTF-8 to fetch locale info in Win32). * * @param int $date the timestamp - since Moodle 2.9 this is a real UTC timestamp * @param string $format strftime format. * @param int|float|string $tz the user timezone * @return string the formatted date/time. * @since Moodle 2.3.3 */ function date_format_string($date, $format, $tz = 99) {
< global $CFG; < < $localewincharset = null; < // Get the calendar type user is using. < if ($CFG->ostype == 'WINDOWS') { < $calendartype = \core_calendar\type_factory::get_calendar_instance(); < $localewincharset = $calendartype->locale_win_charset(); < } < < if ($localewincharset) { < $format = core_text::convert($format, 'utf-8', $localewincharset); < }
date_default_timezone_set(core_date::get_user_timezone($tz));
< if (strftime('%p', 0) === strftime('%p', HOURSECS * 18)) {
> if (date('A', 0) === date('A', HOURSECS * 18)) {
$datearray = getdate($date); $format = str_replace([ '%P', '%p', ], [ $datearray['hours'] < 12 ? get_string('am', 'langconfig') : get_string('pm', 'langconfig'), $datearray['hours'] < 12 ? get_string('amcaps', 'langconfig') : get_string('pmcaps', 'langconfig'), ], $format); }
< $datestring = strftime($format, $date);
> $datestring = core_date::strftime($format, $date);
core_date::set_default_server_timezone();
< if ($localewincharset) { < $datestring = core_text::convert($datestring, $localewincharset, 'utf-8'); < } <
return $datestring; } /** * Given a $time timestamp in GMT (seconds since epoch), * returns an array that represents the Gregorian date in user time * * @package core * @category time * @param int $time Timestamp in GMT * @param float|int|string $timezone user timezone * @return array An array that represents the date in user time */ function usergetdate($time, $timezone=99) { if ($time === null) { // PHP8 and PHP7 return different results when getdate(null) is called. // Display warning and cast to 0 to make sure the usergetdate() behaves consistently on all versions of PHP. // In the future versions of Moodle we may consider adding a strict typehint. debugging('usergetdate() expects parameter $time to be int, null given', DEBUG_DEVELOPER); $time = 0; } date_default_timezone_set(core_date::get_user_timezone($timezone)); $result = getdate($time); core_date::set_default_server_timezone(); return $result; } /** * Given a GMT timestamp (seconds since epoch), offsets it by * the timezone. eg 3pm in India is 3pm GMT - 7 * 3600 seconds * * NOTE: this function does not include DST properly, * you should use the PHP date stuff instead! * * @package core * @category time * @param int $date Timestamp in GMT * @param float|int|string $timezone user timezone * @return int */ function usertime($date, $timezone=99) { $userdate = new DateTime('@' . $date); $userdate->setTimezone(core_date::get_user_timezone_object($timezone)); $dst = dst_offset_on($date, $timezone); return $date - $userdate->getOffset() + $dst; } /** * Get a formatted string representation of an interval between two unix timestamps. * * E.g. * $intervalstring = get_time_interval_string(12345600, 12345660); * Will produce the string: * '0d 0h 1m' * * @param int $time1 unix timestamp * @param int $time2 unix timestamp * @param string $format string (can be lang string) containing format chars: https://www.php.net/manual/en/dateinterval.format.php.
> * @param bool $dropzeroes If format is not provided and this is set to true, do not include zero time units. * @return string the formatted string describing the time difference, e.g. '10d 11h 45m'. > * e.g. a duration of 3 days and 2 hours will be displayed as '3d 2h' instead of '3d 2h 0s' */ > * @param bool $fullformat If format is not provided and this is set to true, display time units in full format. function get_time_interval_string(int $time1, int $time2, string $format = ''): string { > * e.g. instead of showing "3d", "3 days" will be returned.
< function get_time_interval_string(int $time1, int $time2, string $format = ''): string {
> function get_time_interval_string(int $time1, int $time2, string $format = '', > bool $dropzeroes = false, bool $fullformat = false): string {
$dtdate->setTimeStamp($time1); $dtdate2 = new DateTime(); $dtdate2->setTimeStamp($time2); $interval = $dtdate2->diff($dtdate);
< $format = empty($format) ? get_string('dateintervaldayshoursmins', 'langconfig') : $format;
> > if (empty(trim($format))) { > // Default to this key. > $formatkey = 'dateintervaldayhrmin'; > > if ($dropzeroes) { > $units = [ > 'y' => 'yr', > 'm' => 'mo', > 'd' => 'day', > 'h' => 'hr', > 'i' => 'min', > 's' => 'sec', > ]; > $formatunits = []; > foreach ($units as $key => $unit) { > if (empty($interval->$key)) { > continue; > } > $formatunits[] = $unit; > } > if (!empty($formatunits)) { > $formatkey = 'dateinterval' . implode("", $formatunits); > } > } > > if ($fullformat) { > $formatkey .= 'full'; > } > $format = get_string($formatkey, 'langconfig'); > }
return $interval->format($format); } /** * Given a time, return the GMT timestamp of the most recent midnight * for the current user. * * @package core * @category time * @param int $date Timestamp in GMT * @param float|int|string $timezone user timezone * @return int Returns a GMT timestamp */ function usergetmidnight($date, $timezone=99) { $userdate = usergetdate($date, $timezone); // Time of midnight of this user's day, in GMT. return make_timestamp($userdate['year'], $userdate['mon'], $userdate['mday'], 0, 0, 0, $timezone); } /** * Returns a string that prints the user's timezone * * @package core * @category time * @param float|int|string $timezone user timezone * @return string */ function usertimezone($timezone=99) { $tz = core_date::get_user_timezone($timezone); return core_date::get_localised_timezone($tz); } /** * Returns a float or a string which denotes the user's timezone * A float value means that a simple offset from GMT is used, while a string (it will be the name of a timezone in the database) * means that for this timezone there are also DST rules to be taken into account * Checks various settings and picks the most dominant of those which have a value * * @package core * @category time * @param float|int|string $tz timezone to calculate GMT time offset before * calculating user timezone, 99 is default user timezone
< * {@link http://docs.moodle.org/dev/Time_API#Timezone}
> * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
* @return float|string */ function get_user_timezone($tz = 99) { global $USER, $CFG; $timezones = array( $tz, isset($CFG->forcetimezone) ? $CFG->forcetimezone : 99, isset($USER->timezone) ? $USER->timezone : 99, isset($CFG->timezone) ? $CFG->timezone : 99, ); $tz = 99; // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array. foreach ($timezones as $nextvalue) { if ((empty($tz) && !is_numeric($tz)) || $tz == 99) { $tz = $nextvalue; } } return is_numeric($tz) ? (float) $tz : $tz; } /** * Calculates the Daylight Saving Offset for a given date/time (timestamp) * - Note: Daylight saving only works for string timezones and not for float. * * @package core * @category time * @param int $time must NOT be compensated at all, it has to be a pure timestamp * @param int|float|string $strtimezone user timezone * @return int */ function dst_offset_on($time, $strtimezone = null) { $tz = core_date::get_user_timezone($strtimezone); $date = new DateTime('@' . $time); $date->setTimezone(new DateTimeZone($tz)); if ($date->format('I') == '1') { if ($tz === 'Australia/Lord_Howe') { return 1800; } return 3600; } return 0; } /** * Calculates when the day appears in specific month * * @package core * @category time * @param int $startday starting day of the month * @param int $weekday The day when week starts (normally taken from user preferences) * @param int $month The month whose day is sought * @param int $year The year of the month whose day is sought * @return int */ function find_day_in_month($startday, $weekday, $month, $year) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); $daysinmonth = days_in_month($month, $year); $daysinweek = count($calendartype->get_weekdays()); if ($weekday == -1) { // Don't care about weekday, so return: // abs($startday) if $startday != -1 // $daysinmonth otherwise. return ($startday == -1) ? $daysinmonth : abs($startday); } // From now on we 're looking for a specific weekday. // Give "end of month" its actual value, since we know it. if ($startday == -1) { $startday = -1 * $daysinmonth; } // Starting from day $startday, the sign is the direction. if ($startday < 1) { $startday = abs($startday); $lastmonthweekday = dayofweek($daysinmonth, $month, $year); // This is the last such weekday of the month. $lastinmonth = $daysinmonth + $weekday - $lastmonthweekday; if ($lastinmonth > $daysinmonth) { $lastinmonth -= $daysinweek; } // Find the first such weekday <= $startday. while ($lastinmonth > $startday) { $lastinmonth -= $daysinweek; } return $lastinmonth; } else { $indexweekday = dayofweek($startday, $month, $year); $diff = $weekday - $indexweekday; if ($diff < 0) { $diff += $daysinweek; } // This is the first such weekday of the month equal to or after $startday. $firstfromindex = $startday + $diff; return $firstfromindex; } } /** * Calculate the number of days in a given month * * @package core * @category time * @param int $month The month whose day count is sought * @param int $year The year of the month whose day count is sought * @return int */ function days_in_month($month, $year) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); return $calendartype->get_num_days_in_month($year, $month); } /** * Calculate the position in the week of a specific calendar day * * @package core * @category time * @param int $day The day of the date whose position in the week is sought * @param int $month The month of the date whose position in the week is sought * @param int $year The year of the date whose position in the week is sought * @return int */ function dayofweek($day, $month, $year) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); return $calendartype->get_weekday($year, $month, $day); } // USER AUTHENTICATION AND LOGIN. /** * Returns full login url. * * Any form submissions for authentication to this URL must include username, * password as well as a logintoken generated by \core\session\manager::get_login_token(). * * @return string login url */ function get_login_url() { global $CFG; return "$CFG->wwwroot/login/index.php"; } /** * This function checks that the current user is logged in and has the * required privileges * * This function checks that the current user is logged in, and optionally * whether they are allowed to be in a particular course and view a particular * course module. * If they are not logged in, then it redirects them to the site login unless * $autologinguest is set and {@link $CFG}->autologinguests is set to 1 in which * case they are automatically logged in as guests. * If $courseid is given and the user is not enrolled in that course then the * user is redirected to the course enrolment page. * If $cm is given and the course module is hidden and the user is not a teacher * in the course then the user is redirected to the course home page. * * When $cm parameter specified, this function sets page layout to 'module'. * You need to change it manually later if some other layout needed. * * @package core_access * @category access * * @param mixed $courseorid id of the course or course object * @param bool $autologinguest default true * @param object $cm course module object * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to * true. Used to avoid (=false) some scripts (file.php...) to set that variable, * in order to keep redirects working properly. MDL-14495 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions * @return mixed Void, exit, and die depending on path * @throws coding_exception * @throws require_login_exception * @throws moodle_exception */ function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT; // Must not redirect when byteserving already started. if (!empty($_SERVER['HTTP_RANGE'])) { $preventredirect = true; } if (AJAX_SCRIPT) { // We cannot redirect for AJAX scripts either. $preventredirect = true; } // Setup global $COURSE, themes, language and locale. if (!empty($courseorid)) { if (is_object($courseorid)) { $course = $courseorid; } else if ($courseorid == SITEID) { $course = clone($SITE); } else { $course = $DB->get_record('course', array('id' => $courseorid), '*', MUST_EXIST); } if ($cm) { if ($cm->course != $course->id) { throw new coding_exception('course and cm parameters in require_login() call do not match!!'); } // Make sure we have a $cm from get_fast_modinfo as this contains activity access details. if (!($cm instanceof cm_info)) { // Note: nearly all pages call get_fast_modinfo anyway and it does not make any // db queries so this is not really a performance concern, however it is obviously // better if you use get_fast_modinfo to get the cm before calling this. $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cm->id); } } } else { // Do not touch global $COURSE via $PAGE->set_course(), // the reasons is we need to be able to call require_login() at any time!! $course = $SITE; if ($cm) { throw new coding_exception('cm parameter in require_login() requires valid course parameter!'); } } // If this is an AJAX request and $setwantsurltome is true then we need to override it and set it to false. // Otherwise the AJAX request URL will be set to $SESSION->wantsurl and events such as self enrolment in the future // risk leading the user back to the AJAX request URL. if ($setwantsurltome && defined('AJAX_SCRIPT') && AJAX_SCRIPT) { $setwantsurltome = false; } // Redirect to the login page if session has expired, only with dbsessions enabled (MDL-35029) to maintain current behaviour. if ((!isloggedin() or isguestuser()) && !empty($SESSION->has_timed_out) && !empty($CFG->dbsessions)) { if ($preventredirect) { throw new require_login_session_timeout_exception(); } else { if ($setwantsurltome) { $SESSION->wantsurl = qualified_me(); } redirect(get_login_url()); } } // If the user is not even logged in yet then make sure they are. if (!isloggedin()) {
< if ($autologinguest and !empty($CFG->guestloginbutton) and !empty($CFG->autologinguests)) {
> if ($autologinguest && !empty($CFG->autologinguests)) {
if (!$guest = get_complete_user_data('id', $CFG->siteguest)) { // Misconfigured site guest, just redirect to login page. redirect(get_login_url()); exit; // Never reached. } $lang = isset($SESSION->lang) ? $SESSION->lang : $CFG->lang; complete_user_login($guest); $USER->autologinguest = true; $SESSION->lang = $lang; } else { // NOTE: $USER->site check was obsoleted by session test cookie, $USER->confirmed test is in login/index.php. if ($preventredirect) { throw new require_login_exception('You are not logged in'); } if ($setwantsurltome) { $SESSION->wantsurl = qualified_me(); } // Give auth plugins an opportunity to authenticate or redirect to an external login page $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. foreach($authsequence as $authname) { $authplugin = get_auth_plugin($authname); $authplugin->pre_loginpage_hook(); if (isloggedin()) { if ($cm) { $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cm->id); } set_access_log_user(); break; } } // If we're still not logged in then go to the login page if (!isloggedin()) { redirect(get_login_url()); exit; // Never reached. } } } // Loginas as redirection if needed. if ($course->id != SITEID and \core\session\manager::is_loggedinas()) { if ($USER->loginascontext->contextlevel == CONTEXT_COURSE) { if ($USER->loginascontext->instanceid != $course->id) {
< print_error('loginasonecourse', '', $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
> throw new \moodle_exception('loginasonecourse', '', > $CFG->wwwroot.'/course/view.php?id='.$USER->loginascontext->instanceid);
} } } // Check whether the user should be changing password (but only if it is REALLY them). if (get_user_preferences('auth_forcepasswordchange') && !\core\session\manager::is_loggedinas()) { $userauth = get_auth_plugin($USER->auth); if ($userauth->can_change_password() and !$preventredirect) { if ($setwantsurltome) { $SESSION->wantsurl = qualified_me(); } if ($changeurl = $userauth->change_password_url()) { // Use plugin custom url. redirect($changeurl); } else { // Use moodle internal method. redirect($CFG->wwwroot .'/login/change_password.php'); } } else if ($userauth->can_change_password()) { throw new moodle_exception('forcepasswordchangenotice'); } else { throw new moodle_exception('nopasswordchangeforced', 'auth'); } } // Check that the user account is properly set up. If we can't redirect to // edit their profile and this is not a WS request, perform just the lax check. // It will allow them to use filepicker on the profile edit page. if ($preventredirect && !WS_SERVER) { $usernotfullysetup = user_not_fully_set_up($USER, false); } else { $usernotfullysetup = user_not_fully_set_up($USER, true); } if ($usernotfullysetup) { if ($preventredirect) { throw new moodle_exception('usernotfullysetup'); } if ($setwantsurltome) { $SESSION->wantsurl = qualified_me(); } redirect($CFG->wwwroot .'/user/edit.php?id='. $USER->id .'&amp;course='. SITEID); } // Make sure the USER has a sesskey set up. Used for CSRF protection. sesskey(); if (\core\session\manager::is_loggedinas()) { // During a "logged in as" session we should force all content to be cleaned because the // logged in user will be viewing potentially malicious user generated content. // See MDL-63786 for more details. $CFG->forceclean = true; } $afterlogins = get_plugins_with_function('after_require_login', 'lib.php'); // Do not bother admins with any formalities, except for activities pending deletion. if (is_siteadmin() && !($cm && $cm->deletioninprogress)) { // Set the global $COURSE. if ($cm) { $PAGE->set_cm($cm, $course); $PAGE->set_pagelayout('incourse'); } else if (!empty($courseorid)) { $PAGE->set_course($course); } // Set accesstime or the user will appear offline which messes up messaging. // Do not update access time for webservice or ajax requests. if (!WS_SERVER && !AJAX_SCRIPT) { user_accesstime_log($course->id); } foreach ($afterlogins as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); } } return; } // Scripts have a chance to declare that $USER->policyagreed should not be checked. // This is mostly for places where users are actually accepting the policies, to avoid the redirect loop. if (!defined('NO_SITEPOLICY_CHECK')) { define('NO_SITEPOLICY_CHECK', false); } // Check that the user has agreed to a site policy if there is one - do not test in case of admins. // Do not test if the script explicitly asked for skipping the site policies check.
< if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK) {
> // Or if the user auth type is webservice. > if (!$USER->policyagreed && !is_siteadmin() && !NO_SITEPOLICY_CHECK && $USER->auth !== 'webservice') {
$manager = new \core_privacy\local\sitepolicy\manager(); if ($policyurl = $manager->get_redirect_url(isguestuser())) { if ($preventredirect) { throw new moodle_exception('sitepolicynotagreed', 'error', '', $policyurl->out()); } if ($setwantsurltome) { $SESSION->wantsurl = qualified_me(); } redirect($policyurl); } } // Fetch the system context, the course context, and prefetch its child contexts. $sysctx = context_system::instance(); $coursecontext = context_course::instance($course->id, MUST_EXIST); if ($cm) { $cmcontext = context_module::instance($cm->id, MUST_EXIST); } else { $cmcontext = null; } // If the site is currently under maintenance, then print a message. if (!empty($CFG->maintenance_enabled) and !has_capability('moodle/site:maintenanceaccess', $sysctx)) { if ($preventredirect) { throw new require_login_exception('Maintenance in progress'); } $PAGE->set_context(null); print_maintenance_message(); } // Make sure the course itself is not hidden. if ($course->id == SITEID) { // Frontpage can not be hidden. } else { if (is_role_switched($course->id)) { // When switching roles ignore the hidden flag - user had to be in course to do the switch. } else { if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) { // Originally there was also test of parent category visibility, BUT is was very slow in complex queries // involving "my courses" now it is also possible to simply hide all courses user is not enrolled in :-). if ($preventredirect) { throw new require_login_exception('Course is hidden'); } $PAGE->set_context(null); // We need to override the navigation URL as the course won't have been added to the navigation and thus // the navigation will mess up when trying to find it. navigation_node::override_active_url(new moodle_url('/')); notice(get_string('coursehidden'), $CFG->wwwroot .'/'); } } } // Is the user enrolled? if ($course->id == SITEID) { // Everybody is enrolled on the frontpage. } else { if (\core\session\manager::is_loggedinas()) { // Make sure the REAL person can access this course first. $realuser = \core\session\manager::get_realuser(); if (!is_enrolled($coursecontext, $realuser->id, '', true) and !is_viewing($coursecontext, $realuser->id) and !is_siteadmin($realuser->id)) { if ($preventredirect) { throw new require_login_exception('Invalid course login-as access'); } $PAGE->set_context(null); echo $OUTPUT->header(); notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/'); } } $access = false; if (is_role_switched($course->id)) { // Ok, user had to be inside this course before the switch. $access = true; } else if (is_viewing($coursecontext, $USER)) { // Ok, no need to mess with enrol. $access = true; } else { if (isset($USER->enrol['enrolled'][$course->id])) { if ($USER->enrol['enrolled'][$course->id] > time()) { $access = true; if (isset($USER->enrol['tempguest'][$course->id])) { unset($USER->enrol['tempguest'][$course->id]); remove_temp_course_roles($coursecontext); } } else { // Expired. unset($USER->enrol['enrolled'][$course->id]); } } if (isset($USER->enrol['tempguest'][$course->id])) { if ($USER->enrol['tempguest'][$course->id] == 0) { $access = true; } else if ($USER->enrol['tempguest'][$course->id] > time()) { $access = true; } else { // Expired. unset($USER->enrol['tempguest'][$course->id]); remove_temp_course_roles($coursecontext); } } if (!$access) { // Cache not ok. $until = enrol_get_enrolment_end($coursecontext->instanceid, $USER->id); if ($until !== false) { // Active participants may always access, a timestamp in the future, 0 (always) or false. if ($until == 0) { $until = ENROL_MAX_TIMESTAMP; } $USER->enrol['enrolled'][$course->id] = $until; $access = true; } else if (core_course_category::can_view_course_info($course)) { $params = array('courseid' => $course->id, 'status' => ENROL_INSTANCE_ENABLED); $instances = $DB->get_records('enrol', $params, 'sortorder, id ASC'); $enrols = enrol_get_plugins(true); // First ask all enabled enrol instances in course if they want to auto enrol user. foreach ($instances as $instance) { if (!isset($enrols[$instance->enrol])) { continue; } // Get a duration for the enrolment, a timestamp in the future, 0 (always) or false. $until = $enrols[$instance->enrol]->try_autoenrol($instance); if ($until !== false) { if ($until == 0) { $until = ENROL_MAX_TIMESTAMP; } $USER->enrol['enrolled'][$course->id] = $until; $access = true; break; } } // If not enrolled yet try to gain temporary guest access. if (!$access) { foreach ($instances as $instance) { if (!isset($enrols[$instance->enrol])) { continue; } // Get a duration for the guest access, a timestamp in the future or false. $until = $enrols[$instance->enrol]->try_guestaccess($instance); if ($until !== false and $until > time()) { $USER->enrol['tempguest'][$course->id] = $until; $access = true; break; } } } } else { // User is not enrolled and is not allowed to browse courses here. if ($preventredirect) { throw new require_login_exception('Course is not available'); } $PAGE->set_context(null); // We need to override the navigation URL as the course won't have been added to the navigation and thus // the navigation will mess up when trying to find it. navigation_node::override_active_url(new moodle_url('/')); notice(get_string('coursehidden'), $CFG->wwwroot .'/'); } } } if (!$access) { if ($preventredirect) { throw new require_login_exception('Not enrolled'); } if ($setwantsurltome) { $SESSION->wantsurl = qualified_me(); } redirect($CFG->wwwroot .'/enrol/index.php?id='. $course->id); } } // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins. if ($cm && $cm->deletioninprogress) { if ($preventredirect) { throw new moodle_exception('activityisscheduledfordeletion'); } require_once($CFG->dirroot . '/course/lib.php'); redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error')); } // Check visibility of activity to current user; includes visible flag, conditional availability, etc. if ($cm && !$cm->uservisible) { if ($preventredirect) { throw new require_login_exception('Activity is hidden'); } // Get the error message that activity is not available and why (if explanation can be shown to the user). $PAGE->set_course($course); $renderer = $PAGE->get_renderer('course'); $message = $renderer->course_section_cm_unavailable_error_message($cm); redirect(course_get_url($course), $message, null, \core\output\notification::NOTIFY_ERROR); } // Set the global $COURSE. if ($cm) { $PAGE->set_cm($cm, $course); $PAGE->set_pagelayout('incourse'); } else if (!empty($courseorid)) { $PAGE->set_course($course); } foreach ($afterlogins as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); } } // Finally access granted, update lastaccess times. // Do not update access time for webservice or ajax requests. if (!WS_SERVER && !AJAX_SCRIPT) { user_accesstime_log($course->id); } } /** * A convenience function for where we must be logged in as admin * @return void */ function require_admin() { require_login(null, false); require_capability('moodle/site:config', context_system::instance()); } /** * This function just makes sure a user is logged out. * * @package core_access * @category access */ function require_logout() { global $USER, $DB; if (!isloggedin()) { // This should not happen often, no need for hooks or events here. \core\session\manager::terminate_current(); return; } // Execute hooks before action. $authplugins = array(); $authsequence = get_enabled_auth_plugins(); foreach ($authsequence as $authname) { $authplugins[$authname] = get_auth_plugin($authname); $authplugins[$authname]->prelogout_hook(); } // Store info that gets removed during logout. $sid = session_id(); $event = \core\event\user_loggedout::create( array( 'userid' => $USER->id, 'objectid' => $USER->id, 'other' => array('sessionid' => $sid), ) ); if ($session = $DB->get_record('sessions', array('sid'=>$sid))) { $event->add_record_snapshot('sessions', $session); } // Clone of $USER object to be used by auth plugins. $user = fullclone($USER); // Delete session record and drop $_SESSION content. \core\session\manager::terminate_current(); // Trigger event AFTER action. $event->trigger(); // Hook to execute auth plugins redirection after event trigger. foreach ($authplugins as $authplugin) { $authplugin->postlogout_hook($user); } } /** * Weaker version of require_login() * * This is a weaker version of {@link require_login()} which only requires login * when called from within a course rather than the site page, unless * the forcelogin option is turned on. * @see require_login() * * @package core_access * @category access * * @param mixed $courseorid The course object or id in question * @param bool $autologinguest Allow autologin guests if that is wanted * @param object $cm Course activity module if known * @param bool $setwantsurltome Define if we want to set $SESSION->wantsurl, defaults to * true. Used to avoid (=false) some scripts (file.php...) to set that variable, * in order to keep redirects working properly. MDL-14495 * @param bool $preventredirect set to true in scripts that can not redirect (CLI, rss feeds, etc.), throws exceptions * @return void * @throws coding_exception */ function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) { global $CFG, $PAGE, $SITE; $issite = ((is_object($courseorid) and $courseorid->id == SITEID) or (!is_object($courseorid) and $courseorid == SITEID)); if ($issite && !empty($cm) && !($cm instanceof cm_info)) { // Note: nearly all pages call get_fast_modinfo anyway and it does not make any // db queries so this is not really a performance concern, however it is obviously // better if you use get_fast_modinfo to get the cm before calling this. if (is_object($courseorid)) { $course = $courseorid; } else { $course = clone($SITE); } $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cm->id); } if (!empty($CFG->forcelogin)) { // Login required for both SITE and courses. require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); } else if ($issite && !empty($cm) and !$cm->uservisible) { // Always login for hidden activities. require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); } else if (isloggedin() && !isguestuser()) { // User is already logged in. Make sure the login is complete (user is fully setup, policies agreed). require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); } else if ($issite) { // Login for SITE not required. // We still need to instatiate PAGE vars properly so that things that rely on it like navigation function correctly. if (!empty($courseorid)) { if (is_object($courseorid)) { $course = $courseorid; } else { $course = clone $SITE; } if ($cm) { if ($cm->course != $course->id) { throw new coding_exception('course and cm parameters in require_course_login() call do not match!!'); } $PAGE->set_cm($cm, $course); $PAGE->set_pagelayout('incourse'); } else { $PAGE->set_course($course); } } else { // If $PAGE->course, and hence $PAGE->context, have not already been set up properly, set them up now. $PAGE->set_course($PAGE->course); } // Do not update access time for webservice or ajax requests. if (!WS_SERVER && !AJAX_SCRIPT) { user_accesstime_log(SITEID); } return; } else { // Course login always required. require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); } } /** * Validates a user key, checking if the key exists, is not expired and the remote ip is correct. * * @param string $keyvalue the key value * @param string $script unique script identifier * @param int $instance instance id * @return stdClass the key entry in the user_private_key table * @since Moodle 3.2 * @throws moodle_exception */ function validate_user_key($keyvalue, $script, $instance) { global $DB; if (!$key = $DB->get_record('user_private_key', array('script' => $script, 'value' => $keyvalue, 'instance' => $instance))) {
< print_error('invalidkey');
> throw new \moodle_exception('invalidkey');
} if (!empty($key->validuntil) and $key->validuntil < time()) {
< print_error('expiredkey');
> throw new \moodle_exception('expiredkey');
} if ($key->iprestriction) { $remoteaddr = getremoteaddr(null); if (empty($remoteaddr) or !address_in_subnet($remoteaddr, $key->iprestriction)) {
< print_error('ipmismatch');
> throw new \moodle_exception('ipmismatch');
} } return $key; } /** * Require key login. Function terminates with error if key not found or incorrect. * * @uses NO_MOODLE_COOKIES * @uses PARAM_ALPHANUM * @param string $script unique script identifier * @param int $instance optional instance id * @param string $keyvalue The key. If not supplied, this will be fetched from the current session. * @return int Instance ID */ function require_user_key_login($script, $instance = null, $keyvalue = null) { global $DB; if (!NO_MOODLE_COOKIES) {
< print_error('sessioncookiesdisable');
> throw new \moodle_exception('sessioncookiesdisable');
} // Extra safety. \core\session\manager::write_close(); if (null === $keyvalue) { $keyvalue = required_param('key', PARAM_ALPHANUM); } $key = validate_user_key($keyvalue, $script, $instance); if (!$user = $DB->get_record('user', array('id' => $key->userid))) {
< print_error('invaliduserid');
> throw new \moodle_exception('invaliduserid');
} core_user::require_active_user($user, true, true); // Emulate normal session.
< enrol_check_plugins($user);
> enrol_check_plugins($user, false);
\core\session\manager::set_user($user); // Note we are not using normal login. if (!defined('USER_KEY_LOGIN')) { define('USER_KEY_LOGIN', true); } // Return instance id - it might be empty. return $key->instance; } /** * Creates a new private user access key. * * @param string $script unique target identifier * @param int $userid * @param int $instance optional instance id * @param string $iprestriction optional ip restricted access * @param int $validuntil key valid only until given data * @return string access key value */ function create_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { global $DB; $key = new stdClass(); $key->script = $script; $key->userid = $userid; $key->instance = $instance; $key->iprestriction = $iprestriction; $key->validuntil = $validuntil; $key->timecreated = time(); // Something long and unique. $key->value = md5($userid.'_'.time().random_string(40)); while ($DB->record_exists('user_private_key', array('value' => $key->value))) { // Must be unique. $key->value = md5($userid.'_'.time().random_string(40)); } $DB->insert_record('user_private_key', $key); return $key->value; } /** * Delete the user's new private user access keys for a particular script. * * @param string $script unique target identifier * @param int $userid * @return void */ function delete_user_key($script, $userid) { global $DB; $DB->delete_records('user_private_key', array('script' => $script, 'userid' => $userid)); } /** * Gets a private user access key (and creates one if one doesn't exist). * * @param string $script unique target identifier * @param int $userid * @param int $instance optional instance id * @param string $iprestriction optional ip restricted access * @param int $validuntil key valid only until given date * @return string access key value */ function get_user_key($script, $userid, $instance=null, $iprestriction=null, $validuntil=null) { global $DB; if ($key = $DB->get_record('user_private_key', array('script' => $script, 'userid' => $userid, 'instance' => $instance, 'iprestriction' => $iprestriction, 'validuntil' => $validuntil))) { return $key->value; } else { return create_user_key($script, $userid, $instance, $iprestriction, $validuntil); } } /** * Modify the user table by setting the currently logged in user's last login to now. * * @return bool Always returns true */ function update_user_login_times() {
< global $USER, $DB;
> global $USER, $DB, $SESSION;
if (isguestuser()) { // Do not update guest access times/ips for performance. return true; } if (defined('USER_KEY_LOGIN') && USER_KEY_LOGIN === true) { // Do not update user login time when using user key login. return true; } $now = time(); $user = new stdClass(); $user->id = $USER->id; // Make sure all users that logged in have some firstaccess. if ($USER->firstaccess == 0) { $USER->firstaccess = $user->firstaccess = $now; } // Store the previous current as lastlogin. $USER->lastlogin = $user->lastlogin = $USER->currentlogin; $USER->currentlogin = $user->currentlogin = $now; // Function user_accesstime_log() may not update immediately, better do it here. $USER->lastaccess = $user->lastaccess = $now;
> $SESSION->userpreviousip = $USER->lastip;
$USER->lastip = $user->lastip = getremoteaddr(); // Note: do not call user_update_user() here because this is part of the login process, // the login event means that these fields were updated. $DB->update_record('user', $user); return true; } /** * Determines if a user has completed setting up their account. * * The lax mode (with $strict = false) has been introduced for special cases * only where we want to skip certain checks intentionally. This is valid in * certain mnet or ajax scenarios when the user cannot / should not be * redirected to edit their profile. In most cases, you should perform the * strict check. * * @param stdClass $user A {@link $USER} object to test for the existence of a valid name and email * @param bool $strict Be more strict and assert id and custom profile fields set, too * @return bool */ function user_not_fully_set_up($user, $strict = true) {
< global $CFG;
> global $CFG, $SESSION, $USER;
require_once($CFG->dirroot.'/user/profile/lib.php');
> // If the user is setup then store this in the session to avoid re-checking. if (isguestuser($user)) { > // Some edge cases are when the users email starts to bounce or the return false; > // configuration for custom fields has changed while they are logged in so } > // we re-check this fully every hour for the rare cases it has changed. > if (isset($USER->id) && isset($user->id) && $USER->id === $user->id && if (empty($user->firstname) or empty($user->lastname) or empty($user->email) or over_bounce_threshold($user)) { > isset($SESSION->fullysetupstrict) && (time() - $SESSION->fullysetupstrict) < HOURSECS) { return true; > return false; } > } >
if ($strict) { if (empty($user->id)) { // Strict mode can be used with existing accounts only. return true; } if (!profile_has_required_custom_fields_set($user->id)) { return true; }
> if (isset($USER->id) && isset($user->id) && $USER->id === $user->id) { } > $SESSION->fullysetupstrict = time(); > }
return false; } /** * Check whether the user has exceeded the bounce threshold * * @param stdClass $user A {@link $USER} object * @return bool true => User has exceeded bounce threshold */ function over_bounce_threshold($user) { global $CFG, $DB; if (empty($CFG->handlebounces)) { return false; } if (empty($user->id)) { // No real (DB) user, nothing to do here. return false; } // Set sensible defaults. if (empty($CFG->minbounces)) { $CFG->minbounces = 10; } if (empty($CFG->bounceratio)) { $CFG->bounceratio = .20; } $bouncecount = 0; $sendcount = 0; if ($bounce = $DB->get_record('user_preferences', array ('userid' => $user->id, 'name' => 'email_bounce_count'))) { $bouncecount = $bounce->value; } if ($send = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { $sendcount = $send->value; } return ($bouncecount >= $CFG->minbounces && $bouncecount/$sendcount >= $CFG->bounceratio); } /** * Used to increment or reset email sent count * * @param stdClass $user object containing an id * @param bool $reset will reset the count to 0 * @return void */ function set_send_count($user, $reset=false) { global $DB; if (empty($user->id)) { // No real (DB) user, nothing to do here. return; } if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_send_count'))) { $pref->value = (!empty($reset)) ? 0 : $pref->value+1; $DB->update_record('user_preferences', $pref); } else if (!empty($reset)) { // If it's not there and we're resetting, don't bother. Make a new one. $pref = new stdClass(); $pref->name = 'email_send_count'; $pref->value = 1; $pref->userid = $user->id; $DB->insert_record('user_preferences', $pref, false); } } /** * Increment or reset user's email bounce count * * @param stdClass $user object containing an id * @param bool $reset will reset the count to 0 */ function set_bounce_count($user, $reset=false) { global $DB; if ($pref = $DB->get_record('user_preferences', array('userid' => $user->id, 'name' => 'email_bounce_count'))) { $pref->value = (!empty($reset)) ? 0 : $pref->value+1; $DB->update_record('user_preferences', $pref); } else if (!empty($reset)) { // If it's not there and we're resetting, don't bother. Make a new one. $pref = new stdClass(); $pref->name = 'email_bounce_count'; $pref->value = 1; $pref->userid = $user->id; $DB->insert_record('user_preferences', $pref, false); } } /** * Determines if the logged in user is currently moving an activity * * @param int $courseid The id of the course being tested * @return bool */ function ismoving($courseid) { global $USER; if (!empty($USER->activitycopy)) { return ($USER->activitycopycourse == $courseid); } return false; } /** * Returns a persons full name * * Given an object containing all of the users name values, this function returns a string with the full name of the person. * The result may depend on system settings or language. 'override' will force the alternativefullnameformat to be used. In * English, fullname as well as alternativefullnameformat is set to 'firstname lastname' by default. But you could have * fullname set to 'firstname lastname' and alternativefullnameformat set to 'firstname middlename alternatename lastname'. * * @param stdClass $user A {@link $USER} object to get full name of. * @param bool $override If true then the alternativefullnameformat format rather than fullnamedisplay format will be used. * @return string */ function fullname($user, $override=false) {
< global $CFG, $SESSION;
> // Note: We do not intend to deprecate this function any time soon as it is too widely used at this time. > // Uses of it should be updated to use the new API and pass updated arguments.
< if (!isset($user->firstname) and !isset($user->lastname)) {
> // Return an empty string if there is no user. > if (empty($user)) {
return ''; }
< // Get all of the name fields. < $allnames = \core_user\fields::get_name_fields(); < if ($CFG->debugdeveloper) { < foreach ($allnames as $allname) { < if (!property_exists($user, $allname)) { < // If all the user name fields are not set in the user object, then notify the programmer that it needs to be fixed. < debugging('You need to update your sql to include additional name fields in the user object.', DEBUG_DEVELOPER); < // Message has been sent, no point in sending the message multiple times. < break; < } < } < } < < if (!$override) { < if (!empty($CFG->forcefirstname)) { < $user->firstname = $CFG->forcefirstname; < } < if (!empty($CFG->forcelastname)) { < $user->lastname = $CFG->forcelastname; < } < } < < if (!empty($SESSION->fullnamedisplay)) { < $CFG->fullnamedisplay = $SESSION->fullnamedisplay; < } < < $template = null; < // If the fullnamedisplay setting is available, set the template to that. < if (isset($CFG->fullnamedisplay)) { < $template = $CFG->fullnamedisplay; < } < // If the template is empty, or set to language, return the language string. < if ((empty($template) || $template == 'language') && !$override) { < return get_string('fullnamedisplay', null, $user); < } < < // Check to see if we are displaying according to the alternative full name format. < if ($override) { < if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') { < // Default to show just the user names according to the fullnamedisplay string. < return get_string('fullnamedisplay', null, $user); < } else { < // If the override is true, then change the template to use the complete name. < $template = $CFG->alternativefullnameformat; < } < } < < $requirednames = array(); < // With each name, see if it is in the display name template, and add it to the required names array if it is. < foreach ($allnames as $allname) { < if (strpos($template, $allname) !== false) { < $requirednames[] = $allname; < } < } < < $displayname = $template; < // Switch in the actual data into the template. < foreach ($requirednames as $altname) { < if (isset($user->$altname)) { < // Using empty() on the below if statement causes breakages. < if ((string)$user->$altname == '') { < $displayname = str_replace($altname, 'EMPTY', $displayname); < } else { < $displayname = str_replace($altname, $user->$altname, $displayname); < } < } else { < $displayname = str_replace($altname, 'EMPTY', $displayname); < } < } < // Tidy up any misc. characters (Not perfect, but gets most characters). < // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or < // katakana and parenthesis. < $patterns = array(); < // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been < // filled in by a user. < // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:). < $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u'; < // This regular expression is to remove any double spaces in the display name. < $patterns[] = '/\s{2,}/u'; < foreach ($patterns as $pattern) { < $displayname = preg_replace($pattern, ' ', $displayname); < } < < // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces. < $displayname = trim($displayname); < if (empty($displayname)) { < // Going with just the first name if no alternate fields are filled out. May be changed later depending on what < // people in general feel is a good setting to fall back on. < $displayname = $user->firstname; < } < return $displayname;
> $options = ['override' => $override]; > return core_user::get_fullname($user, null, $options);
} /** * Reduces lines of duplicated code for getting user name fields. * * See also {@link user_picture::unalias()} * * @param object $addtoobject Object to add user name fields to. * @param object $secondobject Object that contains user name field information. * @param string $prefix prefix to be added to all fields (including $additionalfields) e.g. authorfirstname. * @param array $additionalfields Additional fields to be matched with data in the second object. * The key can be set to the user table field name. * @return object User name fields. */ function username_load_fields_from_object($addtoobject, $secondobject, $prefix = null, $additionalfields = null) { $fields = []; foreach (\core_user\fields::get_name_fields() as $field) { $fields[$field] = $prefix . $field; } if ($additionalfields) { // Additional fields can specify their own 'alias' such as 'id' => 'userid'. This checks to see if // the key is a number and then sets the key to the array value. foreach ($additionalfields as $key => $value) { if (is_numeric($key)) { $additionalfields[$value] = $prefix . $value; unset($additionalfields[$key]); } else { $additionalfields[$key] = $prefix . $value; } } $fields = array_merge($fields, $additionalfields); } foreach ($fields as $key => $field) { // Important that we have all of the user name fields present in the object that we are sending back. $addtoobject->$key = ''; if (isset($secondobject->$field)) { $addtoobject->$key = $secondobject->$field; } } return $addtoobject; } /** * Returns an array of values in order of occurance in a provided string. * The key in the result is the character postion in the string. * * @param array $values Values to be found in the string format * @param string $stringformat The string which may contain values being searched for. * @return array An array of values in order according to placement in the string format. */ function order_in_string($values, $stringformat) { $valuearray = array(); foreach ($values as $value) { $pattern = "/$value\b/"; // Using preg_match as strpos() may match values that are similar e.g. firstname and firstnamephonetic. if (preg_match($pattern, $stringformat)) { $replacement = "thing"; // Replace the value with something more unique to ensure we get the right position when using strpos(). $newformat = preg_replace($pattern, $replacement, $stringformat); $position = strpos($newformat, $replacement); $valuearray[$position] = $value; } } ksort($valuearray); return $valuearray; } /** * Returns whether a given authentication plugin exists. * * @param string $auth Form of authentication to check for. Defaults to the global setting in {@link $CFG}. * @return boolean Whether the plugin is available. */ function exists_auth_plugin($auth) { global $CFG; if (file_exists("{$CFG->dirroot}/auth/$auth/auth.php")) { return is_readable("{$CFG->dirroot}/auth/$auth/auth.php"); } return false; } /** * Checks if a given plugin is in the list of enabled authentication plugins. * * @param string $auth Authentication plugin. * @return boolean Whether the plugin is enabled. */ function is_enabled_auth($auth) { if (empty($auth)) { return false; } $enabled = get_enabled_auth_plugins(); return in_array($auth, $enabled); } /** * Returns an authentication plugin instance. * * @param string $auth name of authentication plugin * @return auth_plugin_base An instance of the required authentication plugin. */ function get_auth_plugin($auth) { global $CFG; // Check the plugin exists first. if (! exists_auth_plugin($auth)) {
< print_error('authpluginnotfound', 'debug', '', $auth);
> throw new \moodle_exception('authpluginnotfound', 'debug', '', $auth);
} // Return auth plugin instance. require_once("{$CFG->dirroot}/auth/$auth/auth.php"); $class = "auth_plugin_$auth"; return new $class; } /** * Returns array of active auth plugins. * * @param bool $fix fix $CFG->auth if needed. Only set if logged in as admin. * @return array */ function get_enabled_auth_plugins($fix=false) { global $CFG; $default = array('manual', 'nologin'); if (empty($CFG->auth)) { $auths = array(); } else { $auths = explode(',', $CFG->auth); } $auths = array_unique($auths); $oldauthconfig = implode(',', $auths); foreach ($auths as $k => $authname) { if (in_array($authname, $default)) { // The manual and nologin plugin never need to be stored. unset($auths[$k]); } else if (!exists_auth_plugin($authname)) { debugging(get_string('authpluginnotfound', 'debug', $authname)); unset($auths[$k]); } } // Ideally only explicit interaction from a human admin should trigger a // change in auth config, see MDL-70424 for details. if ($fix) { $newconfig = implode(',', $auths); if (!isset($CFG->auth) or $newconfig != $CFG->auth) { add_to_config_log('auth', $oldauthconfig, $newconfig, 'core'); set_config('auth', $newconfig); } } return (array_merge($default, $auths)); } /** * Returns true if an internal authentication method is being used. * if method not specified then, global default is assumed * * @param string $auth Form of authentication required * @return bool */ function is_internal_auth($auth) { // Throws error if bad $auth. $authplugin = get_auth_plugin($auth); return $authplugin->is_internal(); } /** * Returns true if the user is a 'restored' one. * * Used in the login process to inform the user and allow him/her to reset the password * * @param string $username username to be checked * @return bool */ function is_restored_user($username) { global $CFG, $DB; return $DB->record_exists('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'password' => 'restored')); } /** * Returns an array of user fields * * @return array User field/column names */ function get_user_fieldnames() { global $DB; $fieldarray = $DB->get_columns('user'); unset($fieldarray['id']); $fieldarray = array_keys($fieldarray); return $fieldarray; } /** * Returns the string of the language for the new user. * * @return string language for the new user */ function get_newuser_language() { global $CFG, $SESSION; return (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang; } /** * Creates a bare-bones user record * * @todo Outline auth types and provide code example * * @param string $username New user's username to add to record * @param string $password New user's password to add to record * @param string $auth Form of authentication required * @return stdClass A complete user object */ function create_user_record($username, $password, $auth = 'manual') { global $CFG, $DB, $SESSION; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); // Just in case check text case. $username = trim(core_text::strtolower($username)); $authplugin = get_auth_plugin($auth); $customfields = $authplugin->get_custom_user_profile_fields(); $newuser = new stdClass(); if ($newinfo = $authplugin->get_userinfo($username)) { $newinfo = truncate_userinfo($newinfo); foreach ($newinfo as $key => $value) { if (in_array($key, $authplugin->userfields) || (in_array($key, $customfields))) { $newuser->$key = $value; } } } if (!empty($newuser->email)) { if (email_is_not_allowed($newuser->email)) { unset($newuser->email); } } $newuser->auth = $auth; $newuser->username = $username; // Fix for MDL-8480 // user CFG lang for user if $newuser->lang is empty // or $user->lang is not an installed language. if (empty($newuser->lang) || !get_string_manager()->translation_exists($newuser->lang)) { $newuser->lang = get_newuser_language(); } $newuser->confirmed = 1; $newuser->lastip = getremoteaddr(); $newuser->timecreated = time(); $newuser->timemodified = $newuser->timecreated; $newuser->mnethostid = $CFG->mnet_localhost_id; $newuser->id = user_create_user($newuser, false, false); // Save user profile data. profile_save_data($newuser); $user = get_complete_user_data('id', $newuser->id); if (!empty($CFG->{'auth_'.$newuser->auth.'_forcechangepassword'})) { set_user_preference('auth_forcepasswordchange', 1, $user); } // Set the password. update_internal_user_password($user, $password); // Trigger event. \core\event\user_created::create_from_userid($newuser->id)->trigger(); return $user; } /** * Will update a local user record from an external source (MNET users can not be updated using this method!). * * @param string $username user's username to update the record * @return stdClass A complete user object */ function update_user_record($username) { global $DB, $CFG; // Just in case check text case. $username = trim(core_text::strtolower($username)); $oldinfo = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id), '*', MUST_EXIST); return update_user_record_by_id($oldinfo->id); } /** * Will update a local user record from an external source (MNET users can not be updated using this method!). * * @param int $id user id * @return stdClass A complete user object */ function update_user_record_by_id($id) { global $DB, $CFG; require_once($CFG->dirroot."/user/profile/lib.php"); require_once($CFG->dirroot.'/user/lib.php'); $params = array('mnethostid' => $CFG->mnet_localhost_id, 'id' => $id, 'deleted' => 0); $oldinfo = $DB->get_record('user', $params, '*', MUST_EXIST); $newuser = array(); $userauth = get_auth_plugin($oldinfo->auth); if ($newinfo = $userauth->get_userinfo($oldinfo->username)) { $newinfo = truncate_userinfo($newinfo); $customfields = $userauth->get_custom_user_profile_fields(); foreach ($newinfo as $key => $value) { $iscustom = in_array($key, $customfields); if (!$iscustom) { $key = strtolower($key); } if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id' or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') { // Unknown or must not be changed. continue; } if (empty($userauth->config->{'field_updatelocal_' . $key}) || empty($userauth->config->{'field_lock_' . $key})) { continue; } $confval = $userauth->config->{'field_updatelocal_' . $key}; $lockval = $userauth->config->{'field_lock_' . $key}; if ($confval === 'onlogin') { // MDL-4207 Don't overwrite modified user profile values with // empty LDAP values when 'unlocked if empty' is set. The purpose // of the setting 'unlocked if empty' is to allow the user to fill // in a value for the selected field _if LDAP is giving // nothing_ for this field. Thus it makes sense to let this value // stand in until LDAP is giving a value for this field. if (!(empty($value) && $lockval === 'unlockedifempty')) { if ($iscustom || (in_array($key, $userauth->userfields) && ((string)$oldinfo->$key !== (string)$value))) { $newuser[$key] = (string)$value; } } } } if ($newuser) { $newuser['id'] = $oldinfo->id; $newuser['timemodified'] = time(); user_update_user((object) $newuser, false, false); // Save user profile data. profile_save_data((object) $newuser); // Trigger event. \core\event\user_updated::create_from_userid($newuser['id'])->trigger(); } } return get_complete_user_data('id', $oldinfo->id); } /** * Will truncate userinfo as it comes from auth_get_userinfo (from external auth) which may have large fields. * * @param array $info Array of user properties to truncate if needed * @return array The now truncated information that was passed in */ function truncate_userinfo(array $info) { // Define the limits. $limit = array( 'username' => 100, 'idnumber' => 255, 'firstname' => 100, 'lastname' => 100, 'email' => 100, 'phone1' => 20, 'phone2' => 20, 'institution' => 255, 'department' => 255, 'address' => 255, 'city' => 120, 'country' => 2, ); // Apply where needed. foreach (array_keys($info) as $key) { if (!empty($limit[$key])) { $info[$key] = trim(core_text::substr($info[$key], 0, $limit[$key])); } } return $info; } /** * Marks user deleted in internal user database and notifies the auth plugin. * Also unenrols user from all roles and does other cleanup. * * Any plugin that needs to purge user data should register the 'user_deleted' event. * * @param stdClass $user full user object before delete * @return boolean success * @throws coding_exception if invalid $user parameter detected */ function delete_user(stdClass $user) { global $CFG, $DB, $SESSION; require_once($CFG->libdir.'/grouplib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/message/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); // Make sure nobody sends bogus record type as parameter. if (!property_exists($user, 'id') or !property_exists($user, 'username')) { throw new coding_exception('Invalid $user parameter in delete_user() detected'); } // Better not trust the parameter and fetch the latest info this will be very expensive anyway. if (!$user = $DB->get_record('user', array('id' => $user->id))) { debugging('Attempt to delete unknown user account.'); return false; } // There must be always exactly one guest record, originally the guest account was identified by username only, // now we use $CFG->siteguest for performance reasons. if ($user->username === 'guest' or isguestuser($user)) { debugging('Guest user account can not be deleted.'); return false; } // Admin can be theoretically from different auth plugin, but we want to prevent deletion of internal accoutns only, // if anything goes wrong ppl may force somebody to be admin via config.php setting $CFG->siteadmins. if ($user->auth === 'manual' and is_siteadmin($user)) { debugging('Local administrator accounts can not be deleted.'); return false; } // Allow plugins to use this user object before we completely delete it. if ($pluginsfunction = get_plugins_with_function('pre_user_delete')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($user); } } } // Keep user record before updating it, as we have to pass this to user_deleted event. $olduser = clone $user; // Keep a copy of user context, we need it for event. $usercontext = context_user::instance($user->id);
> // Remove user from communication rooms immediately. // Delete all grades - backup is kept in grade_grades_history table. > if (core_communication\api::is_available()) { grade_user_delete($user->id); > foreach (enrol_get_users_courses($user->id) as $course) { > $communication = \core_communication\processor::load_by_instance( // TODO: remove from cohorts using standard API here. > context: \core\context\course::instance($course->id), > component: 'core_course', // Remove user tags. > instancetype: 'coursecommunication', core_tag_tag::remove_all_item_tags('core', 'user', $user->id); > instanceid: $course->id, > ); // Unconditionally unenrol from all courses. > if ($communication !== null) { enrol_user_delete($user); > $communication->get_room_user_provider()->remove_members_from_room([$user->id]); > $communication->delete_instance_user_mapping([$user->id]); // Unenrol from all roles in all contexts. > } // This might be slow but it is really needed - modules might do some extra cleanup! > } role_unassign_all(array('userid' => $user->id)); > } >
// Notify the competency subsystem. \core_competency\api::hook_user_deleted($user->id); // Now do a brute force cleanup. // Delete all user events and subscription events. $DB->delete_records_select('event', 'userid = :userid AND subscriptionid IS NOT NULL', ['userid' => $user->id]); // Now, delete all calendar subscription from the user. $DB->delete_records('event_subscriptions', ['userid' => $user->id]); // Remove from all cohorts. $DB->delete_records('cohort_members', array('userid' => $user->id)); // Remove from all groups. $DB->delete_records('groups_members', array('userid' => $user->id)); // Brute force unenrol from all courses. $DB->delete_records('user_enrolments', array('userid' => $user->id)); // Purge user preferences. $DB->delete_records('user_preferences', array('userid' => $user->id)); // Purge user extra profile info. $DB->delete_records('user_info_data', array('userid' => $user->id)); // Purge log of previous password hashes. $DB->delete_records('user_password_history', array('userid' => $user->id)); // Last course access not necessary either. $DB->delete_records('user_lastaccess', array('userid' => $user->id)); // Remove all user tokens. $DB->delete_records('external_tokens', array('userid' => $user->id)); // Unauthorise the user for all services. $DB->delete_records('external_services_users', array('userid' => $user->id)); // Remove users private keys. $DB->delete_records('user_private_key', array('userid' => $user->id)); // Remove users customised pages. $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1)); // Remove user's oauth2 refresh tokens, if present. $DB->delete_records('oauth2_refresh_token', array('userid' => $user->id)); // Delete user from $SESSION->bulk_users. if (isset($SESSION->bulk_users[$user->id])) { unset($SESSION->bulk_users[$user->id]); } // Force logout - may fail if file based sessions used, sorry. \core\session\manager::kill_user_sessions($user->id); // Generate username from email address, or a fake email. $delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid'; $deltime = time(); $deltimelength = core_text::strlen((string) $deltime); // Max username length is 100 chars. Select up to limit - (length of current time + 1 [period character]) from users email. $delname = clean_param($delemail, PARAM_USERNAME); $delname = core_text::substr($delname, 0, 100 - ($deltimelength + 1)) . ".{$deltime}"; // Workaround for bulk deletes of users with the same email address. while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here. $delname++; } // Mark internal user record as "deleted". $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->deleted = 1; $updateuser->username = $delname; // Remember it just in case. $updateuser->email = md5($user->username);// Store hash of username, useful importing/restoring users. $updateuser->idnumber = ''; // Clear this field to free it up. $updateuser->picture = 0; $updateuser->timemodified = $deltime; // Don't trigger update event, as user is being deleted. user_update_user($updateuser, false, false); // Delete all content associated with the user context, but not the context itself. $usercontext->delete_content(); // Delete any search data. \core_search\manager::context_deleted($usercontext); // Any plugin that needs to cleanup should register this event. // Trigger event. $event = \core\event\user_deleted::create( array( 'objectid' => $user->id, 'relateduserid' => $user->id, 'context' => $usercontext, 'other' => array( 'username' => $user->username, 'email' => $user->email, 'idnumber' => $user->idnumber, 'picture' => $user->picture, 'mnethostid' => $user->mnethostid ) ) ); $event->add_record_snapshot('user', $olduser); $event->trigger(); // We will update the user's timemodified, as it will be passed to the user_deleted event, which // should know about this updated property persisted to the user's table. $user->timemodified = $updateuser->timemodified; // Notify auth plugin - do not block the delete even when plugin fails. $authplugin = get_auth_plugin($user->auth); $authplugin->user_delete($user); return true; } /** * Retrieve the guest user object. * * @return stdClass A {@link $USER} object */ function guest_user() { global $CFG, $DB; if ($newuser = $DB->get_record('user', array('id' => $CFG->siteguest))) { $newuser->confirmed = 1; $newuser->lang = get_newuser_language(); $newuser->lastip = getremoteaddr(); } return $newuser; } /** * Authenticates a user against the chosen authentication mechanism * * Given a username and password, this function looks them * up using the currently selected authentication mechanism, * and if the authentication is successful, it returns a * valid $user object from the 'user' table. * * Uses auth_ functions from the currently active auth module * * After authenticate_user_login() returns success, you will need to * log that the user has logged in, and call complete_user_login() to set * the session up. * * Note: this function works only with non-mnet accounts! * * @param string $username User's username (or also email if $CFG->authloginviaemail enabled) * @param string $password User's password * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
< * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
> * @param string|bool $logintoken If this is set to a string it is validated against the login token for the session. > * @param string|bool $loginrecaptcha If this is set to a string it is validated against Google reCaptcha.
* @return stdClass|false A {@link $USER} object or false if error */
< function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) { < global $CFG, $DB, $PAGE;
> function authenticate_user_login( > $username, > $password, > $ignorelockout = false, > &$failurereason = null, > $logintoken = false, > string|bool $loginrecaptcha = false, > ) { > global $CFG, $DB, $PAGE, $SESSION;
require_once("$CFG->libdir/authlib.php"); if ($user = get_complete_user_data('username', $username, $CFG->mnet_localhost_id)) { // we have found the user } else if (!empty($CFG->authloginviaemail)) { if ($email = clean_param($username, PARAM_EMAIL)) { $select = "mnethostid = :mnethostid AND LOWER(email) = LOWER(:email) AND deleted = 0"; $params = array('mnethostid' => $CFG->mnet_localhost_id, 'email' => $email); $users = $DB->get_records_select('user', $select, $params, 'id', 'id', 0, 2); if (count($users) === 1) { // Use email for login only if unique. $user = reset($users); $user = get_complete_user_data('id', $user->id); $username = $user->username; } unset($users); } } // Make sure this request came from the login form. if (!\core\session\manager::validate_login_token($logintoken)) { $failurereason = AUTH_LOGIN_FAILED; // Trigger login failed event (specifying the ID of the found user, if available). \core\event\user_login_failed::create([ 'userid' => ($user->id ?? 0), 'other' => [ 'username' => $username, 'reason' => $failurereason, ], ])->trigger(); error_log('[client '.getremoteaddr()."] $CFG->wwwroot Invalid Login Token: $username ".$_SERVER['HTTP_USER_AGENT']); return false; }
> // Login reCaptcha. $authsenabled = get_enabled_auth_plugins(); > if (login_captcha_enabled() && !validate_login_captcha($loginrecaptcha)) { > $failurereason = AUTH_LOGIN_FAILED_RECAPTCHA; if ($user) { > // Trigger login failed event (specifying the ID of the found user, if available). // Use manual if auth not set. > \core\event\user_login_failed::create([ $auth = empty($user->auth) ? 'manual' : $user->auth; > 'userid' => ($user->id ?? 0), > 'other' => [ if (in_array($user->auth, $authsenabled)) { > 'username' => $username, $authplugin = get_auth_plugin($user->auth); > 'reason' => $failurereason, $authplugin->pre_user_login_hook($user); > ], } > ])->trigger(); > return false; if (!empty($user->suspended)) { > } $failurereason = AUTH_LOGIN_SUSPENDED; >
// Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } if ($auth=='nologin' or !is_enabled_auth($auth)) { // Legacy way to suspend user. $failurereason = AUTH_LOGIN_SUSPENDED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client '.getremoteaddr()."] $CFG->wwwroot Disabled Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } $auths = array($auth); } else { // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user(). if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 1))) { $failurereason = AUTH_LOGIN_NOUSER; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client '.getremoteaddr()."] $CFG->wwwroot Deleted Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } // User does not exist. $auths = $authsenabled; $user = new stdClass(); $user->id = 0; } if ($ignorelockout) { // Some other mechanism protects against brute force password guessing, for example login form might include reCAPTCHA // or this function is called from a SSO script. } else if ($user->id) { // Verify login lockout after other ways that may prevent user login. if (login_is_lockedout($user)) { $failurereason = AUTH_LOGIN_LOCKOUT; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client '.getremoteaddr()."] $CFG->wwwroot Login lockout: $username ".$_SERVER['HTTP_USER_AGENT']);
> $SESSION->loginerrormsg = get_string('accountlocked', 'admin'); return false; >
} } else { // We can not lockout non-existing accounts. } foreach ($auths as $auth) { $authplugin = get_auth_plugin($auth); // On auth fail fall through to the next plugin. if (!$authplugin->user_login($username, $password)) { continue; } // Before performing login actions, check if user still passes password policy, if admin setting is enabled. if (!empty($CFG->passwordpolicycheckonlogin)) { $errmsg = ''; $passed = check_password_policy($password, $errmsg, $user); if (!$passed) { // First trigger event for failure. $failedevent = \core\event\user_password_policy_failed::create_from_user($user); $failedevent->trigger(); // If able to change password, set flag and move on. if ($authplugin->can_change_password()) { // Check if we are on internal change password page, or service is external, don't show notification. $internalchangeurl = new moodle_url('/login/change_password.php'); if (!($PAGE->has_set_url() && $internalchangeurl->compare($PAGE->url)) && $authplugin->is_internal()) { \core\notification::error(get_string('passwordpolicynomatch', '', $errmsg)); } set_user_preference('auth_forcepasswordchange', 1, $user); } else if ($authplugin->can_reset_password()) { // Else force a reset if possible. \core\notification::error(get_string('forcepasswordresetnotice', '', $errmsg)); redirect(new moodle_url('/login/forgot_password.php')); } else { $notifymsg = get_string('forcepasswordresetfailurenotice', '', $errmsg); // If support page is set, add link for help. if (!empty($CFG->supportpage)) { $link = \html_writer::link($CFG->supportpage, $CFG->supportpage); $link = \html_writer::tag('p', $link); $notifymsg .= $link; } // If no change or reset is possible, add a notification for user. \core\notification::error($notifymsg); } } } // Successful authentication. if ($user->id) { // User already exists in database. if (empty($user->auth)) { // For some reason auth isn't set yet. $DB->set_field('user', 'auth', $auth, array('id' => $user->id)); $user->auth = $auth; } // If the existing hash is using an out-of-date algorithm (or the legacy md5 algorithm), then we should update to // the current hash algorithm while we have access to the user's password. update_internal_user_password($user, $password); if ($authplugin->is_synchronised_with_external()) { // Update user record from external DB. $user = update_user_record_by_id($user->id); } } else { // The user is authenticated but user creation may be disabled. if (!empty($CFG->authpreventaccountcreation)) { $failurereason = AUTH_LOGIN_UNAUTHORISED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client '.getremoteaddr()."] $CFG->wwwroot Unknown user, can not create new accounts: $username ". $_SERVER['HTTP_USER_AGENT']); return false; } else { $user = create_user_record($username, $password, $auth); } } $authplugin->sync_roles($user); foreach ($authsenabled as $hau) { $hauth = get_auth_plugin($hau); $hauth->user_authenticated_hook($user, $username, $password); } if (empty($user->id)) { $failurereason = AUTH_LOGIN_NOUSER; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); return false; } if (!empty($user->suspended)) { // Just in case some auth plugin suspended account. $failurereason = AUTH_LOGIN_SUSPENDED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); error_log('[client '.getremoteaddr()."] $CFG->wwwroot Suspended Login: $username ".$_SERVER['HTTP_USER_AGENT']); return false; } login_attempt_valid($user); $failurereason = AUTH_LOGIN_OK; return $user; } // Failed if all the plugins have failed. if (debugging('', DEBUG_ALL)) { error_log('[client '.getremoteaddr()."] $CFG->wwwroot Failed Login: $username ".$_SERVER['HTTP_USER_AGENT']); } if ($user->id) { login_attempt_failed($user); $failurereason = AUTH_LOGIN_FAILED; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('userid' => $user->id, 'other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); } else { $failurereason = AUTH_LOGIN_NOUSER; // Trigger login failed event. $event = \core\event\user_login_failed::create(array('other' => array('username' => $username, 'reason' => $failurereason))); $event->trigger(); } return false; } /** * Call to complete the user login process after authenticate_user_login() * has succeeded. It will setup the $USER variable and other required bits * and pieces. * * NOTE: * - It will NOT log anything -- up to the caller to decide what to log. * - this function does not set any cookies any more! * * @param stdClass $user
> * @param array $extrauserinfo
* @return stdClass A {@link $USER} object - BC only, do not use */
< function complete_user_login($user) {
> function complete_user_login($user, array $extrauserinfo = []) {
global $CFG, $DB, $USER, $SESSION; \core\session\manager::login_user($user); // Reload preferences from DB. unset($USER->preference); check_user_preferences_loaded($USER); // Update login times. update_user_login_times(); // Extra session prefs init. set_login_session_preferences(); // Trigger login event. $event = \core\event\user_loggedin::create( array( 'userid' => $USER->id, 'objectid' => $USER->id,
< 'other' => array('username' => $USER->username),
> 'other' => [ > 'username' => $USER->username, > 'extrauserinfo' => $extrauserinfo > ]
) ); $event->trigger();
> // Check if the user is using a new browser or session (a new MoodleSession cookie is set in that case). // Queue migrating the messaging data, if we need to. > // If the user is accessing from the same IP, ignore everything (most of the time will be a new session in the same browser). if (!get_user_preferences('core_message_migrate_data', false, $USER->id)) { > // Skip Web Service requests, CLI scripts, AJAX scripts, and request from the mobile app itself. // Check if there are any legacy messages to migrate. > $loginip = getremoteaddr(); if (\core_message\helper::legacy_messages_exist($USER->id)) { > $isnewip = isset($SESSION->userpreviousip) && $SESSION->userpreviousip != $loginip; \core_message\task\migrate_message_data::queue_task($USER->id); > $isvalidenv = (!WS_SERVER && !CLI_SCRIPT && !NO_MOODLE_COOKIES) || PHPUNIT_TEST; } else { > set_user_preference('core_message_migrate_data', true, $USER->id); > if (!empty($SESSION->isnewsessioncookie) && $isnewip && $isvalidenv && !\core_useragent::is_moodle_app()) { } > } > $logintime = time(); > $ismoodleapp = false; if (isguestuser()) { > $useragent = \core_useragent::get_user_agent_string(); // No need to continue when user is THE guest. > return $USER; > // Schedule adhoc task to sent a login notification to the user. } > $task = new \core\task\send_login_notifications(); > $task->set_userid($USER->id); if (CLI_SCRIPT) { > $task->set_custom_data(compact('ismoodleapp', 'useragent', 'loginip', 'logintime')); // We can redirect to password change URL only in browser. > $task->set_component('core'); return $USER; > \core\task\manager::queue_adhoc_task($task); } > } >
// Select password change url. $userauth = get_auth_plugin($USER->auth); // Check whether the user should be changing password. if (get_user_preferences('auth_forcepasswordchange', false)) { if ($userauth->can_change_password()) { if ($changeurl = $userauth->change_password_url()) { redirect($changeurl); } else { require_once($CFG->dirroot . '/login/lib.php'); $SESSION->wantsurl = core_login_get_return_url(); redirect($CFG->wwwroot.'/login/change_password.php'); } } else {
< print_error('nopasswordchangeforced', 'auth');
> throw new \moodle_exception('nopasswordchangeforced', 'auth');
} } return $USER; } /**
< * Check a password hash to see if it was hashed using the legacy hash algorithm (md5).
> * Check a password hash to see if it was hashed using the legacy hash algorithm (bcrypt).
* * @param string $password String to check.
< * @return boolean True if the $password matches the format of an md5 sum.
> * @return bool True if the $password matches the format of a bcrypt hash. > */ > function password_is_legacy_hash(#[\SensitiveParameter] string $password): bool { > return (bool) preg_match('/^\$2y\$[\d]{2}\$[A-Za-z0-9\.\/]{53}$/', $password); > } > > /** > * Calculate the Shannon entropy of a string. > * > * @param string $pepper The pepper to calculate the entropy of. > * @return float The Shannon entropy of the string.
*/
< function password_is_legacy_hash($password) { < return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
> function calculate_entropy(#[\SensitiveParameter] string $pepper): float { > // Initialize entropy. > $h = 0; > > // Calculate the length of the string. > $size = strlen($pepper); > > // For each unique character in the string. > foreach (count_chars($pepper, 1) as $v) { > // Calculate the probability of the character. > $p = $v / $size; > > // Add the character's contribution to the total entropy. > // This uses the formula for the entropy of a discrete random variable. > $h -= $p * log($p) / log(2); > } > > // Instead of returning the average entropy per symbol (Shannon entropy), > // we multiply by the length of the string to get total entropy. > return $h * $size; > } > > /** > * Get the available password peppers. > * The latest pepper is checked for minimum entropy as part of this function. > * We only calculate the entropy of the most recent pepper, > * because passwords are always updated to the latest pepper, > * and in the past we may have enforced a lower minimum entropy. > * Also, we allow the latest pepper to be empty, to allow admins to migrate off peppers. > * > * @return array The password peppers. > * @throws coding_exception If the entropy of the password pepper is less than the recommended minimum. > */ > function get_password_peppers(): array { > global $CFG; > > // Get all available peppers. > if (isset($CFG->passwordpeppers) && is_array($CFG->passwordpeppers)) { > // Sort the array in descending order of keys (numerical). > $peppers = $CFG->passwordpeppers; > krsort($peppers, SORT_NUMERIC); > } else { > $peppers = []; // Set an empty array if no peppers are found. > } > > // Check if the entropy of the most recent pepper is less than the minimum. > // Also, we allow the most recent pepper to be empty, to allow admins to migrate off peppers. > $lastpepper = reset($peppers); > if (!empty($peppers) && $lastpepper !== '' && calculate_entropy($lastpepper) < PEPPER_ENTROPY) { > throw new coding_exception( > 'password pepper below minimum', > 'The entropy of the password pepper is less than the recommended minimum.'); > } > return $peppers;
} /** * Compare password against hash stored in user object to determine if it is valid. * * If necessary it also updates the stored hash to the current format. * * @param stdClass $user (Password property may be updated). * @param string $password Plain text password. * @return bool True if password is valid. */
< function validate_internal_user_password($user, $password) { < global $CFG;
> function validate_internal_user_password(stdClass $user, #[\SensitiveParameter] string $password): bool { > > if (exceeds_password_length($password)) { > // Password cannot be more than MAX_PASSWORD_CHARACTERS characters. > return false; > }
if ($user->password === AUTH_PASSWORD_NOT_CACHED) { // Internal password is not used at all, it can not validate. return false; }
< // If hash isn't a legacy (md5) hash, validate using the library function. < if (!password_is_legacy_hash($user->password)) { < return password_verify($password, $user->password); < } < < // Otherwise we need to check for a legacy (md5) hash instead. If the hash < // is valid we can then update it to the new algorithm. < < $sitesalt = isset($CFG->passwordsaltmain) ? $CFG->passwordsaltmain : ''; < $validated = false; < < if ($user->password === md5($password.$sitesalt) < or $user->password === md5($password) < or $user->password === md5(addslashes($password).$sitesalt) < or $user->password === md5(addslashes($password))) { < // Note: we are intentionally using the addslashes() here because we < // need to accept old password hashes of passwords with magic quotes. < $validated = true; < < } else { < for ($i=1; $i<=20; $i++) { // 20 alternative salts should be enough, right? < $alt = 'passwordsaltalt'.$i; < if (!empty($CFG->$alt)) { < if ($user->password === md5($password.$CFG->$alt) or $user->password === md5(addslashes($password).$CFG->$alt)) { < $validated = true; < break; < } < } < }
> $peppers = get_password_peppers(); // Get the array of available peppers. > $islegacy = password_is_legacy_hash($user->password); // Check if the password is a legacy bcrypt hash. > > // If the password is a legacy hash, no peppers were used, so verify and update directly. > if ($islegacy && password_verify($password, $user->password)) { > update_internal_user_password($user, $password); > return true;
}
< if ($validated) { < // If the password matches the existing md5 hash, update to the < // current hash algorithm while we have access to the user's password.
> // If the password is not a legacy hash, iterate through the peppers. > $latestpepper = reset($peppers); > // Add an empty pepper to the beginning of the array. To make it easier to check if the password matches without any pepper. > $peppers = [-1 => ''] + $peppers; > foreach ($peppers as $pepper) { > $pepperedpassword = $password . $pepper; > > // If the peppered password is correct, update (if necessary) and return true. > if (password_verify($pepperedpassword, $user->password)) { > // If the pepper used is not the latest one, update the password. > if ($pepper !== $latestpepper) {
update_internal_user_password($user, $password); }
> return true; > } return $validated; > }
< return $validated;
> // If no peppered password was correct, the password is wrong. > return false;
/** * Calculate hash for a plain text password. * * @param string $password Plain text password to be hashed.
< * @param bool $fasthash If true, use a low cost factor when generating the hash < * This is much faster to generate but makes the hash < * less secure. It is used when lots of hashes need to < * be generated quickly.
> * @param bool $fasthash If true, use a low number of rounds when generating the hash > * This is faster to generate but makes the hash less secure. > * It is used when lots of hashes need to be generated quickly. > * @param int $pepperlength Lenght of the peppers
* @return string The hashed password. * * @throws moodle_exception If a problem occurs while generating the hash. */
< function hash_internal_user_password($password, $fasthash = false) { < global $CFG; < < // Set the cost factor to 4 for fast hashing, otherwise use default cost. < $options = ($fasthash) ? array('cost' => 4) : array(); < < $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);
> function hash_internal_user_password(#[\SensitiveParameter] string $password, $fasthash = false, $pepperlength = 0): string { > if (exceeds_password_length($password, $pepperlength)) { > // Password cannot be more than MAX_PASSWORD_CHARACTERS. > throw new \moodle_exception(get_string("passwordexceeded", 'error', MAX_PASSWORD_CHARACTERS)); > } > > // Set the cost factor to 5000 for fast hashing, otherwise use default cost. > $rounds = $fasthash ? 5000 : 10000; > > // First generate a cryptographically suitable salt. > $randombytes = random_bytes(16); > $salt = substr(strtr(base64_encode($randombytes), '+', '.'), 0, 16); > > // Now construct the password string with the salt and number of rounds. > // The password string is in the format $algorithm$rounds$salt$hash. ($6 is the SHA512 algorithm). > $generatedhash = crypt($password, implode('$', [ > '', > // The SHA512 Algorithm > '6', > "rounds={$rounds}", > $salt, > '', > ]));
if ($generatedhash === false || $generatedhash === null) { throw new moodle_exception('Failed to generate password hash.'); } return $generatedhash; } /** * Update password hash in user object (if necessary). * * The password is updated if: * 1. The password has changed (the hash of $user->password is different * to the hash of $password). * 2. The existing hash is using an out-of-date algorithm (or the legacy * md5 algorithm). *
> * The password is peppered with the latest pepper before hashing, * Updating the password will modify the $user object and the database > * if peppers are available.
* record to use the current hashing algorithm. * It will remove Web Services user tokens too. * * @param stdClass $user User object (password property may be updated). * @param string $password Plain text password. * @param bool $fasthash If true, use a low cost factor when generating the hash * This is much faster to generate but makes the hash * less secure. It is used when lots of hashes need to * be generated quickly. * @return bool Always returns true. */
< function update_internal_user_password($user, $password, $fasthash = false) {
> function update_internal_user_password( > stdClass $user, > #[\SensitiveParameter] string $password, > bool $fasthash = false > ): bool {
global $CFG, $DB;
> // Add the latest password pepper to the password before further processing. // Figure out what the hashed password should be. > $peppers = get_password_peppers(); if (!isset($user->auth)) { > if (!empty($peppers)) { debugging('User record in update_internal_user_password() must include field auth', > $password = $password . reset($peppers); DEBUG_DEVELOPER); > } $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id)); >
} $authplugin = get_auth_plugin($user->auth); if ($authplugin->prevent_local_passwords()) { $hashedpassword = AUTH_PASSWORD_NOT_CACHED; } else { $hashedpassword = hash_internal_user_password($password, $fasthash); } $algorithmchanged = false; if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) { // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED. $passwordchanged = ($user->password !== $hashedpassword); } else if (isset($user->password)) { // If verification fails then it means the password has changed. $passwordchanged = !password_verify($password, $user->password);
< $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
> $algorithmchanged = password_is_legacy_hash($user->password);
} else { // While creating new user, password in unset in $user object, to avoid // saving it with user_create() $passwordchanged = true; } if ($passwordchanged || $algorithmchanged) { $DB->set_field('user', 'password', $hashedpassword, array('id' => $user->id)); $user->password = $hashedpassword; // Trigger event. $user = $DB->get_record('user', array('id' => $user->id)); \core\event\user_password_updated::create_from_user($user)->trigger(); // Remove WS user tokens. if (!empty($CFG->passwordchangetokendeletion)) { require_once($CFG->dirroot.'/webservice/lib.php'); webservice::delete_user_ws_tokens($user->id); } } return true; } /** * Get a complete user record, which includes all the info in the user record. * * Intended for setting as $USER session variable * * @param string $field The user field to be checked for a given value. * @param string $value The value to match for $field. * @param int $mnethostid * @param bool $throwexception If true, it will throw an exception when there's no record found or when there are multiple records * found. Otherwise, it will just return false. * @return mixed False, or A {@link $USER} object. */ function get_complete_user_data($field, $value, $mnethostid = null, $throwexception = false) { global $CFG, $DB; if (!$field || !$value) { return false; } // Change the field to lowercase. $field = core_text::strtolower($field); // List of case insensitive fields. $caseinsensitivefields = ['email']; // Username input is forced to lowercase and should be case sensitive. if ($field == 'username') { $value = core_text::strtolower($value); } // Build the WHERE clause for an SQL query. $params = array('fieldval' => $value); // Do a case-insensitive query, if necessary. These are generally very expensive. The performance can be improved on some DBs // such as MySQL by pre-filtering users with accent-insensitive subselect. if (in_array($field, $caseinsensitivefields)) { $fieldselect = $DB->sql_equal($field, ':fieldval', false); $idsubselect = $DB->sql_equal($field, ':fieldval2', false, false); $params['fieldval2'] = $value; } else { $fieldselect = "$field = :fieldval"; $idsubselect = ''; } $constraints = "$fieldselect AND deleted <> 1"; // If we are loading user data based on anything other than id, // we must also restrict our search based on mnet host. if ($field != 'id') { if (empty($mnethostid)) { // If empty, we restrict to local users. $mnethostid = $CFG->mnet_localhost_id; } } if (!empty($mnethostid)) { $params['mnethostid'] = $mnethostid; $constraints .= " AND mnethostid = :mnethostid"; } if ($idsubselect) { $constraints .= " AND id IN (SELECT id FROM {user} WHERE {$idsubselect})"; } // Get all the basic user data. try { // Make sure that there's only a single record that matches our query. // For example, when fetching by email, multiple records might match the query as there's no guarantee that email addresses // are unique. Therefore we can't reliably tell whether the user profile data that we're fetching is the correct one. $user = $DB->get_record_select('user', $constraints, $params, '*', MUST_EXIST); } catch (dml_exception $exception) { if ($throwexception) { throw $exception; } else { // Return false when no records or multiple records were found. return false; } } // Get various settings and preferences. // Preload preference cache. check_user_preferences_loaded($user); // Load course enrolment related stuff. $user->lastcourseaccess = array(); // During last session. $user->currentcourseaccess = array(); // During current session. if ($lastaccesses = $DB->get_records('user_lastaccess', array('userid' => $user->id))) { foreach ($lastaccesses as $lastaccess) { $user->lastcourseaccess[$lastaccess->courseid] = $lastaccess->timeaccess; } }
< $sql = "SELECT g.id, g.courseid < FROM {groups} g, {groups_members} gm < WHERE gm.groupid=g.id AND gm.userid=?"; < < // This is a special hack to speedup calendar display. < $user->groupmember = array(); < if (!isguestuser($user)) { < if ($groups = $DB->get_records_sql($sql, array($user->id))) { < foreach ($groups as $group) { < if (!array_key_exists($group->courseid, $user->groupmember)) { < $user->groupmember[$group->courseid] = array(); < } < $user->groupmember[$group->courseid][$group->id] = $group->id; < } < } < } <
// Add cohort theme. if (!empty($CFG->allowcohortthemes)) { require_once($CFG->dirroot . '/cohort/lib.php'); if ($cohorttheme = cohort_get_user_cohort_theme($user->id)) { $user->cohorttheme = $cohorttheme; } } // Add the custom profile fields to the user record. $user->profile = array(); if (!isguestuser($user)) { require_once($CFG->dirroot.'/user/profile/lib.php'); profile_load_custom_fields($user); } // Rewrite some variables if necessary. if (!empty($user->description)) { // No need to cart all of it around. $user->description = true; } if (isguestuser($user)) { // Guest language always same as site. $user->lang = get_newuser_language(); // Name always in current language. $user->firstname = get_string('guestuser'); $user->lastname = ' '; } return $user; } /** * Validate a password against the configured password policy * * @param string $password the password to be checked against the password policy * @param string $errmsg the error message to display when the password doesn't comply with the policy. * @param stdClass $user the user object to perform password validation against. Defaults to null if not provided. * * @return bool true if the password is valid according to the policy. false otherwise. */ function check_password_policy($password, &$errmsg, $user = null) { global $CFG;
< if (!empty($CFG->passwordpolicy)) {
> if (!empty($CFG->passwordpolicy) && !isguestuser($user)) {
$errmsg = ''; if (core_text::strlen($password) < $CFG->minpasswordlength) { $errmsg .= '<div>'. get_string('errorminpasswordlength', 'auth', $CFG->minpasswordlength) .'</div>'; } if (preg_match_all('/[[:digit:]]/u', $password, $matches) < $CFG->minpassworddigits) { $errmsg .= '<div>'. get_string('errorminpassworddigits', 'auth', $CFG->minpassworddigits) .'</div>'; } if (preg_match_all('/[[:lower:]]/u', $password, $matches) < $CFG->minpasswordlower) { $errmsg .= '<div>'. get_string('errorminpasswordlower', 'auth', $CFG->minpasswordlower) .'</div>'; } if (preg_match_all('/[[:upper:]]/u', $password, $matches) < $CFG->minpasswordupper) { $errmsg .= '<div>'. get_string('errorminpasswordupper', 'auth', $CFG->minpasswordupper) .'</div>'; } if (preg_match_all('/[^[:upper:][:lower:][:digit:]]/u', $password, $matches) < $CFG->minpasswordnonalphanum) { $errmsg .= '<div>'. get_string('errorminpasswordnonalphanum', 'auth', $CFG->minpasswordnonalphanum) .'</div>'; } if (!check_consecutive_identical_characters($password, $CFG->maxconsecutiveidentchars)) { $errmsg .= '<div>'. get_string('errormaxconsecutiveidentchars', 'auth', $CFG->maxconsecutiveidentchars) .'</div>'; } // Fire any additional password policy functions from plugins. // Plugin functions should output an error message string or empty string for success. $pluginsfunction = get_plugins_with_function('check_password_policy'); foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginerr = $pluginfunction($password, $user); if ($pluginerr) { $errmsg .= '<div>'. $pluginerr .'</div>'; } } } } if ($errmsg == '') { return true; } else { return false; } } /** * When logging in, this function is run to set certain preferences for the current SESSION. */ function set_login_session_preferences() { global $SESSION; $SESSION->justloggedin = true; unset($SESSION->lang); unset($SESSION->forcelang); unset($SESSION->load_navigation_admin); } /** * Delete a course, including all related data from the database, and any associated files. * * @param mixed $courseorid The id of the course or course object to delete. * @param bool $showfeedback Whether to display notifications of each action the function performs. * @return bool true if all the removals succeeded. false if there were any failures. If this * method returns false, some of the removals will probably have succeeded, and others * failed, but you have no way of knowing which. */ function delete_course($courseorid, $showfeedback = true) {
< global $DB;
> global $DB, $CFG;
if (is_object($courseorid)) { $courseid = $courseorid->id; $course = $courseorid; } else { $courseid = $courseorid; if (!$course = $DB->get_record('course', array('id' => $courseid))) { return false; } } $context = context_course::instance($courseid); // Frontpage course can not be deleted!! if ($courseid == SITEID) { return false; } // Allow plugins to use this course before we completely delete it. if ($pluginsfunction = get_plugins_with_function('pre_course_delete')) { foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { $pluginfunction($course); } } } // Tell the search manager we are about to delete a course. This prevents us sending updates // for each individual context being deleted. \core_search\manager::course_deleting_start($courseid); $handler = core_course\customfield\course_handler::create(); $handler->delete_instance($courseid); // Make the course completely empty. remove_course_contents($courseid, $showfeedback);
> // Communication provider delete associated information. // Delete the course and related context instance. > $communication = \core_communication\api::load_by_instance( context_helper::delete_instance(CONTEXT_COURSE, $courseid); > $context, > 'core_course', $DB->delete_records("course", array("id" => $courseid)); > 'coursecommunication', $DB->delete_records("course_format_options", array("courseid" => $courseid)); > $course->id > ); // Reset all course related caches here. >
if (class_exists('format_base', false)) {
> // Update communication room membership of enrolled users. format_base::reset_course_cache($courseid); > require_once($CFG->libdir . '/enrollib.php'); } > $courseusers = enrol_get_course_users($courseid); > $enrolledusers = []; // Tell search that we have deleted the course so it can delete course data from the index. > \core_search\manager::course_deleting_finish($courseid); > foreach ($courseusers as $user) { > $enrolledusers[] = $user->id; // Trigger a course deleted event. > } $event = \core\event\course_deleted::create(array( > 'objectid' => $course->id, > $communication->remove_members_from_room($enrolledusers); 'context' => $context, > 'other' => array( > $communication->delete_room(); 'shortname' => $course->shortname, >
< if (class_exists('format_base', false)) { < format_base::reset_course_cache($courseid); < }
> core_courseformat\base::reset_course_cache($courseid);
)); $event->add_record_snapshot('course', $course); $event->trigger(); return true; } /** * Clear a course out completely, deleting all content but don't delete the course itself. * * This function does not verify any permissions. * * Please note this function also deletes all user enrolments, * enrolment instances and role assignments by default. * * $options: * - 'keep_roles_and_enrolments' - false by default * - 'keep_groups_and_groupings' - false by default * * @param int $courseid The id of the course that is being deleted * @param bool $showfeedback Whether to display notifications of each action the function performs. * @param array $options extra options * @return bool true if all the removals succeeded. false if there were any failures. If this * method returns false, some of the removals will probably have succeeded, and others * failed, but you have no way of knowing which. */ function remove_course_contents($courseid, $showfeedback = true, array $options = null) { global $CFG, $DB, $OUTPUT; require_once($CFG->libdir.'/badgeslib.php'); require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/questionlib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->dirroot.'/group/lib.php'); require_once($CFG->dirroot.'/comment/lib.php'); require_once($CFG->dirroot.'/rating/lib.php'); require_once($CFG->dirroot.'/notes/lib.php'); // Handle course badges. badges_handle_course_deletion($courseid); // NOTE: these concatenated strings are suboptimal, but it is just extra info... $strdeleted = get_string('deleted').' - '; // Some crazy wishlist of stuff we should skip during purging of course content. $options = (array)$options; $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $coursecontext = context_course::instance($courseid); $fs = get_file_storage(); // Delete course completion information, this has to be done before grades and enrols. $cc = new completion_info($course); $cc->clear_criteria(); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.get_string('completion', 'completion'), 'notifysuccess'); } // Remove all data from gradebook - this needs to be done before course modules // because while deleting this information, the system may need to reference // the course modules that own the grades. remove_course_grades($courseid, $showfeedback); remove_grade_letters($coursecontext, $showfeedback); // Delete course blocks in any all child contexts, // they may depend on modules so delete them first. $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. foreach ($childcontexts as $childcontext) { blocks_delete_all_for_context($childcontext->id); } unset($childcontexts); blocks_delete_all_for_context($coursecontext->id); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.get_string('type_block_plural', 'plugin'), 'notifysuccess'); } $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $courseid]); rebuild_course_cache($courseid, true); // Get the list of all modules that are properly installed. $allmodules = $DB->get_records_menu('modules', array(), '', 'name, id'); // Delete every instance of every module, // this has to be done before deleting of course level stuff. $locations = core_component::get_plugin_list('mod'); foreach ($locations as $modname => $moddir) { if ($modname === 'NEWMODULE') { continue; } if (array_key_exists($modname, $allmodules)) { $sql = "SELECT cm.*, m.id AS modinstance, m.name, '$modname' AS modname FROM {".$modname."} m LEFT JOIN {course_modules} cm ON cm.instance = m.id AND cm.module = :moduleid WHERE m.course = :courseid"; $instances = $DB->get_records_sql($sql, array('courseid' => $course->id, 'modulename' => $modname, 'moduleid' => $allmodules[$modname])); include_once("$moddir/lib.php"); // Shows php warning only if plugin defective. $moddelete = $modname .'_delete_instance'; // Delete everything connected to an instance. if ($instances) { foreach ($instances as $cm) { if ($cm->id) { // Delete activity context questions and question categories. question_delete_activity($cm); // Notify the competency subsystem. \core_competency\api::hook_course_module_deleted($cm); // Delete all tag instances associated with the instance of this module. core_tag_tag::delete_instances("mod_{$modname}", null, context_module::instance($cm->id)->id); core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); } if (function_exists($moddelete)) { // This purges all module data in related tables, extra user prefs, settings, etc. $moddelete($cm->modinstance); } else { // NOTE: we should not allow installation of modules with missing delete support! debugging("Defective module '$modname' detected when deleting course contents: missing function $moddelete()!"); $DB->delete_records($modname, array('id' => $cm->modinstance)); } if ($cm->id) { // Delete cm and its context - orphaned contexts are purged in cron in case of any race condition. context_helper::delete_instance(CONTEXT_MODULE, $cm->id); $DB->delete_records('course_modules_completion', ['coursemoduleid' => $cm->id]);
> $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
$DB->delete_records('course_modules', array('id' => $cm->id)); rebuild_course_cache($cm->course, true); } } } if ($instances and $showfeedback) { echo $OUTPUT->notification($strdeleted.get_string('pluginname', $modname), 'notifysuccess'); } } else { // Ooops, this module is not properly installed, force-delete it in the next block. } } // We have tried to delete everything the nice way - now let's force-delete any remaining module data. // Delete completion defaults. $DB->delete_records("course_completion_defaults", array("course" => $courseid)); // Remove all data from availability and completion tables that is associated // with course-modules belonging to this course. Note this is done even if the // features are not enabled now, in case they were enabled previously. $DB->delete_records_subquery('course_modules_completion', 'coursemoduleid', 'id', 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
> $DB->delete_records_subquery('course_modules_viewed', 'coursemoduleid', 'id', > 'SELECT id from {course_modules} WHERE course = ?', [$courseid]);
// Remove course-module data that has not been removed in modules' _delete_instance callbacks. $cms = $DB->get_records('course_modules', array('course' => $course->id)); $allmodulesbyid = array_flip($allmodules); foreach ($cms as $cm) { if (array_key_exists($cm->module, $allmodulesbyid)) { try { $DB->delete_records($allmodulesbyid[$cm->module], array('id' => $cm->instance)); } catch (Exception $e) { // Ignore weird or missing table problems. } } context_helper::delete_instance(CONTEXT_MODULE, $cm->id); $DB->delete_records('course_modules', array('id' => $cm->id)); rebuild_course_cache($cm->course, true); } if ($showfeedback) { echo $OUTPUT->notification($strdeleted.get_string('type_mod_plural', 'plugin'), 'notifysuccess'); } // Delete questions and question categories. question_delete_course($course); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.get_string('questions', 'question'), 'notifysuccess'); } // Delete content bank contents. $cb = new \core_contentbank\contentbank(); $cbdeleted = $cb->delete_contents($coursecontext); if ($showfeedback && $cbdeleted) { echo $OUTPUT->notification($strdeleted.get_string('contentbank', 'contentbank'), 'notifysuccess'); } // Make sure there are no subcontexts left - all valid blocks and modules should be already gone. $childcontexts = $coursecontext->get_child_contexts(); // Returns all subcontexts since 2.2. foreach ($childcontexts as $childcontext) { $childcontext->delete(); } unset($childcontexts); // Remove roles and enrolments by default. if (empty($options['keep_roles_and_enrolments'])) { // This hack is used in restore when deleting contents of existing course. // During restore, we should remove only enrolment related data that the user performing the restore has a // permission to remove. $userid = $options['userid'] ?? null; enrol_course_delete($course, $userid); role_unassign_all(array('contextid' => $coursecontext->id, 'component' => ''), true); if ($showfeedback) { echo $OUTPUT->notification($strdeleted.get_string('type_enrol_plural', 'plugin'), 'notifysuccess'); } } // Delete any groups, removing members and grouping/course links first. if (empty($options['keep_groups_and_groupings'])) { groups_delete_groupings($course->id, $showfeedback); groups_delete_groups($course->id, $showfeedback); } // Filters be gone! filter_delete_all_for_context($coursecontext->id); // Notes, you shall not pass! note_delete_all($course->id); // Die comments! comment::delete_comments($coursecontext->id); // Ratings are history too. $delopt = new stdclass(); $delopt->contextid = $coursecontext->id; $rm = new rating_manager(); $rm->delete_ratings($delopt); // Delete course tags. core_tag_tag::remove_all_item_tags('core', 'course', $course->id);
> // Give the course format the opportunity to remove its obscure data. // Notify the competency subsystem. > $format = course_get_format($course); \core_competency\api::hook_course_deleted($course); > $format->delete_format_data(); >
// Delete calendar events. $DB->delete_records('event', array('courseid' => $course->id)); $fs->delete_area_files($coursecontext->id, 'calendar'); // Delete all related records in other core tables that may have a courseid // This array stores the tables that need to be cleared, as // table_name => column_name that contains the course id. $tablestoclear = array( 'backup_courses' => 'courseid', // Scheduled backup stuff. 'user_lastaccess' => 'courseid', // User access info. ); foreach ($tablestoclear as $table => $col) { $DB->delete_records($table, array($col => $course->id)); } // Delete all course backup files. $fs->delete_area_files($coursecontext->id, 'backup'); // Cleanup course record - remove links to deleted stuff.
> // Do not wipe cacherev, as this course might be reused and we need to ensure that it keeps $oldcourse = new stdClass(); > // increasing.
$oldcourse->id = $course->id; $oldcourse->summary = '';
< $oldcourse->cacherev = 0;
$oldcourse->legacyfiles = 0; if (!empty($options['keep_groups_and_groupings'])) { $oldcourse->defaultgroupingid = 0; } $DB->update_record('course', $oldcourse); // Delete course sections. $DB->delete_records('course_sections', array('course' => $course->id)); // Delete legacy, section and any other course files. $fs->delete_area_files($coursecontext->id, 'course'); // Files from summary and section. // Delete all remaining stuff linked to context such as files, comments, ratings, etc. if (empty($options['keep_roles_and_enrolments']) and empty($options['keep_groups_and_groupings'])) { // Easy, do not delete the context itself... $coursecontext->delete_content(); } else { // Hack alert!!!! // We can not drop all context stuff because it would bork enrolments and roles, // there might be also files used by enrol plugins... } // Delete legacy files - just in case some files are still left there after conversion to new file api, // also some non-standard unsupported plugins may try to store something there. fulldelete($CFG->dataroot.'/'.$course->id); // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion. course_modinfo::purge_course_cache($courseid); // Trigger a course content deleted event. $event = \core\event\course_content_deleted::create(array( 'objectid' => $course->id, 'context' => $coursecontext, 'other' => array('shortname' => $course->shortname, 'fullname' => $course->fullname, 'options' => $options) // Passing this for legacy reasons. )); $event->add_record_snapshot('course', $course); $event->trigger(); return true; } /** * Change dates in module - used from course reset. * * @param string $modname forum, assignment, etc * @param array $fields array of date fields from mod table * @param int $timeshift time difference * @param int $courseid * @param int $modid (Optional) passed if specific mod instance in course needs to be updated. * @return bool success */ function shift_course_mod_dates($modname, $fields, $timeshift, $courseid, $modid = 0) { global $CFG, $DB; include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php'); $return = true; $params = array($timeshift, $courseid); foreach ($fields as $field) { $updatesql = "UPDATE {".$modname."} SET $field = $field + ? WHERE course=? AND $field<>0"; if ($modid) { $updatesql .= ' AND id=?'; $params[] = $modid; } $return = $DB->execute($updatesql, $params) && $return; } return $return; } /** * This function will empty a course of user data. * It will retain the activities and the structure of the course. * * @param object $data an object containing all the settings including courseid (without magic quotes) * @return array status array of array component, item, error */ function reset_course_userdata($data) { global $CFG, $DB; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); require_once($CFG->dirroot.'/group/lib.php'); $data->courseid = $data->id; $context = context_course::instance($data->courseid); $eventparams = array( 'context' => $context, 'courseid' => $data->id, 'other' => array( 'reset_options' => (array) $data ) ); $event = \core\event\course_reset_started::create($eventparams); $event->trigger(); // Calculate the time shift of dates. if (!empty($data->reset_start_date)) { // Time part of course startdate should be zero. $data->timeshift = $data->reset_start_date - usergetmidnight($data->reset_start_date_old); } else { $data->timeshift = 0; } // Result array: component, item, error. $status = array(); // Start the resetting. $componentstr = get_string('general'); // Move the course start time. if (!empty($data->reset_start_date) and $data->timeshift) { // Change course start data. $DB->set_field('course', 'startdate', $data->reset_start_date, array('id' => $data->courseid)); // Update all course and group events - do not move activity events. $updatesql = "UPDATE {event} SET timestart = timestart + ? WHERE courseid=? AND instance=0"; $DB->execute($updatesql, array($data->timeshift, $data->courseid)); // Update any date activity restrictions. if ($CFG->enableavailability) { \availability_date\condition::update_all_dates($data->courseid, $data->timeshift); } // Update completion expected dates. if ($CFG->enablecompletion) { $modinfo = get_fast_modinfo($data->courseid); $changed = false; foreach ($modinfo->get_cms() as $cm) { if ($cm->completion && !empty($cm->completionexpected)) { $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift, array('id' => $cm->id)); $changed = true; } } // Clear course cache if changes made. if ($changed) { rebuild_course_cache($data->courseid, true); } // Update course date completion criteria. \completion_criteria_date::update_date($data->courseid, $data->timeshift); } $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false); } if (!empty($data->reset_end_date)) { // If the user set a end date value respect it. $DB->set_field('course', 'enddate', $data->reset_end_date, array('id' => $data->courseid)); } else if ($data->timeshift > 0 && $data->reset_end_date_old) { // If there is a time shift apply it to the end date as well. $enddate = $data->reset_end_date_old + $data->timeshift; $DB->set_field('course', 'enddate', $enddate, array('id' => $data->courseid)); } if (!empty($data->reset_events)) { $DB->delete_records('event', array('courseid' => $data->courseid)); $status[] = array('component' => $componentstr, 'item' => get_string('deleteevents', 'calendar'), 'error' => false); } if (!empty($data->reset_notes)) { require_once($CFG->dirroot.'/notes/lib.php'); note_delete_all($data->courseid); $status[] = array('component' => $componentstr, 'item' => get_string('deletenotes', 'notes'), 'error' => false); } if (!empty($data->delete_blog_associations)) { require_once($CFG->dirroot.'/blog/lib.php'); blog_remove_associations_for_course($data->courseid); $status[] = array('component' => $componentstr, 'item' => get_string('deleteblogassociations', 'blog'), 'error' => false); } if (!empty($data->reset_completion)) { // Delete course and activity completion information. $course = $DB->get_record('course', array('id' => $data->courseid)); $cc = new completion_info($course); $cc->delete_all_completion_data(); $status[] = array('component' => $componentstr, 'item' => get_string('deletecompletiondata', 'completion'), 'error' => false); } if (!empty($data->reset_competency_ratings)) { \core_competency\api::hook_course_reset_competency_ratings($data->courseid); $status[] = array('component' => $componentstr, 'item' => get_string('deletecompetencyratings', 'core_competency'), 'error' => false); } $componentstr = get_string('roles'); if (!empty($data->reset_roles_overrides)) { $children = $context->get_child_contexts(); foreach ($children as $child) { $child->delete_capabilities(); } $context->delete_capabilities(); $status[] = array('component' => $componentstr, 'item' => get_string('deletecourseoverrides', 'role'), 'error' => false); } if (!empty($data->reset_roles_local)) { $children = $context->get_child_contexts(); foreach ($children as $child) { role_unassign_all(array('contextid' => $child->id)); } $status[] = array('component' => $componentstr, 'item' => get_string('deletelocalroles', 'role'), 'error' => false); } // First unenrol users - this cleans some of related user data too, such as forum subscriptions, tracking, etc. $data->unenrolled = array(); if (!empty($data->unenrol_users)) { $plugins = enrol_get_plugins(true); $instances = enrol_get_instances($data->courseid, true); foreach ($instances as $key => $instance) { if (!isset($plugins[$instance->enrol])) { unset($instances[$key]); continue; } } $usersroles = enrol_get_course_users_roles($data->courseid); foreach ($data->unenrol_users as $withroleid) { if ($withroleid) { $sql = "SELECT ue.* FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.roleid = :roleid AND ra.userid = ue.userid)"; $params = array('courseid' => $data->courseid, 'roleid' => $withroleid, 'courselevel' => CONTEXT_COURSE); } else { // Without any role assigned at course context. $sql = "SELECT ue.* FROM {user_enrolments} ue JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid) JOIN {context} c ON (c.contextlevel = :courselevel AND c.instanceid = e.courseid) LEFT JOIN {role_assignments} ra ON (ra.contextid = c.id AND ra.userid = ue.userid) WHERE ra.id IS null"; $params = array('courseid' => $data->courseid, 'courselevel' => CONTEXT_COURSE); } $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ue) { if (!isset($instances[$ue->enrolid])) { continue; } $instance = $instances[$ue->enrolid]; $plugin = $plugins[$instance->enrol]; if (!$plugin->allow_unenrol($instance) and !$plugin->allow_unenrol_user($instance, $ue)) { continue; } if ($withroleid && count($usersroles[$ue->userid]) > 1) { // If we don't remove all roles and user has more than one role, just remove this role. role_unassign($withroleid, $ue->userid, $context->id); unset($usersroles[$ue->userid][$withroleid]); } else { // If we remove all roles or user has only one role, unenrol user from course. $plugin->unenrol_user($instance, $ue->userid); } $data->unenrolled[$ue->userid] = $ue->userid; } $rs->close(); } } if (!empty($data->unenrolled)) { $status[] = array( 'component' => $componentstr, 'item' => get_string('unenrol', 'enrol').' ('.count($data->unenrolled).')', 'error' => false ); } $componentstr = get_string('groups'); // Remove all group members. if (!empty($data->reset_groups_members)) { groups_delete_group_members($data->courseid); $status[] = array('component' => $componentstr, 'item' => get_string('removegroupsmembers', 'group'), 'error' => false); } // Remove all groups. if (!empty($data->reset_groups_remove)) { groups_delete_groups($data->courseid, false); $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroups', 'group'), 'error' => false); } // Remove all grouping members. if (!empty($data->reset_groupings_members)) { groups_delete_groupings_groups($data->courseid, false); $status[] = array('component' => $componentstr, 'item' => get_string('removegroupingsmembers', 'group'), 'error' => false); } // Remove all groupings. if (!empty($data->reset_groupings_remove)) { groups_delete_groupings($data->courseid, false); $status[] = array('component' => $componentstr, 'item' => get_string('deleteallgroupings', 'group'), 'error' => false); } // Look in every instance of every module for data to delete. $unsupportedmods = array(); if ($allmods = $DB->get_records('modules') ) { foreach ($allmods as $mod) { $modname = $mod->name; $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php'; $moddeleteuserdata = $modname.'_reset_userdata'; // Function to delete user data. if (file_exists($modfile)) { if (!$DB->count_records($modname, array('course' => $data->courseid))) { continue; // Skip mods with no instances. } include_once($modfile); if (function_exists($moddeleteuserdata)) { $modstatus = $moddeleteuserdata($data); if (is_array($modstatus)) { $status = array_merge($status, $modstatus); } else { debugging('Module '.$modname.' returned incorrect staus - must be an array!'); } } else { $unsupportedmods[] = $mod; } } else { debugging('Missing lib.php in '.$modname.' module!'); } // Update calendar events for all modules. course_module_bulk_update_calendar_events($modname, $data->courseid); }
> // Purge the course cache after resetting course start date. MDL-76936 } > if ($data->timeshift) { > course_modinfo::purge_course_cache($data->courseid); // Mention unsupported mods. > }
if (!empty($unsupportedmods)) { foreach ($unsupportedmods as $mod) { $status[] = array( 'component' => get_string('modulenameplural', $mod->name), 'item' => '', 'error' => get_string('resetnotimplemented') ); } } $componentstr = get_string('gradebook', 'grades'); // Reset gradebook,. if (!empty($data->reset_gradebook_items)) { remove_course_grades($data->courseid, false); grade_grab_course_grades($data->courseid); grade_regrade_final_grades($data->courseid); $status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false); } else if (!empty($data->reset_gradebook_grades)) { grade_course_reset($data->courseid); $status[] = array('component' => $componentstr, 'item' => get_string('removeallcoursegrades', 'grades'), 'error' => false); } // Reset comments. if (!empty($data->reset_comments)) { require_once($CFG->dirroot.'/comment/lib.php'); comment::reset_course_page_comments($context); } $event = \core\event\course_reset_ended::create($eventparams); $event->trigger(); return $status; } /** * Generate an email processing address. * * @param int $modid * @param string $modargs * @return string Returns email processing address */ function generate_email_processing_address($modid, $modargs) { global $CFG; $header = $CFG->mailprefix . substr(base64_encode(pack('C', $modid)), 0, 2).$modargs; return $header . substr(md5($header.get_site_identifier()), 0, 16).'@'.$CFG->maildomain; } /** * ? * * @todo Finish documenting this function * * @param string $modargs * @param string $body Currently unused */ function moodle_process_email($modargs, $body) { global $DB; // The first char should be an unencoded letter. We'll take this as an action. switch ($modargs[0]) { case 'B': { // Bounce. list(, $userid) = unpack('V', base64_decode(substr($modargs, 1, 8))); if ($user = $DB->get_record("user", array('id' => $userid), "id,email")) { // Check the half md5 of their email. $md5check = substr(md5($user->email), 0, 16); if ($md5check == substr($modargs, -16)) { set_bounce_count($user); } // Else maybe they've already changed it? } } break; // Maybe more later? } } // CORRESPONDENCE. /** * Get mailer instance, enable buffering, flush buffer or disable buffering. * * @param string $action 'get', 'buffer', 'close' or 'flush' * @return moodle_phpmailer|null mailer instance if 'get' used or nothing */ function get_mailer($action='get') { global $CFG; /** @var moodle_phpmailer $mailer */ static $mailer = null; static $counter = 0; if (!isset($CFG->smtpmaxbulk)) { $CFG->smtpmaxbulk = 1; } if ($action == 'get') { $prevkeepalive = false; if (isset($mailer) and $mailer->Mailer == 'smtp') { if ($counter < $CFG->smtpmaxbulk and !$mailer->isError()) { $counter++; // Reset the mailer. $mailer->Priority = 3; $mailer->CharSet = 'UTF-8'; // Our default. $mailer->ContentType = "text/plain"; $mailer->Encoding = "8bit"; $mailer->From = "root@localhost"; $mailer->FromName = "Root User"; $mailer->Sender = ""; $mailer->Subject = ""; $mailer->Body = ""; $mailer->AltBody = ""; $mailer->ConfirmReadingTo = ""; $mailer->clearAllRecipients(); $mailer->clearReplyTos(); $mailer->clearAttachments(); $mailer->clearCustomHeaders(); return $mailer; } $prevkeepalive = $mailer->SMTPKeepAlive; get_mailer('flush'); } require_once($CFG->libdir.'/phpmailer/moodle_phpmailer.php'); $mailer = new moodle_phpmailer(); $counter = 1; if ($CFG->smtphosts == 'qmail') { // Use Qmail system. $mailer->isQmail(); } else if (empty($CFG->smtphosts)) { // Use PHP mail() = sendmail. $mailer->isMail(); } else { // Use SMTP directly. $mailer->isSMTP(); if (!empty($CFG->debugsmtp) && (!empty($CFG->debugdeveloper))) { $mailer->SMTPDebug = 3; } // Specify main and backup servers. $mailer->Host = $CFG->smtphosts; // Specify secure connection protocol. $mailer->SMTPSecure = $CFG->smtpsecure; // Use previous keepalive. $mailer->SMTPKeepAlive = $prevkeepalive; if ($CFG->smtpuser) { // Use SMTP authentication. $mailer->SMTPAuth = true; $mailer->Username = $CFG->smtpuser; $mailer->Password = $CFG->smtppass; } } return $mailer; } $nothing = null; // Keep smtp session open after sending. if ($action == 'buffer') { if (!empty($CFG->smtpmaxbulk)) { get_mailer('flush'); $m = get_mailer(); if ($m->Mailer == 'smtp') { $m->SMTPKeepAlive = true; } } return $nothing; } // Close smtp session, but continue buffering. if ($action == 'flush') { if (isset($mailer) and $mailer->Mailer == 'smtp') { if (!empty($mailer->SMTPDebug)) { echo '<pre>'."\n"; } $mailer->SmtpClose(); if (!empty($mailer->SMTPDebug)) { echo '</pre>'; } } return $nothing; } // Close smtp session, do not buffer anymore. if ($action == 'close') { if (isset($mailer) and $mailer->Mailer == 'smtp') { get_mailer('flush'); $mailer->SMTPKeepAlive = false; } $mailer = null; // Better force new instance. return $nothing; } } /** * A helper function to test for email diversion * * @param string $email * @return bool Returns true if the email should be diverted */ function email_should_be_diverted($email) { global $CFG; if (empty($CFG->divertallemailsto)) { return false; } if (empty($CFG->divertallemailsexcept)) { return true; } $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept, -1, PREG_SPLIT_NO_EMPTY)); foreach ($patterns as $pattern) {
< if (preg_match("/$pattern/", $email)) {
> if (preg_match("/{$pattern}/i", $email)) {
return false; } } return true; } /** * Generate a unique email Message-ID using the moodle domain and install path * * @param string $localpart An optional unique message id prefix. * @return string The formatted ID ready for appending to the email headers. */ function generate_email_messageid($localpart = null) { global $CFG; $urlinfo = parse_url($CFG->wwwroot); $base = '@' . $urlinfo['host']; // If multiple moodles are on the same domain we want to tell them // apart so we add the install path to the local part. This means // that the id local part should never contain a / character so // we can correctly parse the id to reassemble the wwwroot. if (isset($urlinfo['path'])) { $base = $urlinfo['path'] . $base; } if (empty($localpart)) { $localpart = uniqid('', true); } // Because we may have an option /installpath suffix to the local part // of the id we need to escape any / chars which are in the $localpart. $localpart = str_replace('/', '%2F', $localpart); return '<' . $localpart . $base . '>'; } /** * Send an email to a specified user * * @param stdClass $user A {@link $USER} object * @param stdClass $from A {@link $USER} object * @param string $subject plain text subject line of the email * @param string $messagetext plain text version of the message * @param string $messagehtml complete html version of the message (optional) * @param string $attachment a file on the filesystem, either relative to $CFG->dataroot or a full path to a file in one of * the following directories: $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir * @param string $attachname the name of the file (extension indicates MIME) * @param bool $usetrueaddress determines whether $from email address should * be sent out. Will be overruled by user profile setting for maildisplay * @param string $replyto Email address to reply to * @param string $replytoname Name of reply to recipient * @param int $wordwrapwidth custom word wrap width, default 79 * @return bool Returns true if mail was sent OK and false if there was an error. */ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', $attachment = '', $attachname = '', $usetrueaddress = true, $replyto = '', $replytoname = '', $wordwrapwidth = 79) { global $CFG, $PAGE, $SITE; if (empty($user) or empty($user->id)) { debugging('Can not send email to null user', DEBUG_DEVELOPER); return false; } if (empty($user->email)) { debugging('Can not send email to user without email: '.$user->id, DEBUG_DEVELOPER); return false; } if (!empty($user->deleted)) { debugging('Can not send email to deleted user: '.$user->id, DEBUG_DEVELOPER); return false; } if (defined('BEHAT_SITE_RUNNING')) { // Fake email sending in behat. return true; } if (!empty($CFG->noemailever)) { // Hidden setting for development sites, set in config.php if needed. debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); return true; } if (email_should_be_diverted($user->email)) { $subject = "[DIVERTED {$user->email}] $subject"; $user = clone($user); $user->email = $CFG->divertallemailsto; } // Skip mail to suspended users. if ((isset($user->auth) && $user->auth=='nologin') or (isset($user->suspended) && $user->suspended)) { return true; } if (!validate_email($user->email)) { // We can not send emails to invalid addresses - it might create security issue or confuse the mailer. debugging("email_to_user: User $user->id (".fullname($user).") email ($user->email) is invalid! Not sending."); return false; } if (over_bounce_threshold($user)) { debugging("email_to_user: User $user->id (".fullname($user).") is over bounce threshold! Not sending."); return false; } // TLD .invalid is specifically reserved for invalid domain names. // For More information, see {@link http://tools.ietf.org/html/rfc2606#section-2}. if (substr($user->email, -8) == '.invalid') { debugging("email_to_user: User $user->id (".fullname($user).") email domain ($user->email) is invalid! Not sending."); return true; // This is not an error. } // If the user is a remote mnet user, parse the email text for URL to the // wwwroot and modify the url to direct the user's browser to login at their // home site (identity provider - idp) before hitting the link itself. if (is_mnet_remote_user($user)) { require_once($CFG->dirroot.'/mnet/lib.php'); $jumpurl = mnet_get_idp_jump_url($user); $callback = partial('mnet_sso_apply_indirection', $jumpurl); $messagetext = preg_replace_callback("%($CFG->wwwroot[^[:space:]]*)%", $callback, $messagetext); $messagehtml = preg_replace_callback("%href=[\"'`]($CFG->wwwroot[\w_:\?=#&@/;.~-]*)[\"'`]%", $callback, $messagehtml); } $mail = get_mailer(); if (!empty($mail->SMTPDebug)) { echo '<pre>' . "\n"; } $temprecipients = array(); $tempreplyto = array(); // Make sure that we fall back onto some reasonable no-reply address. $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot); $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress; if (!validate_email($noreplyaddress)) { debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress)); $noreplyaddress = $noreplyaddressdefault; } // Make up an email address for handling bounces. if (!empty($CFG->handlebounces)) { $modargs = 'B'.base64_encode(pack('V', $user->id)).substr(md5($user->email), 0, 16); $mail->Sender = generate_email_processing_address(0, $modargs); } else { $mail->Sender = $noreplyaddress; } // Make sure that the explicit replyto is valid, fall back to the implicit one. if (!empty($replyto) && !validate_email($replyto)) { debugging('email_to_user: Invalid replyto-email '.s($replyto)); $replyto = $noreplyaddress; } if (is_string($from)) { // So we can pass whatever we want if there is need. $mail->From = $noreplyaddress; $mail->FromName = $from; // Check if using the true address is true, and the email is in the list of allowed domains for sending email, // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled // in a course with the sender. } else if ($usetrueaddress && can_send_from_real_email_address($from, $user)) { if (!validate_email($from->email)) { debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending'); // Better not to use $noreplyaddress in this case. return false; } $mail->From = $from->email; $fromdetails = new stdClass(); $fromdetails->name = fullname($from); $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); $fromdetails->siteshortname = format_string($SITE->shortname); $fromstring = $fromdetails->name; if ($CFG->emailfromvia == EMAIL_VIA_ALWAYS) { $fromstring = get_string('emailvia', 'core', $fromdetails); } $mail->FromName = $fromstring; if (empty($replyto)) { $tempreplyto[] = array($from->email, fullname($from)); } } else { $mail->From = $noreplyaddress; $fromdetails = new stdClass(); $fromdetails->name = fullname($from); $fromdetails->url = preg_replace('#^https?://#', '', $CFG->wwwroot); $fromdetails->siteshortname = format_string($SITE->shortname); $fromstring = $fromdetails->name; if ($CFG->emailfromvia != EMAIL_VIA_NEVER) { $fromstring = get_string('emailvia', 'core', $fromdetails); } $mail->FromName = $fromstring; if (empty($replyto)) { $tempreplyto[] = array($noreplyaddress, get_string('noreplyname')); } } if (!empty($replyto)) { $tempreplyto[] = array($replyto, $replytoname); } $temprecipients[] = array($user->email, fullname($user)); // Set word wrap. $mail->WordWrap = $wordwrapwidth; if (!empty($from->customheaders)) { // Add custom headers. if (is_array($from->customheaders)) { foreach ($from->customheaders as $customheader) { $mail->addCustomHeader($customheader); } } else { $mail->addCustomHeader($from->customheaders); } } // If the X-PHP-Originating-Script email header is on then also add an additional // header with details of where exactly in moodle the email was triggered from, // either a call to message_send() or to email_to_user(). if (ini_get('mail.add_x_header')) { $stack = debug_backtrace(false); $origin = $stack[0]; foreach ($stack as $depth => $call) { if ($call['function'] == 'message_send') { $origin = $call; } } $originheader = $CFG->wwwroot . ' => ' . gethostname() . ':' . str_replace($CFG->dirroot . '/', '', $origin['file']) . ':' . $origin['line']; $mail->addCustomHeader('X-Moodle-Originating-Script: ' . $originheader); } if (!empty($CFG->emailheaders)) { $headers = array_map('trim', explode("\n", $CFG->emailheaders)); foreach ($headers as $header) { if (!empty($header)) { $mail->addCustomHeader($header); } } } if (!empty($from->priority)) { $mail->Priority = $from->priority; } $renderer = $PAGE->get_renderer('core'); $context = array( 'sitefullname' => $SITE->fullname, 'siteshortname' => $SITE->shortname, 'sitewwwroot' => $CFG->wwwroot, 'subject' => $subject, 'prefix' => $CFG->emailsubjectprefix, 'to' => $user->email, 'toname' => fullname($user), 'from' => $mail->From, 'fromname' => $mail->FromName, ); if (!empty($tempreplyto[0])) { $context['replyto'] = $tempreplyto[0][0]; $context['replytoname'] = $tempreplyto[0][1]; } if ($user->id > 0) { $context['touserid'] = $user->id; $context['tousername'] = $user->username; } if (!empty($user->mailformat) && $user->mailformat == 1) { // Only process html templates if the user preferences allow html email. if (!$messagehtml) { // If no html has been given, BUT there is an html wrapping template then // auto convert the text to html and then wrap it. $messagehtml = trim(text_to_html($messagetext)); } $context['body'] = $messagehtml; $messagehtml = $renderer->render_from_template('core/email_html', $context); } $context['body'] = html_to_text(nl2br($messagetext)); $mail->Subject = $renderer->render_from_template('core/email_subject', $context); $mail->FromName = $renderer->render_from_template('core/email_fromname', $context); $messagetext = $renderer->render_from_template('core/email_text', $context); // Autogenerate a MessageID if it's missing. if (empty($mail->MessageID)) { $mail->MessageID = generate_email_messageid(); } if ($messagehtml && !empty($user->mailformat) && $user->mailformat == 1) { // Don't ever send HTML to users who don't want it. $mail->isHTML(true); $mail->Encoding = 'quoted-printable'; $mail->Body = $messagehtml; $mail->AltBody = "\n$messagetext\n"; } else { $mail->IsHTML(false); $mail->Body = "\n$messagetext\n"; } if ($attachment && $attachname) { if (preg_match( "~\\.\\.~" , $attachment )) { // Security check for ".." in dir path. $supportuser = core_user::get_support_user(); $temprecipients[] = array($supportuser->email, fullname($supportuser, true)); $mail->addStringAttachment('Error in attachment. User attempted to attach a filename with a unsafe name.', 'error.txt', '8bit', 'text/plain'); } else { require_once($CFG->libdir.'/filelib.php'); $mimetype = mimeinfo('type', $attachname); // Before doing the comparison, make sure that the paths are correct (Windows uses slashes in the other direction). // The absolute (real) path is also fetched to ensure that comparisons to allowed paths are compared equally. $attachpath = str_replace('\\', '/', realpath($attachment)); // Build an array of all filepaths from which attachments can be added (normalised slashes, absolute/real path). $allowedpaths = array_map(function(string $path): string { return str_replace('\\', '/', realpath($path)); }, [ $CFG->cachedir, $CFG->dataroot, $CFG->dirroot, $CFG->localcachedir, $CFG->tempdir, $CFG->localrequestdir, ]); // Set addpath to true. $addpath = true; // Check if attachment includes one of the allowed paths. foreach (array_filter($allowedpaths) as $allowedpath) { // Set addpath to false if the attachment includes one of the allowed paths. if (strpos($attachpath, $allowedpath) === 0) { $addpath = false; break; } } // If the attachment is a full path to a file in the multiple allowed paths, use it as is, // otherwise assume it is a relative path from the dataroot (for backwards compatibility reasons). if ($addpath == true) { $attachment = $CFG->dataroot . '/' . $attachment; } $mail->addAttachment($attachment, $attachname, 'base64', $mimetype); } } // Check if the email should be sent in an other charset then the default UTF-8. if ((!empty($CFG->sitemailcharset) || !empty($CFG->allowusermailcharset))) { // Use the defined site mail charset or eventually the one preferred by the recipient. $charset = $CFG->sitemailcharset; if (!empty($CFG->allowusermailcharset)) { if ($useremailcharset = get_user_preferences('mailcharset', '0', $user->id)) { $charset = $useremailcharset; } } // Convert all the necessary strings if the charset is supported. $charsets = get_list_of_charsets(); unset($charsets['UTF-8']); if (in_array($charset, $charsets)) { $mail->CharSet = $charset; $mail->FromName = core_text::convert($mail->FromName, 'utf-8', strtolower($charset)); $mail->Subject = core_text::convert($mail->Subject, 'utf-8', strtolower($charset)); $mail->Body = core_text::convert($mail->Body, 'utf-8', strtolower($charset)); $mail->AltBody = core_text::convert($mail->AltBody, 'utf-8', strtolower($charset)); foreach ($temprecipients as $key => $values) { $temprecipients[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); } foreach ($tempreplyto as $key => $values) { $tempreplyto[$key][1] = core_text::convert($values[1], 'utf-8', strtolower($charset)); } } } foreach ($temprecipients as $values) { $mail->addAddress($values[0], $values[1]); } foreach ($tempreplyto as $values) { $mail->addReplyTo($values[0], $values[1]); } if (!empty($CFG->emaildkimselector)) { $domain = substr(strrchr($mail->From, "@"), 1); $pempath = "{$CFG->dataroot}/dkim/{$domain}/{$CFG->emaildkimselector}.private"; if (file_exists($pempath)) { $mail->DKIM_domain = $domain; $mail->DKIM_private = $pempath; $mail->DKIM_selector = $CFG->emaildkimselector; $mail->DKIM_identity = $mail->From; } else { debugging("Email DKIM selector chosen due to {$mail->From} but no certificate found at $pempath", DEBUG_DEVELOPER); } } if ($mail->send()) { set_send_count($user); if (!empty($mail->SMTPDebug)) { echo '</pre>'; } return true; } else { // Trigger event for failing to send email. $event = \core\event\email_failed::create(array( 'context' => context_system::instance(), 'userid' => $from->id, 'relateduserid' => $user->id, 'other' => array( 'subject' => $subject, 'message' => $messagetext, 'errorinfo' => $mail->ErrorInfo ) )); $event->trigger(); if (CLI_SCRIPT) { mtrace('Error: lib/moodlelib.php email_to_user(): '.$mail->ErrorInfo); } if (!empty($mail->SMTPDebug)) { echo '</pre>'; } return false; } } /** * Check to see if a user's real email address should be used for the "From" field. * * @param object $from The user object for the user we are sending the email from. * @param object $user The user object that we are sending the email to. * @param array $unused No longer used. * @return bool Returns true if we can use the from user's email adress in the "From" field. */ function can_send_from_real_email_address($from, $user, $unused = null) { global $CFG; if (!isset($CFG->allowedemaildomains) || empty(trim($CFG->allowedemaildomains))) { return false; } $alloweddomains = array_map('trim', explode("\n", $CFG->allowedemaildomains)); // Email is in the list of allowed domains for sending email, // and the senders email setting is either displayed to everyone, or display to only other users that are enrolled // in a course with the sender. if (\core\ip_utils::is_domain_in_allowed_list(substr($from->email, strpos($from->email, '@') + 1), $alloweddomains) && ($from->maildisplay == core_user::MAILDISPLAY_EVERYONE || ($from->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY && enrol_get_shared_courses($user, $from, false, true)))) { return true; } return false; } /** * Generate a signoff for emails based on support settings * * @return string */ function generate_email_signoff() {
< global $CFG;
> global $CFG, $OUTPUT;
$signoff = "\n"; if (!empty($CFG->supportname)) { $signoff .= $CFG->supportname."\n"; }
< if (!empty($CFG->supportemail)) { < $signoff .= $CFG->supportemail."\n"; < } < if (!empty($CFG->supportpage)) { < $signoff .= $CFG->supportpage."\n";
> > $supportemail = $OUTPUT->supportemail(['class' => 'font-weight-bold']); > > if ($supportemail) { > $signoff .= "\n" . $supportemail . "\n";
}
>
return $signoff; } /** * Sets specified user's password and send the new password to the user via email. * * @param stdClass $user A {@link $USER} object * @param bool $fasthash If true, use a low cost factor when generating the hash for speed. * @return bool|string Returns "true" if mail was sent OK and "false" if there was an error */ function setnew_password_and_mail($user, $fasthash = false) { global $CFG, $DB; // We try to send the mail in language the user understands, // unfortunately the filter_string() does not support alternative langs yet // so multilang will not work properly for site->fullname. $lang = empty($user->lang) ? get_newuser_language() : $user->lang; $site = get_site(); $supportuser = core_user::get_support_user(); $newpassword = generate_password(); update_internal_user_password($user, $newpassword, $fasthash); $a = new stdClass(); $a->firstname = fullname($user, true); $a->sitename = format_string($site->fullname); $a->username = $user->username; $a->newpassword = $newpassword; $a->link = $CFG->wwwroot .'/login/?lang='.$lang; $a->signoff = generate_email_signoff(); $message = (string)new lang_string('newusernewpasswordtext', '', $a, $lang); $subject = format_string($site->fullname) .': '. (string)new lang_string('newusernewpasswordsubj', '', $a, $lang); // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message); } /** * Resets specified user's password and send the new password to the user via email. * * @param stdClass $user A {@link $USER} object * @return bool Returns true if mail was sent OK and false if there was an error. */ function reset_password_and_mail($user) { global $CFG; $site = get_site(); $supportuser = core_user::get_support_user(); $userauth = get_auth_plugin($user->auth); if (!$userauth->can_reset_password() or !is_enabled_auth($user->auth)) { trigger_error("Attempt to reset user password for user $user->username with Auth $user->auth."); return false; } $newpassword = generate_password(); if (!$userauth->user_update_password($user, $newpassword)) {
< print_error("cannotsetpassword");
> throw new \moodle_exception("cannotsetpassword");
} $a = new stdClass(); $a->firstname = $user->firstname; $a->lastname = $user->lastname; $a->sitename = format_string($site->fullname); $a->username = $user->username; $a->newpassword = $newpassword; $a->link = $CFG->wwwroot .'/login/change_password.php'; $a->signoff = generate_email_signoff(); $message = get_string('newpasswordtext', '', $a); $subject = format_string($site->fullname) .': '. get_string('changedpassword'); unset_user_preference('create_password', $user); // Prevent cron from generating the password. // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message); } /** * Send email to specified user with confirmation text and activation link. * * @param stdClass $user A {@link $USER} object * @param string $confirmationurl user confirmation URL * @return bool Returns true if mail was sent OK and false if there was an error. */ function send_confirmation_email($user, $confirmationurl = null) { global $CFG; $site = get_site(); $supportuser = core_user::get_support_user(); $data = new stdClass(); $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $subject = get_string('emailconfirmationsubject', '', format_string($site->fullname)); if (empty($confirmationurl)) { $confirmationurl = '/login/confirm.php'; } $confirmationurl = new moodle_url($confirmationurl); // Remove data parameter just in case it was included in the confirmation so we can add it manually later. $confirmationurl->remove_params('data'); $confirmationpath = $confirmationurl->out(false); // We need to custom encode the username to include trailing dots in the link. // Because of this custom encoding we can't use moodle_url directly. // Determine if a query string is present in the confirmation url. $hasquerystring = strpos($confirmationpath, '?') !== false; // Perform normal url encoding of the username first. $username = urlencode($user->username); // Prevent problems with trailing dots not being included as part of link in some mail clients. $username = str_replace('.', '%2E', $username); $data->link = $confirmationpath . ( $hasquerystring ? '&' : '?') . 'data='. $user->secret .'/'. $username; $message = get_string('emailconfirmation', '', $data); $messagehtml = text_to_html(get_string('emailconfirmation', '', $data), false, false, true); // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message, $messagehtml); } /** * Sends a password change confirmation email. * * @param stdClass $user A {@link $USER} object * @param stdClass $resetrecord An object tracking metadata regarding password reset request * @return bool Returns true if mail was sent OK and false if there was an error. */ function send_password_change_confirmation_email($user, $resetrecord) { global $CFG; $site = get_site(); $supportuser = core_user::get_support_user(); $pwresetmins = isset($CFG->pwresettime) ? floor($CFG->pwresettime / MINSECS) : 30; $data = new stdClass(); $data->firstname = $user->firstname; $data->lastname = $user->lastname; $data->username = $user->username; $data->sitename = format_string($site->fullname); $data->link = $CFG->wwwroot .'/login/forgot_password.php?token='. $resetrecord->token; $data->admin = generate_email_signoff(); $data->resetminutes = $pwresetmins; $message = get_string('emailresetconfirmation', '', $data); $subject = get_string('emailresetconfirmationsubject', '', format_string($site->fullname)); // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message); } /** * Sends an email containing information on how to change your password. * * @param stdClass $user A {@link $USER} object * @return bool Returns true if mail was sent OK and false if there was an error. */ function send_password_change_info($user) { $site = get_site(); $supportuser = core_user::get_support_user(); $data = new stdClass(); $data->firstname = $user->firstname; $data->lastname = $user->lastname; $data->username = $user->username; $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); if (!is_enabled_auth($user->auth)) { $message = get_string('emailpasswordchangeinfodisabled', '', $data); $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message); } $userauth = get_auth_plugin($user->auth); ['subject' => $subject, 'message' => $message] = $userauth->get_password_change_info($user); // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message); } /** * Check that an email is allowed. It returns an error message if there was a problem. * * @param string $email Content of email * @return string|false */ function email_is_not_allowed($email) { global $CFG; // Comparing lowercase domains. $email = strtolower($email); if (!empty($CFG->allowemailaddresses)) { $allowed = explode(' ', strtolower($CFG->allowemailaddresses)); foreach ($allowed as $allowedpattern) { $allowedpattern = trim($allowedpattern); if (!$allowedpattern) { continue; } if (strpos($allowedpattern, '.') === 0) { if (strpos(strrev($email), strrev($allowedpattern)) === 0) { // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". return false; } } else if (strpos(strrev($email), strrev('@'.$allowedpattern)) === 0) { return false; } } return get_string('emailonlyallowed', '', $CFG->allowemailaddresses); } else if (!empty($CFG->denyemailaddresses)) { $denied = explode(' ', strtolower($CFG->denyemailaddresses)); foreach ($denied as $deniedpattern) { $deniedpattern = trim($deniedpattern); if (!$deniedpattern) { continue; } if (strpos($deniedpattern, '.') === 0) { if (strpos(strrev($email), strrev($deniedpattern)) === 0) { // Subdomains are in a form ".example.com" - matches "xxx@anything.example.com". return get_string('emailnotallowed', '', $CFG->denyemailaddresses); } } else if (strpos(strrev($email), strrev('@'.$deniedpattern)) === 0) { return get_string('emailnotallowed', '', $CFG->denyemailaddresses); } } } return false; } // FILE HANDLING. /** * Returns local file storage instance * * @return file_storage */ function get_file_storage($reset = false) { global $CFG; static $fs = null; if ($reset) { $fs = null; return; } if ($fs) { return $fs; } require_once("$CFG->libdir/filelib.php"); $fs = new file_storage(); return $fs; } /** * Returns local file storage instance * * @return file_browser */ function get_file_browser() { global $CFG; static $fb = null; if ($fb) { return $fb; } require_once("$CFG->libdir/filelib.php"); $fb = new file_browser(); return $fb; } /** * Returns file packer * * @param string $mimetype default application/zip * @return file_packer */ function get_file_packer($mimetype='application/zip') { global $CFG; static $fp = array(); if (isset($fp[$mimetype])) { return $fp[$mimetype]; } switch ($mimetype) { case 'application/zip': case 'application/vnd.moodle.profiling': $classname = 'zip_packer'; break; case 'application/x-gzip' : $classname = 'tgz_packer'; break; case 'application/vnd.moodle.backup': $classname = 'mbz_packer'; break; default: return false; } require_once("$CFG->libdir/filestorage/$classname.php"); $fp[$mimetype] = new $classname(); return $fp[$mimetype]; } /** * Returns current name of file on disk if it exists. * * @param string $newfile File to be verified * @return string Current name of file on disk if true */ function valid_uploaded_file($newfile) { if (empty($newfile)) { return ''; } if (is_uploaded_file($newfile['tmp_name']) and $newfile['size'] > 0) { return $newfile['tmp_name']; } else { return ''; } } /** * Returns the maximum size for uploading files. * * There are seven possible upload limits: * 1. in Apache using LimitRequestBody (no way of checking or changing this) * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP) * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP) * 4. in php.ini for 'post_max_size' (can not be changed inside PHP) * 5. by the Moodle admin in $CFG->maxbytes * 6. by the teacher in the current course $course->maxbytes * 7. by the teacher for the current module, eg $assignment->maxbytes * * These last two are passed to this function as arguments (in bytes). * Anything defined as 0 is ignored. * The smallest of all the non-zero numbers is returned. * * @todo Finish documenting this function * * @param int $sitebytes Set maximum size * @param int $coursebytes Current course $course->maxbytes (in bytes) * @param int $modulebytes Current module ->maxbytes (in bytes) * @param bool $unused This parameter has been deprecated and is not used any more. * @return int The maximum size for uploading files. */ function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) { if (! $filesize = ini_get('upload_max_filesize')) { $filesize = '5M'; } $minimumsize = get_real_size($filesize); if ($postsize = ini_get('post_max_size')) { $postsize = get_real_size($postsize); if ($postsize < $minimumsize) { $minimumsize = $postsize; } } if (($sitebytes > 0) and ($sitebytes < $minimumsize)) { $minimumsize = $sitebytes; } if (($coursebytes > 0) and ($coursebytes < $minimumsize)) { $minimumsize = $coursebytes; } if (($modulebytes > 0) and ($modulebytes < $minimumsize)) { $minimumsize = $modulebytes; } return $minimumsize; } /** * Returns the maximum size for uploading files for the current user * * This function takes in account {@link get_max_upload_file_size()} the user's capabilities * * @param context $context The context in which to check user capabilities * @param int $sitebytes Set maximum size * @param int $coursebytes Current course $course->maxbytes (in bytes) * @param int $modulebytes Current module ->maxbytes (in bytes) * @param stdClass $user The user * @param bool $unused This parameter has been deprecated and is not used any more. * @return int The maximum size for uploading files. */ function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null, $unused = false) { global $USER; if (empty($user)) { $user = $USER; } if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) { return USER_CAN_IGNORE_FILE_SIZE_LIMITS; } return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes); } /** * Returns an array of possible sizes in local language * * Related to {@link get_max_upload_file_size()} - this function returns an * array of possible sizes in an array, translated to the * local language. * * The list of options will go up to the minimum of $sitebytes, $coursebytes or $modulebytes. * * If $coursebytes or $sitebytes is not 0, an option will be included for "Course/Site upload limit (X)" * with the value set to 0. This option will be the first in the list. * * @uses SORT_NUMERIC * @param int $sitebytes Set maximum size * @param int $coursebytes Current course $course->maxbytes (in bytes) * @param int $modulebytes Current module ->maxbytes (in bytes) * @param int|array $custombytes custom upload size/s which will be added to list, * Only value/s smaller then maxsize will be added to list. * @return array */ function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $custombytes = null) { global $CFG; if (!$maxsize = get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes)) { return array(); } if ($sitebytes == 0) { // Will get the minimum of upload_max_filesize or post_max_size. $sitebytes = get_max_upload_file_size(); } $filesize = array(); $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152, 5242880, 10485760, 20971520, 52428800, 104857600, 262144000, 524288000, 786432000, 1073741824, 2147483648, 4294967296, 8589934592); // If custombytes is given and is valid then add it to the list. if (is_number($custombytes) and $custombytes > 0) { $custombytes = (int)$custombytes; if (!in_array($custombytes, $sizelist)) { $sizelist[] = $custombytes; } } else if (is_array($custombytes)) { $sizelist = array_unique(array_merge($sizelist, $custombytes)); } // Allow maxbytes to be selected if it falls outside the above boundaries. if (isset($CFG->maxbytes) && !in_array(get_real_size($CFG->maxbytes), $sizelist)) { // Note: get_real_size() is used in order to prevent problems with invalid values. $sizelist[] = get_real_size($CFG->maxbytes); } foreach ($sizelist as $sizebytes) { if ($sizebytes < $maxsize && $sizebytes > 0) {
< $filesize[(string)intval($sizebytes)] = display_size($sizebytes);
> $filesize[(string)intval($sizebytes)] = display_size($sizebytes, 0);
} } $limitlevel = ''; $displaysize = ''; if ($modulebytes && (($modulebytes < $coursebytes || $coursebytes == 0) && ($modulebytes < $sitebytes || $sitebytes == 0))) { $limitlevel = get_string('activity', 'core');
< $displaysize = display_size($modulebytes);
> $displaysize = display_size($modulebytes, 0);
$filesize[$modulebytes] = $displaysize; // Make sure the limit is also included in the list. } else if ($coursebytes && ($coursebytes < $sitebytes || $sitebytes == 0)) { $limitlevel = get_string('course', 'core');
< $displaysize = display_size($coursebytes);
> $displaysize = display_size($coursebytes, 0);
$filesize[$coursebytes] = $displaysize; // Make sure the limit is also included in the list. } else if ($sitebytes) { $limitlevel = get_string('site', 'core');
< $displaysize = display_size($sitebytes);
> $displaysize = display_size($sitebytes, 0);
$filesize[$sitebytes] = $displaysize; // Make sure the limit is also included in the list. } krsort($filesize, SORT_NUMERIC); if ($limitlevel) { $params = (object) array('contextname' => $limitlevel, 'displaysize' => $displaysize); $filesize = array('0' => get_string('uploadlimitwithsize', 'core', $params)) + $filesize; } return $filesize; } /** * Returns an array with all the filenames in all subdirectories, relative to the given rootdir. * * If excludefiles is defined, then that file/directory is ignored * If getdirs is true, then (sub)directories are included in the output * If getfiles is true, then files are included in the output * (at least one of these must be true!) * * @todo Finish documenting this function. Add examples of $excludefile usage. * * @param string $rootdir A given root directory to start from * @param string|array $excludefiles If defined then the specified file/directory is ignored * @param bool $descend If true then subdirectories are recursed as well * @param bool $getdirs If true then (sub)directories are included in the output * @param bool $getfiles If true then files are included in the output * @return array An array with all the filenames in all subdirectories, relative to the given rootdir */ function get_directory_list($rootdir, $excludefiles='', $descend=true, $getdirs=false, $getfiles=true) { $dirs = array(); if (!$getdirs and !$getfiles) { // Nothing to show. return $dirs; } if (!is_dir($rootdir)) { // Must be a directory. return $dirs; } if (!$dir = opendir($rootdir)) { // Can't open it for some reason. return $dirs; } if (!is_array($excludefiles)) { $excludefiles = array($excludefiles); } while (false !== ($file = readdir($dir))) { $firstchar = substr($file, 0, 1); if ($firstchar == '.' or $file == 'CVS' or in_array($file, $excludefiles)) { continue; } $fullfile = $rootdir .'/'. $file; if (filetype($fullfile) == 'dir') { if ($getdirs) { $dirs[] = $file; } if ($descend) { $subdirs = get_directory_list($fullfile, $excludefiles, $descend, $getdirs, $getfiles); foreach ($subdirs as $subdir) { $dirs[] = $file .'/'. $subdir; } } } else if ($getfiles) { $dirs[] = $file; } } closedir($dir); asort($dirs); return $dirs; } /** * Adds up all the files in a directory and works out the size. * * @param string $rootdir The directory to start from * @param string $excludefile A file to exclude when summing directory size * @return int The summed size of all files and subfiles within the root directory */ function get_directory_size($rootdir, $excludefile='') { global $CFG; // Do it this way if we can, it's much faster. if (!empty($CFG->pathtodu) && is_executable(trim($CFG->pathtodu))) { $command = trim($CFG->pathtodu).' -sk '.escapeshellarg($rootdir); $output = null; $return = null; exec($command, $output, $return); if (is_array($output)) { // We told it to return k. return get_real_size(intval($output[0]).'k'); } } if (!is_dir($rootdir)) { // Must be a directory. return 0; } if (!$dir = @opendir($rootdir)) { // Can't open it for some reason. return 0; } $size = 0; while (false !== ($file = readdir($dir))) { $firstchar = substr($file, 0, 1); if ($firstchar == '.' or $file == 'CVS' or $file == $excludefile) { continue; } $fullfile = $rootdir .'/'. $file; if (filetype($fullfile) == 'dir') { $size += get_directory_size($fullfile, $excludefile); } else { $size += filesize($fullfile); } } closedir($dir); return $size; } /** * Converts bytes into display form *
< * @static string $gb Localized string for size in gigabytes < * @static string $mb Localized string for size in megabytes < * @static string $kb Localized string for size in kilobytes < * @static string $b Localized string for size in bytes
* @param int $size The size to convert to human readable form
< * @return string
> * @param int $decimalplaces If specified, uses fixed number of decimal places > * @param string $fixedunits If specified, uses fixed units (e.g. 'KB') > * @return string Display version of size
*/
< function display_size($size) {
> function display_size($size, int $decimalplaces = 1, string $fixedunits = ''): string {
static $units; if ($size === USER_CAN_IGNORE_FILE_SIZE_LIMITS) { return get_string('unlimited'); } if (empty($units)) { $units[] = get_string('sizeb'); $units[] = get_string('sizekb'); $units[] = get_string('sizemb'); $units[] = get_string('sizegb'); $units[] = get_string('sizetb'); $units[] = get_string('sizepb'); }
< if ($size >= 1024 ** 5) { < $size = round($size / 1024 ** 5 * 10) / 10 . $units[5]; < } else if ($size >= 1024 ** 4) { < $size = round($size / 1024 ** 4 * 10) / 10 . $units[4]; < } else if ($size >= 1024 ** 3) { < $size = round($size / 1024 ** 3 * 10) / 10 . $units[3]; < } else if ($size >= 1024 ** 2) { < $size = round($size / 1024 ** 2 * 10) / 10 . $units[2]; < } else if ($size >= 1024 ** 1) { < $size = round($size / 1024 ** 1 * 10) / 10 . $units[1]; < } else { < $size = intval($size) .' '. $units[0]; // File sizes over 2GB can not work in 32bit PHP anyway.
> switch ($fixedunits) { > case 'PB' : > $magnitude = 5; > break; > case 'TB' : > $magnitude = 4; > break; > case 'GB' : > $magnitude = 3; > break; > case 'MB' : > $magnitude = 2; > break; > case 'KB' : > $magnitude = 1; > break; > case 'B' : > $magnitude = 0; > break; > case '': > $magnitude = floor(log($size, 1024)); > $magnitude = max(0, min(5, $magnitude)); > break; > default: > throw new coding_exception('Unknown fixed units value: ' . $fixedunits);
}
< return $size;
> > // Special case for magnitude 0 (bytes) - never use decimal places. > $nbsp = "\xc2\xa0"; > if ($magnitude === 0) { > return round($size) . $nbsp . $units[$magnitude]; > } > > // Convert to specified units. > $sizeinunit = $size / 1024 ** $magnitude; > > // Fixed decimal places. > return sprintf('%.' . $decimalplaces . 'f', $sizeinunit) . $nbsp . $units[$magnitude];
} /** * Cleans a given filename by removing suspicious or troublesome characters * * @see clean_param() * @param string $string file name * @return string cleaned file name */ function clean_filename($string) { return clean_param($string, PARAM_FILE); } // STRING TRANSLATION. /** * Returns the code for the current language * * @category string * @return string */ function current_language() {
< global $CFG, $USER, $SESSION, $COURSE;
> global $CFG, $PAGE, $SESSION, $USER;
if (!empty($SESSION->forcelang)) { // Allows overriding course-forced language (useful for admins to check // issues in courses whose language they don't understand). // Also used by some code to temporarily get language-related information in a // specific language (see force_current_language()). $return = $SESSION->forcelang;
< } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) {
> } else if (!empty($PAGE->cm->lang)) { > // Activity language, if set. > $return = $PAGE->cm->lang; > > } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) {
// Course language can override all other settings for this page.
< $return = $COURSE->lang;
> $return = $PAGE->course->lang;
} else if (!empty($SESSION->lang)) { // Session language can override other settings. $return = $SESSION->lang; } else if (!empty($USER->lang)) { $return = $USER->lang; } else if (isset($CFG->lang)) { $return = $CFG->lang; } else { $return = 'en'; } // Just in case this slipped in from somewhere by accident. $return = str_replace('_utf8', '', $return); return $return; } /** * Fix the current language to the given language code. * * @param string $lang The language code to use. * @return void */ function fix_current_language(string $lang): void { global $CFG, $COURSE, $SESSION, $USER; if (!get_string_manager()->translation_exists($lang)) { throw new coding_exception("The language pack for $lang is not available"); } $fixglobal = ''; $fixlang = 'lang'; if (!empty($SESSION->forcelang)) { $fixglobal = $SESSION; $fixlang = 'forcelang'; } else if (!empty($COURSE->id) && $COURSE->id != SITEID && !empty($COURSE->lang)) { $fixglobal = $COURSE; } else if (!empty($SESSION->lang)) { $fixglobal = $SESSION; } else if (!empty($USER->lang)) { $fixglobal = $USER; } else if (isset($CFG->lang)) { set_config('lang', $lang); } if ($fixglobal) { $fixglobal->$fixlang = $lang; } } /** * Returns parent language of current active language if defined * * @category string * @param string $lang null means current language * @return string */ function get_parent_language($lang=null) { $parentlang = get_string_manager()->get_string('parentlanguage', 'langconfig', null, $lang); if ($parentlang === 'en') { $parentlang = ''; } return $parentlang; } /** * Force the current language to get strings and dates localised in the given language. * * After calling this function, all strings will be provided in the given language * until this function is called again, or equivalent code is run. * * @param string $language * @return string previous $SESSION->forcelang value */ function force_current_language($language) { global $SESSION; $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : ''; if ($language !== $sessionforcelang) {
< // Seting forcelang to null or an empty string disables it's effect.
> // Setting forcelang to null or an empty string disables its effect.
if (empty($language) || get_string_manager()->translation_exists($language, false)) { $SESSION->forcelang = $language; moodle_setlocale(); } } return $sessionforcelang; } /** * Returns current string_manager instance. * * The param $forcereload is needed for CLI installer only where the string_manager instance * must be replaced during the install.php script life time. * * @category string * @param bool $forcereload shall the singleton be released and new instance created instead? * @return core_string_manager */ function get_string_manager($forcereload=false) { global $CFG; static $singleton = null; if ($forcereload) { $singleton = null; } if ($singleton === null) { if (empty($CFG->early_install_lang)) { $transaliases = array(); if (empty($CFG->langlist)) { $translist = array(); } else { $translist = explode(',', $CFG->langlist); $translist = array_map('trim', $translist); // Each language in the $CFG->langlist can has an "alias" that would substitute the default language name. foreach ($translist as $i => $value) { $parts = preg_split('/\s*\|\s*/', $value, 2); if (count($parts) == 2) { $transaliases[$parts[0]] = $parts[1]; $translist[$i] = $parts[0]; } } } if (!empty($CFG->config_php_settings['customstringmanager'])) { $classname = $CFG->config_php_settings['customstringmanager']; if (class_exists($classname)) { $implements = class_implements($classname); if (isset($implements['core_string_manager'])) { $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); return $singleton; } else { debugging('Unable to instantiate custom string manager: class '.$classname. ' does not implement the core_string_manager interface.'); } } else { debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.'); } } $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist, $transaliases); } else { $singleton = new core_string_manager_install(); } } return $singleton; } /** * Returns a localized string. * * Returns the translated string specified by $identifier as * for $module. Uses the same format files as STphp. * $a is an object, string or number that can be used * within translation strings * * eg 'hello {$a->firstname} {$a->lastname}' * or 'hello {$a}' * * If you would like to directly echo the localized string use * the function {@link print_string()} * * Example usage of this function involves finding the string you would * like a local equivalent of and using its identifier and module information * to retrieve it.<br/> * If you open moodle/lang/en/moodle.php and look near line 278 * you will find a string to prompt a user for their word for 'course' * <code> * $string['course'] = 'Course'; * </code> * So if you want to display the string 'Course' * in any language that supports it on your site * you just need to use the identifier 'course' * <code> * $mystring = '<strong>'. get_string('course') .'</strong>'; * or * </code> * If the string you want is in another file you'd take a slightly * different approach. Looking in moodle/lang/en/calendar.php you find * around line 75: * <code> * $string['typecourse'] = 'Course event'; * </code> * If you want to display the string "Course event" in any language * supported you would use the identifier 'typecourse' and the module 'calendar' * (because it is in the file calendar.php): * <code> * $mystring = '<h1>'. get_string('typecourse', 'calendar') .'</h1>'; * </code> * * As a last resort, should the identifier fail to map to a string * the returned string will be [[ $identifier ]] * * In Moodle 2.3 there is a new argument to this function $lazyload. * Setting $lazyload to true causes get_string to return a lang_string object * rather than the string itself. The fetching of the string is then put off until * the string object is first used. The object can be used by calling it's out * method or by casting the object to a string, either directly e.g. * (string)$stringobject * or indirectly by using the string within another string or echoing it out e.g. * echo $stringobject * return "<p>{$stringobject}</p>"; * It is worth noting that using $lazyload and attempting to use the string as an * array key will cause a fatal error as objects cannot be used as array keys. * But you should never do that anyway! * For more information {@link lang_string} * * @category string * @param string $identifier The key identifier for the localized string * @param string $component The module where the key identifier is stored, * usually expressed as the filename in the language pack without the * .php on the end but can also be written as mod/forum or grade/export/xls. * If none is specified then moodle.php is used.
< * @param string|object|array $a An object, string or number that can be used
> * @param string|object|array|int $a An object, string or number that can be used
* within translation strings * @param bool $lazyload If set to true a string object is returned instead of * the string itself. The string then isn't calculated until it is first used. * @return string The localized string. * @throws coding_exception */ function get_string($identifier, $component = '', $a = null, $lazyload = false) { global $CFG; // If the lazy load argument has been supplied return a lang_string object // instead. // We need to make sure it is true (and a bool) as you will see below there // used to be a forth argument at one point. if ($lazyload === true) { return new lang_string($identifier, $component, $a); } if ($CFG->debugdeveloper && clean_param($identifier, PARAM_STRINGID) === '') { throw new coding_exception('Invalid string identifier. The identifier cannot be empty. Please fix your get_string() call.', DEBUG_DEVELOPER); } // There is now a forth argument again, this time it is a boolean however so // we can still check for the old extralocations parameter. if (!is_bool($lazyload) && !empty($lazyload)) { debugging('extralocations parameter in get_string() is not supported any more, please use standard lang locations only.'); }
< if (strpos($component, '/') !== false) {
> if (strpos((string)$component, '/') !== false) {
debugging('The module name you passed to get_string is the deprecated format ' . 'like mod/mymod or block/myblock. The correct form looks like mymod, or block_myblock.' , DEBUG_DEVELOPER); $componentpath = explode('/', $component); switch ($componentpath[0]) { case 'mod': $component = $componentpath[1]; break; case 'blocks': case 'block': $component = 'block_'.$componentpath[1]; break; case 'enrol': $component = 'enrol_'.$componentpath[1]; break; case 'format': $component = 'format_'.$componentpath[1]; break; case 'grade': $component = 'grade'.$componentpath[1].'_'.$componentpath[2]; break; } } $result = get_string_manager()->get_string($identifier, $component, $a); // Debugging feature lets you display string identifier and component. if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { $result .= ' {' . $identifier . '/' . $component . '}'; } return $result; } /** * Converts an array of strings to their localized value. * * @param array $array An array of strings * @param string $component The language module that these strings can be found in. * @return stdClass translated strings. */ function get_strings($array, $component = '') { $string = new stdClass; foreach ($array as $item) { $string->$item = get_string($item, $component); } return $string; } /** * Prints out a translated string. * * Prints out a translated string using the return value from the {@link get_string()} function. * * Example usage of this function when the string is in the moodle.php file:<br/> * <code> * echo '<strong>'; * print_string('course'); * echo '</strong>'; * </code> * * Example usage of this function when the string is not in the moodle.php file:<br/> * <code> * echo '<h1>'; * print_string('typecourse', 'calendar'); * echo '</h1>'; * </code> * * @category string * @param string $identifier The key identifier for the localized string * @param string $component The module where the key identifier is stored. If none is specified then moodle.php is used. * @param string|object|array $a An object, string or number that can be used within translation strings */ function print_string($identifier, $component = '', $a = null) { echo get_string($identifier, $component, $a); } /** * Returns a list of charset codes * * Returns a list of charset codes. It's hardcoded, so they should be added manually * (checking that such charset is supported by the texlib library!) * * @return array And associative array with contents in the form of charset => charset */ function get_list_of_charsets() { $charsets = array( 'EUC-JP' => 'EUC-JP', 'ISO-2022-JP'=> 'ISO-2022-JP', 'ISO-8859-1' => 'ISO-8859-1', 'SHIFT-JIS' => 'SHIFT-JIS', 'GB2312' => 'GB2312', 'GB18030' => 'GB18030', // GB18030 not supported by typo and mbstring. 'UTF-8' => 'UTF-8'); asort($charsets); return $charsets; } /** * Returns a list of valid and compatible themes * * @return array */ function get_list_of_themes() { global $CFG; $themes = array(); if (!empty($CFG->themelist)) { // Use admin's list of themes. $themelist = explode(',', $CFG->themelist); } else { $themelist = array_keys(core_component::get_plugin_list("theme")); } foreach ($themelist as $key => $themename) { $theme = theme_config::load($themename); $themes[$themename] = $theme; } core_collator::asort_objects_by_method($themes, 'get_theme_name'); return $themes; } /** * Factory function for emoticon_manager * * @return emoticon_manager singleton */ function get_emoticon_manager() { static $singleton = null; if (is_null($singleton)) { $singleton = new emoticon_manager(); } return $singleton; } /** * Provides core support for plugins that have to deal with emoticons (like HTML editor or emoticon filter). * * Whenever this manager mentiones 'emoticon object', the following data * structure is expected: stdClass with properties text, imagename, imagecomponent, * altidentifier and altcomponent * * @see admin_setting_emoticons * * @copyright 2010 David Mudrak * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class emoticon_manager { /** * Returns the currently enabled emoticons * * @param boolean $selectable - If true, only return emoticons that should be selectable from a list. * @return array of emoticon objects */ public function get_emoticons($selectable = false) { global $CFG; $notselectable = ['martin', 'egg']; if (empty($CFG->emoticons)) { return array(); } $emoticons = $this->decode_stored_config($CFG->emoticons); if (!is_array($emoticons)) { // Something is wrong with the format of stored setting. debugging('Invalid format of emoticons setting, please resave the emoticons settings form', DEBUG_NORMAL); return array(); } if ($selectable) { foreach ($emoticons as $index => $emote) { if (in_array($emote->altidentifier, $notselectable)) { // Skip this one. unset($emoticons[$index]); } } } return $emoticons; } /** * Converts emoticon object into renderable pix_emoticon object * * @param stdClass $emoticon emoticon object * @param array $attributes explicit HTML attributes to set * @return pix_emoticon */ public function prepare_renderable_emoticon(stdClass $emoticon, array $attributes = array()) { $stringmanager = get_string_manager(); if ($stringmanager->string_exists($emoticon->altidentifier, $emoticon->altcomponent)) { $alt = get_string($emoticon->altidentifier, $emoticon->altcomponent); } else { $alt = s($emoticon->text); } return new pix_emoticon($emoticon->imagename, $alt, $emoticon->imagecomponent, $attributes); } /** * Encodes the array of emoticon objects into a string storable in config table * * @see self::decode_stored_config() * @param array $emoticons array of emtocion objects * @return string */ public function encode_stored_config(array $emoticons) { return json_encode($emoticons); } /** * Decodes the string into an array of emoticon objects * * @see self::encode_stored_config() * @param string $encoded
< * @return string|null
> * @return array|null
*/ public function decode_stored_config($encoded) { $decoded = json_decode($encoded); if (!is_array($decoded)) { return null; } return $decoded; } /** * Returns default set of emoticons supported by Moodle * * @return array of sdtClasses */ public function default_emoticons() { return array( $this->prepare_emoticon_object(":-)", 's/smiley', 'smiley'), $this->prepare_emoticon_object(":)", 's/smiley', 'smiley'), $this->prepare_emoticon_object(":-D", 's/biggrin', 'biggrin'), $this->prepare_emoticon_object(";-)", 's/wink', 'wink'), $this->prepare_emoticon_object(":-/", 's/mixed', 'mixed'), $this->prepare_emoticon_object("V-.", 's/thoughtful', 'thoughtful'), $this->prepare_emoticon_object(":-P", 's/tongueout', 'tongueout'), $this->prepare_emoticon_object(":-p", 's/tongueout', 'tongueout'), $this->prepare_emoticon_object("B-)", 's/cool', 'cool'), $this->prepare_emoticon_object("^-)", 's/approve', 'approve'), $this->prepare_emoticon_object("8-)", 's/wideeyes', 'wideeyes'), $this->prepare_emoticon_object(":o)", 's/clown', 'clown'), $this->prepare_emoticon_object(":-(", 's/sad', 'sad'), $this->prepare_emoticon_object(":(", 's/sad', 'sad'), $this->prepare_emoticon_object("8-.", 's/shy', 'shy'), $this->prepare_emoticon_object(":-I", 's/blush', 'blush'), $this->prepare_emoticon_object(":-X", 's/kiss', 'kiss'), $this->prepare_emoticon_object("8-o", 's/surprise', 'surprise'), $this->prepare_emoticon_object("P-|", 's/blackeye', 'blackeye'), $this->prepare_emoticon_object("8-[", 's/angry', 'angry'), $this->prepare_emoticon_object("(grr)", 's/angry', 'angry'), $this->prepare_emoticon_object("xx-P", 's/dead', 'dead'), $this->prepare_emoticon_object("|-.", 's/sleepy', 'sleepy'), $this->prepare_emoticon_object("}-]", 's/evil', 'evil'), $this->prepare_emoticon_object("(h)", 's/heart', 'heart'), $this->prepare_emoticon_object("(heart)", 's/heart', 'heart'), $this->prepare_emoticon_object("(y)", 's/yes', 'yes', 'core'), $this->prepare_emoticon_object("(n)", 's/no', 'no', 'core'), $this->prepare_emoticon_object("(martin)", 's/martin', 'martin'), $this->prepare_emoticon_object("( )", 's/egg', 'egg'), ); } /** * Helper method preparing the stdClass with the emoticon properties * * @param string|array $text or array of strings * @param string $imagename to be used by {@link pix_emoticon} * @param string $altidentifier alternative string identifier, null for no alt * @param string $altcomponent where the alternative string is defined * @param string $imagecomponent to be used by {@link pix_emoticon} * @return stdClass */ protected function prepare_emoticon_object($text, $imagename, $altidentifier = null, $altcomponent = 'core_pix', $imagecomponent = 'core') { return (object)array( 'text' => $text, 'imagename' => $imagename, 'imagecomponent' => $imagecomponent, 'altidentifier' => $altidentifier, 'altcomponent' => $altcomponent, ); } } // ENCRYPTION. /** * rc4encrypt * * @param string $data Data to encrypt. * @return string The now encrypted data. */ function rc4encrypt($data) { return endecrypt(get_site_identifier(), $data, ''); } /** * rc4decrypt * * @param string $data Data to decrypt. * @return string The now decrypted data. */ function rc4decrypt($data) { return endecrypt(get_site_identifier(), $data, 'de'); } /** * Based on a class by Mukul Sabharwal [mukulsabharwal @ yahoo.com] * * @todo Finish documenting this function * * @param string $pwd The password to use when encrypting or decrypting * @param string $data The data to be decrypted/encrypted * @param string $case Either 'de' for decrypt or '' for encrypt * @return string */ function endecrypt ($pwd, $data, $case) { if ($case == 'de') { $data = urldecode($data); } $key[] = ''; $box[] = ''; $pwdlength = strlen($pwd); for ($i = 0; $i <= 255; $i++) { $key[$i] = ord(substr($pwd, ($i % $pwdlength), 1)); $box[$i] = $i; } $x = 0; for ($i = 0; $i <= 255; $i++) { $x = ($x + $box[$i] + $key[$i]) % 256; $tempswap = $box[$i]; $box[$i] = $box[$x]; $box[$x] = $tempswap; } $cipher = ''; $a = 0; $j = 0; for ($i = 0; $i < strlen($data); $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $temp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $temp; $k = $box[(($box[$a] + $box[$j]) % 256)]; $cipherby = ord(substr($data, $i, 1)) ^ $k; $cipher .= chr($cipherby); } if ($case == 'de') { $cipher = urldecode(urlencode($cipher)); } else { $cipher = urlencode($cipher); } return $cipher; } // ENVIRONMENT CHECKING. /** * This method validates a plug name. It is much faster than calling clean_param. * * @param string $name a string that might be a plugin name. * @return bool if this string is a valid plugin name. */ function is_valid_plugin_name($name) { // This does not work for 'mod', bad luck, use any other type. return core_component::is_valid_plugin_name('tool', $name); } /** * Get a list of all the plugins of a given type that define a certain API function * in a certain file. The plugin component names and function names are returned. * * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'. * @param string $function the part of the name of the function after the * frankenstyle prefix. e.g 'hook' if you are looking for functions with * names like report_courselist_hook. * @param string $file the name of file within the plugin that defines the * function. Defaults to lib.php. * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum') * and the function names as values (e.g. 'report_courselist_hook', 'forum_hook'). */ function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php') { global $CFG; // We don't include here as all plugin types files would be included. $plugins = get_plugins_with_function($function, $file, false); if (empty($plugins[$plugintype])) { return array(); } $allplugins = core_component::get_plugin_list($plugintype); // Reformat the array and include the files. $pluginfunctions = array(); foreach ($plugins[$plugintype] as $pluginname => $functionname) { // Check that it has not been removed and the file is still available. if (!empty($allplugins[$pluginname])) { $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file; if (file_exists($filepath)) { include_once($filepath); // Now that the file is loaded, we must verify the function still exists. if (function_exists($functionname)) { $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname; } else { // Invalidate the cache for next run. \cache_helper::invalidate_by_definition('core', 'plugin_functions'); } } } } return $pluginfunctions; } /** * Get a list of all the plugins that define a certain API function in a certain file. * * @param string $function the part of the name of the function after the * frankenstyle prefix. e.g 'hook' if you are looking for functions with * names like report_courselist_hook. * @param string $file the name of file within the plugin that defines the * function. Defaults to lib.php. * @param bool $include Whether to include the files that contain the functions or not.
> * @param bool $migratedtohook if true this is a deprecated lib.php callback, if hook callback is present then do nothing
* @return array with [plugintype][plugin] = functionname */
< function get_plugins_with_function($function, $file = 'lib.php', $include = true) {
> function get_plugins_with_function($function, $file = 'lib.php', $include = true, bool $migratedtohook = false) {
global $CFG; if (during_initial_install() || isset($CFG->upgraderunning)) { // API functions _must not_ be called during an installation or upgrade. return []; }
> $plugincallback = $function; $cache = \cache::make('core', 'plugin_functions'); > $filtermigrated = function($plugincallback, $pluginfunctions): array { > foreach ($pluginfunctions as $plugintype => $plugins) { // Including both although I doubt that we will find two functions definitions with the same name. > foreach ($plugins as $plugin => $unusedfunction) { // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_. > $component = $plugintype . '_' . $plugin; $key = $function . '_' . clean_param($file, PARAM_ALPHA); > if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($plugincallback)) { $pluginfunctions = $cache->get($key); > if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $plugincallback)) { $dirty = false; > // Ignore the old callback, it is there only for older Moodle versions. > unset($pluginfunctions[$plugintype][$plugin]); // Use the plugin manager to check that plugins are currently installed. > } else { $pluginmanager = \core_plugin_manager::instance(); > debugging("Callback $plugincallback in $component component should be migrated to new hook callback", > DEBUG_DEVELOPER); if ($pluginfunctions !== false) { > } > } // Checking that the files are still available. > } foreach ($pluginfunctions as $plugintype => $plugins) { > } > return $pluginfunctions; $allplugins = \core_component::get_plugin_list($plugintype); > }; $installedplugins = $pluginmanager->get_installed_plugins($plugintype); >
< // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_. < $key = $function . '_' . clean_param($file, PARAM_ALPHA);
> // Clean the filename as cache_helper::hash_key only allows a-zA-Z0-9_. > $pluginfunctions = false; > if (!empty($CFG->allversionshash)) { > $key = $CFG->allversionshash . '_' . $function . '_' . clean_param($file, PARAM_ALPHA);
// Plugin code is still present on disk but it is not installed.
> }
$dirty = true; break 2; } // Cache might be out of sync with the codebase, skip the plugin if it is not available. if (empty($allplugins[$plugin])) { $dirty = true; break 2; } $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); if ($include && $fileexists) { // Include the files if it was requested. include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); } else if (!$fileexists) { // If the file is not available any more it should not be returned. $dirty = true; break 2; } // Check if the function still exists in the file. if ($include && !function_exists($function)) { $dirty = true; break 2; } } } // If the cache is dirty, we should fall through and let it rebuild. if (!$dirty) {
> if ($migratedtohook && $file === 'lib.php') { return $pluginfunctions; > $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions); } > }
} $pluginfunctions = array(); // To fill the cached. Also, everything should continue working with cache disabled. $plugintypes = \core_component::get_plugin_types(); foreach ($plugintypes as $plugintype => $unused) { // We need to include files here. $pluginswithfile = \core_component::get_plugin_list_with_file($plugintype, $file, true); $installedplugins = $pluginmanager->get_installed_plugins($plugintype); foreach ($pluginswithfile as $plugin => $notused) { if (!isset($installedplugins[$plugin])) { continue; } $fullfunction = $plugintype . '_' . $plugin . '_' . $function; $pluginfunction = false; if (function_exists($fullfunction)) { // Function exists with standard name. Store, indexed by frankenstyle name of plugin. $pluginfunction = $fullfunction; } else if ($plugintype === 'mod') { // For modules, we also allow plugin without full frankenstyle but just starting with the module name. $shortfunction = $plugin . '_' . $function; if (function_exists($shortfunction)) { $pluginfunction = $shortfunction; } } if ($pluginfunction) { if (empty($pluginfunctions[$plugintype])) { $pluginfunctions[$plugintype] = array(); } $pluginfunctions[$plugintype][$plugin] = $pluginfunction; } } }
> if (!empty($CFG->allversionshash)) {
$cache->set($key, $pluginfunctions);
> } > return $pluginfunctions; > if ($migratedtohook && $file === 'lib.php') { > $pluginfunctions = $filtermigrated($plugincallback, $pluginfunctions); } > }
/** * Lists plugin-like directories within specified directory * * This function was originally used for standard Moodle plugins, please use * new core_component::get_plugin_list() now. * * This function is used for general directory listing and backwards compatility. * * @param string $directory relative directory from root * @param string $exclude dir name to exclude from the list (defaults to none) * @param string $basedir full path to the base dir where $plugin resides (defaults to $CFG->dirroot) * @return array Sorted array of directory names found under the requested parameters */ function get_list_of_plugins($directory='mod', $exclude='', $basedir='') { global $CFG; $plugins = array(); if (empty($basedir)) { $basedir = $CFG->dirroot .'/'. $directory; } else { $basedir = $basedir .'/'. $directory; } if ($CFG->debugdeveloper and empty($exclude)) { // Make sure devs do not use this to list normal plugins, // this is intended for general directories that are not plugins! $subtypes = core_component::get_plugin_types(); if (in_array($basedir, $subtypes)) { debugging('get_list_of_plugins() should not be used to list real plugins, use core_component::get_plugin_list() instead!', DEBUG_DEVELOPER); } unset($subtypes); }
> $ignorelist = array_flip(array_filter([ if (file_exists($basedir) && filetype($basedir) == 'dir') { > 'CVS', if (!$dirhandle = opendir($basedir)) { > '_vti_cnf', debugging("Directory permission error for plugin ({$directory}). Directory exists but cannot be read.", DEBUG_DEVELOPER); > 'amd', return array(); > 'classes', } > 'simpletest', while (false !== ($dir = readdir($dirhandle))) { > 'tests', // Func: strpos is marginally but reliably faster than substr($dir, 0, 1). > 'templates', if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or > 'yui', $dir === 'tests' or $dir === 'classes' or $dir === $exclude) { > $exclude, continue; > ])); } >
< // Func: strpos is marginally but reliably faster than substr($dir, 0, 1). < if (strpos($dir, '.') === 0 or $dir === 'CVS' or $dir === '_vti_cnf' or $dir === 'simpletest' or $dir === 'yui' or < $dir === 'tests' or $dir === 'classes' or $dir === $exclude) {
> if (strpos($dir, '.') === 0) { > // Ignore directories starting with . > // These are treated as hidden directories. > continue; > } > if (array_key_exists($dir, $ignorelist)) { > // This directory features on the ignore list.
$plugins[] = $dir; } closedir($dirhandle); } if ($plugins) { asort($plugins); } return $plugins; } /** * Invoke plugin's callback functions * * @param string $type plugin type e.g. 'mod' * @param string $name plugin name * @param string $feature feature name * @param string $action feature's action * @param array $params parameters of callback function, should be an array * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
> * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
* @return mixed * * @todo Decide about to deprecate and drop plugin_callback() - MDL-30743 */
< function plugin_callback($type, $name, $feature, $action, $params = null, $default = null) { < return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default);
> function plugin_callback($type, $name, $feature, $action, $params = null, $default = null, bool $migratedtohook = false) { > return component_callback($type . '_' . $name, $feature . '_' . $action, (array) $params, $default, $migratedtohook);
} /** * Invoke component's callback functions * * @param string $component frankenstyle component name, e.g. 'mod_quiz' * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' * @param array $params parameters of callback function * @param mixed $default default value if callback function hasn't been defined, or if it retursn null.
> * @param bool $migratedtohook if true this is a deprecated callback, if hook callback is present then do nothing
* @return mixed */
< function component_callback($component, $function, array $params = array(), $default = null) {
> function component_callback($component, $function, array $params = array(), $default = null, bool $migratedtohook = false) {
$functionname = component_callback_exists($component, $function); if ($params && (array_keys($params) !== range(0, count($params) - 1))) { // PHP 8 allows to have associative arrays in the call_user_func_array() parameters but // PHP 7 does not. Using associative arrays can result in different behavior in different PHP versions. // See https://php.watch/versions/8.0/named-parameters#named-params-call_user_func_array // This check can be removed when minimum PHP version for Moodle is raised to 8. debugging('Parameters array can not be an associative array while Moodle supports both PHP 7 and PHP 8.', DEBUG_DEVELOPER); $params = array_values($params); } if ($functionname) {
> if ($migratedtohook) { // Function exists, so just return function result. > if (\core\hook\manager::get_instance()->is_deprecated_plugin_callback($function)) { $ret = call_user_func_array($functionname, $params); > if (\core\hook\manager::get_instance()->is_deprecating_hook_present($component, $function)) { if (is_null($ret)) { > // Do not call the old lib.php callback, return $default; > // it is there for compatibility with older Moodle versions only. } else { > return null; return $ret; > } else { } > debugging("Callback $function in $component component should be migrated to new hook callback", } > DEBUG_DEVELOPER); return $default; > } } > } > } /** >
* Determine if a component callback exists and return the function name to call. Note that this * function will include the required library files so that the functioname returned can be * called directly. * * @param string $component frankenstyle component name, e.g. 'mod_quiz' * @param string $function the rest of the function name, e.g. 'cron' will end up calling 'mod_quiz_cron' * @return mixed Complete function name to call if the callback exists or false if it doesn't. * @throws coding_exception if invalid component specfied */ function component_callback_exists($component, $function) { global $CFG; // This is needed for the inclusions. $cleancomponent = clean_param($component, PARAM_COMPONENT); if (empty($cleancomponent)) { throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); } $component = $cleancomponent; list($type, $name) = core_component::normalize_component($component); $component = $type . '_' . $name; $oldfunction = $name.'_'.$function; $function = $component.'_'.$function; $dir = core_component::get_component_directory($component); if (empty($dir)) { throw new coding_exception('Invalid component used in plugin/component_callback():' . $component); } // Load library and look for function. if (file_exists($dir.'/lib.php')) { require_once($dir.'/lib.php'); } if (!function_exists($function) and function_exists($oldfunction)) { if ($type !== 'mod' and $type !== 'core') { debugging("Please use new function name $function instead of legacy $oldfunction", DEBUG_DEVELOPER); } $function = $oldfunction; } if (function_exists($function)) { return $function; } return false; } /** * Call the specified callback method on the provided class. * * If the callback returns null, then the default value is returned instead. * If the class does not exist, then the default value is returned. * * @param string $classname The name of the class to call upon. * @param string $methodname The name of the staticically defined method on the class. * @param array $params The arguments to pass into the method. * @param mixed $default The default value. * @return mixed The return value. */ function component_class_callback($classname, $methodname, array $params, $default = null) { if (!class_exists($classname)) { return $default; } if (!method_exists($classname, $methodname)) { return $default; } $fullfunction = $classname . '::' . $methodname; $result = call_user_func_array($fullfunction, $params); if (null === $result) { return $default; } else { return $result; } } /** * Checks whether a plugin supports a specified feature. * * @param string $type Plugin type e.g. 'mod' * @param string $name Plugin name e.g. 'forum' * @param string $feature Feature code (FEATURE_xx constant) * @param mixed $default default value if feature support unknown * @return mixed Feature result (false if not supported, null if feature is unknown, * otherwise usually true but may have other feature-specific value such as array) * @throws coding_exception */ function plugin_supports($type, $name, $feature, $default = null) { global $CFG; if ($type === 'mod' and $name === 'NEWMODULE') { // Somebody forgot to rename the module template. return false; } $component = clean_param($type . '_' . $name, PARAM_COMPONENT); if (empty($component)) { throw new coding_exception('Invalid component used in plugin_supports():' . $type . '_' . $name); } $function = null; if ($type === 'mod') { // We need this special case because we support subplugins in modules, // otherwise it would end up in infinite loop. if (file_exists("$CFG->dirroot/mod/$name/lib.php")) { include_once("$CFG->dirroot/mod/$name/lib.php"); $function = $component.'_supports'; if (!function_exists($function)) { // Legacy non-frankenstyle function name. $function = $name.'_supports'; } } } else { if (!$path = core_component::get_plugin_directory($type, $name)) { // Non existent plugin type. return false; } if (file_exists("$path/lib.php")) { include_once("$path/lib.php"); $function = $component.'_supports'; } } if ($function and function_exists($function)) { $supports = $function($feature); if (is_null($supports)) { // Plugin does not know - use default. return $default; } else { return $supports; } } // Plugin does not care, so use default. return $default; } /** * Returns true if the current version of PHP is greater that the specified one. * * @todo Check PHP version being required here is it too low? * * @param string $version The version of php being tested. * @return bool */ function check_php_version($version='5.2.4') { return (version_compare(phpversion(), $version) >= 0); } /** * Determine if moodle installation requires update. * * Checks version numbers of main code and all plugins to see * if there are any mismatches. *
> * @param bool $checkupgradeflag check the outagelessupgrade flag to see if an upgrade is running.
* @return bool */
< function moodle_needs_upgrading() { < global $CFG;
> function moodle_needs_upgrading($checkupgradeflag = true) { > global $CFG, $DB; > > // Say no if there is already an upgrade running. > if ($checkupgradeflag) { > $lock = $DB->get_field('config', 'value', ['name' => 'outagelessupgrade']); > $currentprocessrunningupgrade = (defined('CLI_UPGRADE_RUNNING') && CLI_UPGRADE_RUNNING); > // If we ARE locked, but this PHP process is NOT the process running the upgrade, > // We should always return false. > // This means the upgrade is running from CLI somewhere, or about to. > if (!empty($lock) && !$currentprocessrunningupgrade) { > return false; > } > }
if (empty($CFG->version)) { return true; } // There is no need to purge plugininfo caches here because // these caches are not used during upgrade and they are purged after // every upgrade. if (empty($CFG->allversionshash)) { return true; } $hash = core_component::get_all_versions_hash(); return ($hash !== $CFG->allversionshash); } /** * Returns the major version of this site * * Moodle version numbers consist of three numbers separated by a dot, for * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so * called major version. This function extracts the major version from either * $CFG->release (default) or eventually from the $release variable defined in * the main version.php. * * @param bool $fromdisk should the version if source code files be used * @return string|false the major version like '2.3', false if could not be determined */ function moodle_major_version($fromdisk = false) { global $CFG; if ($fromdisk) { $release = null; require($CFG->dirroot.'/version.php'); if (empty($release)) { return false; } } else { if (empty($CFG->release)) { return false; } $release = $CFG->release; } if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) { return $matches[0]; } else { return false; } } // MISCELLANEOUS. /** * Gets the system locale * * @return string Retuns the current locale. */ function moodle_getlocale() { global $CFG; // Fetch the correct locale based on ostype. if ($CFG->ostype == 'WINDOWS') { $stringtofetch = 'localewin'; } else { $stringtofetch = 'locale'; } if (!empty($CFG->locale)) { // Override locale for all language packs. return $CFG->locale; } return get_string($stringtofetch, 'langconfig'); } /** * Sets the system locale * * @category string * @param string $locale Can be used to force a locale */ function moodle_setlocale($locale='') { global $CFG; static $currentlocale = ''; // Last locale caching. $oldlocale = $currentlocale; // The priority is the same as in get_string() - parameter, config, course, session, user, global language. if (!empty($locale)) { $currentlocale = $locale; } else { $currentlocale = moodle_getlocale(); } // Do nothing if locale already set up. if ($oldlocale == $currentlocale) { return; } // Due to some strange BUG we cannot set the LC_TIME directly, so we fetch current values, // set LC_ALL and then set values again. Just wondering why we cannot set LC_ALL only??? - stronk7 // Some day, numeric, monetary and other categories should be set too, I think. :-/. // Get current values. $monetary= setlocale (LC_MONETARY, 0); $numeric = setlocale (LC_NUMERIC, 0); $ctype = setlocale (LC_CTYPE, 0); if ($CFG->ostype != 'WINDOWS') { $messages= setlocale (LC_MESSAGES, 0); } // Set locale to all. $result = setlocale (LC_ALL, $currentlocale); // If setting of locale fails try the other utf8 or utf-8 variant, // some operating systems support both (Debian), others just one (OSX). if ($result === false) { if (stripos($currentlocale, '.UTF-8') !== false) { $newlocale = str_ireplace('.UTF-8', '.UTF8', $currentlocale); setlocale (LC_ALL, $newlocale); } else if (stripos($currentlocale, '.UTF8') !== false) { $newlocale = str_ireplace('.UTF8', '.UTF-8', $currentlocale); setlocale (LC_ALL, $newlocale); } } // Set old values. setlocale (LC_MONETARY, $monetary); setlocale (LC_NUMERIC, $numeric); if ($CFG->ostype != 'WINDOWS') { setlocale (LC_MESSAGES, $messages); } if ($currentlocale == 'tr_TR' or $currentlocale == 'tr_TR.UTF-8') { // To workaround a well-known PHP problem with Turkish letter Ii. setlocale (LC_CTYPE, $ctype); } } /** * Count words in a string. * * Words are defined as things between whitespace. * * @category string * @param string $string The text to be searched for words. May be HTML.
> * @param int|null $format
* @return int The count of words in the specified string */
< function count_words($string) {
> function count_words($string, $format = null) {
// Before stripping tags, add a space after the close tag of anything that is not obviously inline. // Also, br is a special case because it definitely delimits a word, but has no close tag. $string = preg_replace('~ ( # Capture the tag we match. </ # Start of close tag. (?! # Do not match any of these specific close tag names. a> | b> | del> | em> | i> |
< ins> | s> | small> |
> ins> | s> | small> | span> |
strong> | sub> | sup> | u> ) \w+ # But, apart from those execptions, match any tag name. > # End of close tag. | <br> | <br\s*/> # Special cases that are not close tags. ) ~x', '$1 ', $string); // Add a space after the close tag.
> if ($format !== null && $format != FORMAT_PLAIN) { // Now remove HTML tags. > // Match the usual text cleaning before display. $string = strip_tags($string); > // Ideally we should apply multilang filter only here, other filters might add extra text. // Decode HTML entities. > $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); $string = html_entity_decode($string); > }
< $string = html_entity_decode($string);
> $string = html_entity_decode($string, ENT_COMPAT);
// Now, the word count is the number of blocks of characters separated // by any sort of space. That seems to be the definition used by all other systems. // To be precise about what is considered to separate words: // * Anything that Unicode considers a 'Separator' // * Anything that Unicode considers a 'Control character' // * An em- or en- dash. return count(preg_split('~[\p{Z}\p{Cc}—–]+~u', $string, -1, PREG_SPLIT_NO_EMPTY)); } /** * Count letters in a string. * * Letters are defined as chars not in tags and different from whitespace. * * @category string * @param string $string The text to be searched for letters. May be HTML.
> * @param int|null $format
* @return int The count of letters in the specified text. */
< function count_letters($string) {
> function count_letters($string, $format = null) { > if ($format !== null && $format != FORMAT_PLAIN) { > // Match the usual text cleaning before display. > // Ideally we should apply multilang filter only here, other filters might add extra text. > $string = format_text($string, $format, ['filter' => false, 'noclean' => false, 'para' => false]); > }
$string = strip_tags($string); // Tags are out now.
< $string = html_entity_decode($string);
> $string = html_entity_decode($string, ENT_COMPAT);
$string = preg_replace('/[[:space:]]*/', '', $string); // Whitespace are out now. return core_text::strlen($string); } /** * Generate and return a random string of the specified length. * * @param int $length The length of the string to be created. * @return string */ function random_string($length=15) {
< $randombytes = random_bytes_emulate($length);
> $randombytes = random_bytes($length);
$pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $pool .= 'abcdefghijklmnopqrstuvwxyz'; $pool .= '0123456789'; $poollen = strlen($pool); $string = ''; for ($i = 0; $i < $length; $i++) { $rand = ord($randombytes[$i]); $string .= substr($pool, ($rand%($poollen)), 1); } return $string; } /** * Generate a complex random string (useful for md5 salts) * * This function is based on the above {@link random_string()} however it uses a * larger pool of characters and generates a string between 24 and 32 characters * * @param int $length Optional if set generates a string to exactly this length * @return string */ function complex_random_string($length=null) { $pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $pool .= '`~!@#%^&*()_+-=[];,./<>?:{} '; $poollen = strlen($pool); if ($length===null) { $length = floor(rand(24, 32)); }
< $randombytes = random_bytes_emulate($length);
> $randombytes = random_bytes($length);
$string = ''; for ($i = 0; $i < $length; $i++) { $rand = ord($randombytes[$i]); $string .= $pool[($rand%$poollen)]; } return $string; } /**
< * Try to generates cryptographically secure pseudo-random bytes. < * < * Note this is achieved by fallbacking between: < * - PHP 7 random_bytes(). < * - OpenSSL openssl_random_pseudo_bytes(). < * - In house random generator getting its entropy from various, hard to guess, pseudo-random sources. < * < * @param int $length requested length in bytes < * @return string binary data < */ < function random_bytes_emulate($length) { < global $CFG; < if ($length <= 0) { < debugging('Invalid random bytes length', DEBUG_DEVELOPER); < return ''; < } < if (function_exists('random_bytes')) { < // Use PHP 7 goodness. < $hash = @random_bytes($length); < if ($hash !== false) { < return $hash; < } < } < if (function_exists('openssl_random_pseudo_bytes')) { < // If you have the openssl extension enabled. < $hash = openssl_random_pseudo_bytes($length); < if ($hash !== false) { < return $hash; < } < } < < // Bad luck, there is no reliable random generator, let's just slowly hash some unique stuff that is hard to guess. < $staticdata = serialize($CFG) . serialize($_SERVER); < $hash = ''; < do { < $hash .= sha1($staticdata . microtime(true) . uniqid('', true), true); < } while (strlen($hash) < $length); < < return substr($hash, 0, $length); < } < < /**
* Given some text (which may contain HTML) and an ideal length, * this function truncates the text neatly on a word boundary if possible * * @category string * @param string $text text to be shortened * @param int $ideal ideal string length * @param boolean $exact if false, $text will not be cut mid-word * @param string $ending The string to append if the passed string is truncated * @return string $truncate shortened string */ function shorten_text($text, $ideal=30, $exact = false, $ending='...') { // If the plain text is shorter than the maximum length, return the whole text. if (core_text::strlen(preg_replace('/<.*?>/', '', $text)) <= $ideal) { return $text; } // Splits on HTML tags. Each open/close/empty tag will be the first thing // and only tag in its 'line'. preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); $totallength = core_text::strlen($ending); $truncate = ''; // This array stores information about open and close tags and their position // in the truncated string. Each item in the array is an object with fields // ->open (true if open), ->tag (tag name in lower case), and ->pos // (byte position in truncated text). $tagdetails = array(); foreach ($lines as $linematchings) { // If there is any html-tag in this line, handle it and add it (uncounted) to the output. if (!empty($linematchings[1])) { // If it's an "empty element" with or without xhtml-conform closing slash (f.e. <br/>). if (!preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $linematchings[1])) { if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $linematchings[1], $tagmatchings)) { // Record closing tag. $tagdetails[] = (object) array( 'open' => false, 'tag' => core_text::strtolower($tagmatchings[1]), 'pos' => core_text::strlen($truncate), ); } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $linematchings[1], $tagmatchings)) { // Record opening tag. $tagdetails[] = (object) array( 'open' => true, 'tag' => core_text::strtolower($tagmatchings[1]), 'pos' => core_text::strlen($truncate), ); } else if (preg_match('/^<!--\[if\s.*?\]>$/s', $linematchings[1], $tagmatchings)) { $tagdetails[] = (object) array( 'open' => true, 'tag' => core_text::strtolower('if'), 'pos' => core_text::strlen($truncate), ); } else if (preg_match('/^<!--<!\[endif\]-->$/s', $linematchings[1], $tagmatchings)) { $tagdetails[] = (object) array( 'open' => false, 'tag' => core_text::strtolower('if'), 'pos' => core_text::strlen($truncate), ); } } // Add html-tag to $truncate'd text. $truncate .= $linematchings[1]; } // Calculate the length of the plain text part of the line; handle entities as one character. $contentlength = core_text::strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', ' ', $linematchings[2])); if ($totallength + $contentlength > $ideal) { // The number of characters which are left. $left = $ideal - $totallength; $entitieslength = 0; // Search for html entities. if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i', $linematchings[2], $entities, PREG_OFFSET_CAPTURE)) { // Calculate the real length of all entities in the legal range. foreach ($entities[0] as $entity) { if ($entity[1]+1-$entitieslength <= $left) { $left--; $entitieslength += core_text::strlen($entity[0]); } else { // No more characters left. break; } } } $breakpos = $left + $entitieslength; // If the words shouldn't be cut in the middle... if (!$exact) { // Search the last occurence of a space. for (; $breakpos > 0; $breakpos--) { if ($char = core_text::substr($linematchings[2], $breakpos, 1)) { if ($char === '.' or $char === ' ') { $breakpos += 1; break; } else if (strlen($char) > 2) { // Chinese/Japanese/Korean text can be truncated at any UTF-8 character boundary. $breakpos += 1; break; } } } } if ($breakpos == 0) { // This deals with the test_shorten_text_no_spaces case. $breakpos = $left + $entitieslength; } else if ($breakpos > $left + $entitieslength) { // This deals with the previous for loop breaking on the first char. $breakpos = $left + $entitieslength; } $truncate .= core_text::substr($linematchings[2], 0, $breakpos); // Maximum length is reached, so get off the loop. break; } else { $truncate .= $linematchings[2]; $totallength += $contentlength; } // If the maximum length is reached, get off the loop. if ($totallength >= $ideal) { break; } } // Add the defined ending to the text. $truncate .= $ending; // Now calculate the list of open html tags based on the truncate position. $opentags = array(); foreach ($tagdetails as $taginfo) { if ($taginfo->open) { // Add tag to the beginning of $opentags list. array_unshift($opentags, $taginfo->tag); } else { // Can have multiple exact same open tags, close the last one. $pos = array_search($taginfo->tag, array_reverse($opentags, true)); if ($pos !== false) { unset($opentags[$pos]); } } } // Close all unclosed html-tags. foreach ($opentags as $tag) { if ($tag === 'if') { $truncate .= '<!--<![endif]-->'; } else { $truncate .= '</' . $tag . '>'; } } return $truncate; } /** * Shortens a given filename by removing characters positioned after the ideal string length. * When the filename is too long, the file cannot be created on the filesystem due to exceeding max byte size. * Limiting the filename to a certain size (considering multibyte characters) will prevent this. * * @param string $filename file name * @param int $length ideal string length * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. * @return string $shortened shortened file name */ function shorten_filename($filename, $length = MAX_FILENAME_SIZE, $includehash = false) { $shortened = $filename; // Extract a part of the filename if it's char size exceeds the ideal string length. if (core_text::strlen($filename) > $length) { // Exclude extension if present in filename. $mimetypes = get_mimetypes_array(); $extension = pathinfo($filename, PATHINFO_EXTENSION); if ($extension && !empty($mimetypes[$extension])) { $basename = pathinfo($filename, PATHINFO_FILENAME); $hash = empty($includehash) ? '' : ' - ' . substr(sha1($basename), 0, 10); $shortened = core_text::substr($basename, 0, $length - strlen($hash)) . $hash; $shortened .= '.' . $extension; } else { $hash = empty($includehash) ? '' : ' - ' . substr(sha1($filename), 0, 10); $shortened = core_text::substr($filename, 0, $length - strlen($hash)) . $hash; } } return $shortened; } /** * Shortens a given array of filenames by removing characters positioned after the ideal string length. * * @param array $path The paths to reduce the length. * @param int $length Ideal string length * @param bool $includehash Whether to include a file hash in the shortened version. This ensures uniqueness. * @return array $result Shortened paths in array. */ function shorten_filenames(array $path, $length = MAX_FILENAME_SIZE, $includehash = false) { $result = null; $result = array_reduce($path, function($carry, $singlepath) use ($length, $includehash) { $carry[] = shorten_filename($singlepath, $length, $includehash); return $carry; }, []); return $result; } /** * Given dates in seconds, how many weeks is the date from startdate * The first week is 1, the second 2 etc ... * * @param int $startdate Timestamp for the start date * @param int $thedate Timestamp for the end date * @return string */ function getweek ($startdate, $thedate) { if ($thedate < $startdate) { return 0; } return floor(($thedate - $startdate) / WEEKSECS) + 1; } /** * Returns a randomly generated password of length $maxlen. inspired by * * {@link http://www.phpbuilder.com/columns/jesus19990502.php3} and * {@link http://es2.php.net/manual/en/function.str-shuffle.php#73254} * * @param int $maxlen The maximum size of the password being generated. * @return string */ function generate_password($maxlen=10) { global $CFG; if (empty($CFG->passwordpolicy)) { $fillers = PASSWORD_DIGITS; $wordlist = file($CFG->wordlist); $word1 = trim($wordlist[rand(0, count($wordlist) - 1)]); $word2 = trim($wordlist[rand(0, count($wordlist) - 1)]); $filler1 = $fillers[rand(0, strlen($fillers) - 1)]; $password = $word1 . $filler1 . $word2; } else { $minlen = !empty($CFG->minpasswordlength) ? $CFG->minpasswordlength : 0; $digits = $CFG->minpassworddigits; $lower = $CFG->minpasswordlower; $upper = $CFG->minpasswordupper; $nonalphanum = $CFG->minpasswordnonalphanum; $total = $lower + $upper + $digits + $nonalphanum; // Var minlength should be the greater one of the two ( $minlen and $total ). $minlen = $minlen < $total ? $total : $minlen; // Var maxlen can never be smaller than minlen. $maxlen = $minlen > $maxlen ? $minlen : $maxlen; $additional = $maxlen - $total; // Make sure we have enough characters to fulfill // complexity requirements. $passworddigits = PASSWORD_DIGITS; while ($digits > strlen($passworddigits)) { $passworddigits .= PASSWORD_DIGITS; } $passwordlower = PASSWORD_LOWER; while ($lower > strlen($passwordlower)) { $passwordlower .= PASSWORD_LOWER; } $passwordupper = PASSWORD_UPPER; while ($upper > strlen($passwordupper)) { $passwordupper .= PASSWORD_UPPER; } $passwordnonalphanum = PASSWORD_NONALPHANUM; while ($nonalphanum > strlen($passwordnonalphanum)) { $passwordnonalphanum .= PASSWORD_NONALPHANUM; } // Now mix and shuffle it all. $password = str_shuffle (substr(str_shuffle ($passwordlower), 0, $lower) . substr(str_shuffle ($passwordupper), 0, $upper) . substr(str_shuffle ($passworddigits), 0, $digits) . substr(str_shuffle ($passwordnonalphanum), 0 , $nonalphanum) . substr(str_shuffle ($passwordlower . $passwordupper . $passworddigits . $passwordnonalphanum), 0 , $additional)); } return substr ($password, 0, $maxlen); } /** * Given a float, prints it nicely. * Localized floats must not be used in calculations! * * The stripzeros feature is intended for making numbers look nicer in small * areas where it is not necessary to indicate the degree of accuracy by showing * ending zeros. If you turn it on with $decimalpoints set to 3, for example, * then it will display '5.4' instead of '5.400' or '5' instead of '5.000'. * * @param float $float The float to print * @param int $decimalpoints The number of decimal places to print. -1 is a special value for auto detect (full precision). * @param bool $localized use localized decimal separator * @param bool $stripzeros If true, removes final zeros after decimal point. It will be ignored and the trailing zeros after * the decimal point are always striped if $decimalpoints is -1. * @return string locale float */ function format_float($float, $decimalpoints=1, $localized=true, $stripzeros=false) { if (is_null($float)) { return ''; } if ($localized) { $separator = get_string('decsep', 'langconfig'); } else { $separator = '.'; } if ($decimalpoints == -1) { // The following counts the number of decimals. // It is safe as both floatval() and round() functions have same behaviour when non-numeric values are provided. $floatval = floatval($float); for ($decimalpoints = 0; $floatval != round($float, $decimalpoints); $decimalpoints++); } $result = number_format($float, $decimalpoints, $separator, ''); if ($stripzeros && $decimalpoints > 0) { // Remove zeros and final dot if not needed. // However, only do this if there is a decimal point! $result = preg_replace('~(' . preg_quote($separator, '~') . ')?0+$~', '', $result); } return $result; } /** * Converts locale specific floating point/comma number back to standard PHP float value * Do NOT try to do any math operations before this conversion on any user submitted floats! * * @param string $localefloat locale aware float representation * @param bool $strict If true, then check the input and return false if it is not a valid number. * @return mixed float|bool - false or the parsed float. */ function unformat_float($localefloat, $strict = false) {
< $localefloat = trim($localefloat);
> $localefloat = trim((string)$localefloat);
if ($localefloat == '') { return null; } $localefloat = str_replace(' ', '', $localefloat); // No spaces - those might be used as thousand separators. $localefloat = str_replace(get_string('decsep', 'langconfig'), '.', $localefloat); if ($strict && !is_numeric($localefloat)) { return false; } return (float)$localefloat; } /** * Given a simple array, this shuffles it up just like shuffle() * Unlike PHP's shuffle() this function works on any machine. * * @param array $array The array to be rearranged * @return array */ function swapshuffle($array) { $last = count($array) - 1; for ($i = 0; $i <= $last; $i++) { $from = rand(0, $last); $curr = $array[$i]; $array[$i] = $array[$from]; $array[$from] = $curr; } return $array; } /** * Like {@link swapshuffle()}, but works on associative arrays * * @param array $array The associative array to be rearranged * @return array */ function swapshuffle_assoc($array) { $newarray = array(); $newkeys = swapshuffle(array_keys($array)); foreach ($newkeys as $newkey) { $newarray[$newkey] = $array[$newkey]; } return $newarray; } /** * Given an arbitrary array, and a number of draws, * this function returns an array with that amount * of items. The indexes are retained. * * @todo Finish documenting this function * * @param array $array * @param int $draws * @return array */ function draw_rand_array($array, $draws) { $return = array(); $last = count($array); if ($draws > $last) { $draws = $last; } while ($draws > 0) { $last--; $keys = array_keys($array); $rand = rand(0, $last); $return[$keys[$rand]] = $array[$keys[$rand]]; unset($array[$keys[$rand]]); $draws--; } return $return; } /** * Calculate the difference between two microtimes * * @param string $a The first Microtime * @param string $b The second Microtime * @return string */ function microtime_diff($a, $b) { list($adec, $asec) = explode(' ', $a); list($bdec, $bsec) = explode(' ', $b); return $bsec - $asec + $bdec - $adec; } /** * Given a list (eg a,b,c,d,e) this function returns * an array of 1->a, 2->b, 3->c etc * * @param string $list The string to explode into array bits * @param string $separator The separator used within the list string * @return array The now assembled array */ function make_menu_from_list($list, $separator=',') { $array = array_reverse(explode($separator, $list), true); foreach ($array as $key => $item) { $outarray[$key+1] = trim($item); } return $outarray; } /** * Creates an array that represents all the current grades that * can be chosen using the given grading type. * * Negative numbers * are scales, zero is no grade, and positive numbers are maximum * grades. * * @todo Finish documenting this function or better deprecated this completely! * * @param int $gradingtype * @return array */ function make_grades_menu($gradingtype) { global $DB; $grades = array(); if ($gradingtype < 0) { if ($scale = $DB->get_record('scale', array('id'=> (-$gradingtype)))) { return make_menu_from_list($scale->scale); } } else if ($gradingtype > 0) { for ($i=$gradingtype; $i>=0; $i--) { $grades[$i] = $i .' / '. $gradingtype; } return $grades; } return $grades; } /** * make_unique_id_code * * @todo Finish documenting this function * * @uses $_SERVER * @param string $extra Extra string to append to the end of the code * @return string */ function make_unique_id_code($extra = '') { $hostname = 'unknownhost'; if (!empty($_SERVER['HTTP_HOST'])) { $hostname = $_SERVER['HTTP_HOST']; } else if (!empty($_ENV['HTTP_HOST'])) { $hostname = $_ENV['HTTP_HOST']; } else if (!empty($_SERVER['SERVER_NAME'])) { $hostname = $_SERVER['SERVER_NAME']; } else if (!empty($_ENV['SERVER_NAME'])) { $hostname = $_ENV['SERVER_NAME']; } $date = gmdate("ymdHis"); $random = random_string(6); if ($extra) { return $hostname .'+'. $date .'+'. $random .'+'. $extra; } else { return $hostname .'+'. $date .'+'. $random; } } /** * Function to check the passed address is within the passed subnet * * The parameter is a comma separated string of subnet definitions. * Subnet strings can be in one of three formats: * 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn (number of bits in net mask) * 2: xxx.xxx.xxx.xxx-yyy or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy (a range of IP addresses in the last group) * 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. (incomplete address, a bit non-technical ;-) * Code for type 1 modified from user posted comments by mediator at * {@link http://au.php.net/manual/en/function.ip2long.php} * * @param string $addr The address you are checking * @param string $subnetstr The string of subnet addresses * @param bool $checkallzeros The state to whether check for 0.0.0.0 * @return bool */ function address_in_subnet($addr, $subnetstr, $checkallzeros = false) { if ($addr == '0.0.0.0' && !$checkallzeros) { return false; } $subnets = explode(',', $subnetstr); $found = false; $addr = trim($addr); $addr = cleanremoteaddr($addr, false); // Normalise. if ($addr === null) { return false; } $addrparts = explode(':', $addr); $ipv6 = strpos($addr, ':'); foreach ($subnets as $subnet) { $subnet = trim($subnet); if ($subnet === '') { continue; } if (strpos($subnet, '/') !== false) { // 1: xxx.xxx.xxx.xxx/nn or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/nnn. list($ip, $mask) = explode('/', $subnet); $mask = trim($mask); if (!is_number($mask)) { continue; // Incorect mask number, eh? } $ip = cleanremoteaddr($ip, false); // Normalise. if ($ip === null) { continue; } if (strpos($ip, ':') !== false) { // IPv6. if (!$ipv6) { continue; } if ($mask > 128 or $mask < 0) { continue; // Nonsense. } if ($mask == 0) { return true; // Any address. } if ($mask == 128) { if ($ip === $addr) { return true; } continue; } $ipparts = explode(':', $ip); $modulo = $mask % 16; $ipnet = array_slice($ipparts, 0, ($mask-$modulo)/16); $addrnet = array_slice($addrparts, 0, ($mask-$modulo)/16); if (implode(':', $ipnet) === implode(':', $addrnet)) { if ($modulo == 0) { return true; } $pos = ($mask-$modulo)/16; $ipnet = hexdec($ipparts[$pos]); $addrnet = hexdec($addrparts[$pos]); $mask = 0xffff << (16 - $modulo); if (($addrnet & $mask) == ($ipnet & $mask)) { return true; } } } else { // IPv4. if ($ipv6) { continue; } if ($mask > 32 or $mask < 0) { continue; // Nonsense. } if ($mask == 0) { return true; } if ($mask == 32) { if ($ip === $addr) { return true; } continue; } $mask = 0xffffffff << (32 - $mask); if (((ip2long($addr) & $mask) == (ip2long($ip) & $mask))) { return true; } } } else if (strpos($subnet, '-') !== false) { // 2: xxx.xxx.xxx.xxx-yyy or xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx::xxxx-yyyy. A range of IP addresses in the last group. $parts = explode('-', $subnet); if (count($parts) != 2) { continue; } if (strpos($subnet, ':') !== false) { // IPv6. if (!$ipv6) { continue; } $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. if ($ipstart === null) { continue; } $ipparts = explode(':', $ipstart); $start = hexdec(array_pop($ipparts)); $ipparts[] = trim($parts[1]); $ipend = cleanremoteaddr(implode(':', $ipparts), false); // Normalise. if ($ipend === null) { continue; } $ipparts[7] = ''; $ipnet = implode(':', $ipparts); if (strpos($addr, $ipnet) !== 0) { continue; } $ipparts = explode(':', $ipend); $end = hexdec($ipparts[7]); $addrend = hexdec($addrparts[7]); if (($addrend >= $start) and ($addrend <= $end)) { return true; } } else { // IPv4. if ($ipv6) { continue; } $ipstart = cleanremoteaddr(trim($parts[0]), false); // Normalise. if ($ipstart === null) { continue; } $ipparts = explode('.', $ipstart); $ipparts[3] = trim($parts[1]); $ipend = cleanremoteaddr(implode('.', $ipparts), false); // Normalise. if ($ipend === null) { continue; } if ((ip2long($addr) >= ip2long($ipstart)) and (ip2long($addr) <= ip2long($ipend))) { return true; } } } else { // 3: xxx.xxx or xxx.xxx. or xxx:xxx:xxxx or xxx:xxx:xxxx. if (strpos($subnet, ':') !== false) { // IPv6. if (!$ipv6) { continue; } $parts = explode(':', $subnet); $count = count($parts); if ($parts[$count-1] === '') { unset($parts[$count-1]); // Trim trailing :'s. $count--; $subnet = implode('.', $parts); } $isip = cleanremoteaddr($subnet, false); // Normalise. if ($isip !== null) { if ($isip === $addr) { return true; } continue; } else if ($count > 8) { continue; } $zeros = array_fill(0, 8-$count, '0'); $subnet = $subnet.':'.implode(':', $zeros).'/'.($count*16); if (address_in_subnet($addr, $subnet)) { return true; } } else { // IPv4. if ($ipv6) { continue; } $parts = explode('.', $subnet); $count = count($parts); if ($parts[$count-1] === '') { unset($parts[$count-1]); // Trim trailing . $count--; $subnet = implode('.', $parts); } if ($count == 4) { $subnet = cleanremoteaddr($subnet, false); // Normalise. if ($subnet === $addr) { return true; } continue; } else if ($count > 4) { continue; } $zeros = array_fill(0, 4-$count, '0'); $subnet = $subnet.'.'.implode('.', $zeros).'/'.($count*8); if (address_in_subnet($addr, $subnet)) { return true; } } } } return false; } /** * For outputting debugging info * * @param string $string The string to write * @param string $eol The end of line char(s) to use * @param string $sleep Period to make the application sleep * This ensures any messages have time to display before redirect */ function mtrace($string, $eol="\n", $sleep=0) { global $CFG; if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) { $fn = $CFG->mtrace_wrapper; $fn($string, $eol); return; } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) { // We must explicitly call the add_line function here. // Uses of fwrite to STDOUT are not picked up by ob_start. if ($output = \core\task\logmanager::add_line("{$string}{$eol}")) { fwrite(STDOUT, $output); } } else { echo $string . $eol; } // Flush again. flush(); // Delay to keep message on user's screen in case of subsequent redirect. if ($sleep) { sleep($sleep); } } /**
> * Helper to {@see mtrace()} an exception or throwable, including all relevant information. * Replace 1 or more slashes or backslashes to 1 slash > * * > * @param Throwable $e the error to ouptput. * @param string $path The path to strip > */ * @return string the path with double slashes removed > function mtrace_exception(Throwable $e): void { */ > $info = get_exception_info($e); function cleardoubleslashes ($path) { > return preg_replace('/(\/|\\\){1,}/', '/', $path); > $message = $info->message; } > if ($info->debuginfo) { > $message .= "\n\n" . $info->debuginfo; /** > } * Is the current ip in a given list? > if ($info->backtrace) { * > $message .= "\n\n" . format_backtrace($info->backtrace, true); * @param string $list > } * @return bool > */ > mtrace($message); function remoteip_in_list($list) { > } $clientip = getremoteaddr(null); > > /**
if (!$clientip) { // Ensure access on cli. return true; } return \core\ip_utils::is_ip_in_subnet_list($clientip, $list); } /** * Returns most reliable client address * * @param string $default If an address can't be determined, then return this * @return string The remote IP address */ function getremoteaddr($default='0.0.0.0') { global $CFG; if (!isset($CFG->getremoteaddrconf)) { // This will happen, for example, before just after the upgrade, as the // user is redirected to the admin screen. $variablestoskip = GETREMOTEADDR_SKIP_DEFAULT; } else { $variablestoskip = $CFG->getremoteaddrconf; } if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_CLIENT_IP)) { if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $address = cleanremoteaddr($_SERVER['HTTP_CLIENT_IP']); return $address ? $address : $default; } } if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) { if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']); $forwardedaddresses = array_filter($forwardedaddresses, function($ip) { global $CFG; return !\core\ip_utils::is_ip_in_subnet_list($ip, $CFG->reverseproxyignore ?? '', ','); }); // Multiple proxies can append values to this header including an // untrusted original request header so we must only trust the last ip. $address = end($forwardedaddresses); if (substr_count($address, ":") > 1) { // Remove port and brackets from IPv6. if (preg_match("/\[(.*)\]:/", $address, $matches)) { $address = $matches[1]; } } else { // Remove port from IPv4. if (substr_count($address, ":") == 1) { $parts = explode(":", $address); $address = $parts[0]; } } $address = cleanremoteaddr($address); return $address ? $address : $default; } } if (!empty($_SERVER['REMOTE_ADDR'])) { $address = cleanremoteaddr($_SERVER['REMOTE_ADDR']); return $address ? $address : $default; } else { return $default; } } /** * Cleans an ip address. Internal addresses are now allowed. * (Originally local addresses were not allowed.) * * @param string $addr IPv4 or IPv6 address * @param bool $compress use IPv6 address compression * @return string normalised ip address string, null if error */ function cleanremoteaddr($addr, $compress=false) { $addr = trim($addr); if (strpos($addr, ':') !== false) { // Can be only IPv6. $parts = explode(':', $addr); $count = count($parts); if (strpos($parts[$count-1], '.') !== false) { // Legacy ipv4 notation. $last = array_pop($parts); $ipv4 = cleanremoteaddr($last, true); if ($ipv4 === null) { return null; } $bits = explode('.', $ipv4); $parts[] = dechex($bits[0]).dechex($bits[1]); $parts[] = dechex($bits[2]).dechex($bits[3]); $count = count($parts); $addr = implode(':', $parts); } if ($count < 3 or $count > 8) { return null; // Severly malformed. } if ($count != 8) { if (strpos($addr, '::') === false) { return null; // Malformed. } // Uncompress. $insertat = array_search('', $parts, true); $missing = array_fill(0, 1 + 8 - $count, '0'); array_splice($parts, $insertat, 1, $missing); foreach ($parts as $key => $part) { if ($part === '') { $parts[$key] = '0'; } } } $adr = implode(':', $parts); if (!preg_match('/^([0-9a-f]{1,4})(:[0-9a-f]{1,4})*$/i', $adr)) { return null; // Incorrect format - sorry. } // Normalise 0s and case. $parts = array_map('hexdec', $parts); $parts = array_map('dechex', $parts); $result = implode(':', $parts); if (!$compress) { return $result; } if ($result === '0:0:0:0:0:0:0:0') { return '::'; // All addresses. } $compressed = preg_replace('/(:0)+:0$/', '::', $result, 1); if ($compressed !== $result) { return $compressed; } $compressed = preg_replace('/^(0:){2,7}/', '::', $result, 1); if ($compressed !== $result) { return $compressed; } $compressed = preg_replace('/(:0){2,6}:/', '::', $result, 1); if ($compressed !== $result) { return $compressed; } return $result; } // First get all things that look like IPv4 addresses. $parts = array(); if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $addr, $parts)) { return null; } unset($parts[0]); foreach ($parts as $key => $match) { if ($match > 255) { return null; } $parts[$key] = (int)$match; // Normalise 0s. } return implode('.', $parts); } /** * Is IP address a public address? * * @param string $ip The ip to check * @return bool true if the ip is public */ function ip_is_public($ip) { return (bool) filter_var($ip, FILTER_VALIDATE_IP, (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)); } /** * This function will make a complete copy of anything it's given, * regardless of whether it's an object or not. * * @param mixed $thing Something you want cloned * @return mixed What ever it is you passed it */ function fullclone($thing) { return unserialize(serialize($thing)); } /** * Used to make sure that $min <= $value <= $max * * Make sure that value is between min, and max * * @param int $min The minimum value * @param int $value The value to check * @param int $max The maximum value * @return int */ function bounded_number($min, $value, $max) { if ($value < $min) { return $min; } if ($value > $max) { return $max; } return $value; } /** * Check if there is a nested array within the passed array * * @param array $array * @return bool true if there is a nested array false otherwise */ function array_is_nested($array) { foreach ($array as $value) { if (is_array($value)) { return true; } } return false; } /** * get_performance_info() pairs up with init_performance_info() * loaded in setup.php. Returns an array with 'html' and 'txt' * values ready for use, and each of the individual stats provided * separately as well. * * @return array */ function get_performance_info() { global $CFG, $PERF, $DB, $PAGE; $info = array(); $info['txt'] = me() . ' '; // Holds log-friendly representation. $info['html'] = ''; if (!empty($CFG->themedesignermode)) { // Attempt to avoid devs debugging peformance issues, when its caused by css building and so on. $info['html'] .= '<p><strong>Warning: Theme designer mode is enabled.</strong></p>'; } $info['html'] .= '<ul class="list-unstyled row mx-md-0">'; // Holds userfriendly HTML representation. $info['realtime'] = microtime_diff($PERF->starttime, microtime()); $info['html'] .= '<li class="timeused col-sm-4">'.$info['realtime'].' secs</li> '; $info['txt'] .= 'time: '.$info['realtime'].'s '; // GET/POST (or NULL if $_SERVER['REQUEST_METHOD'] is undefined) is useful for txt logged information. $info['txt'] .= 'method: ' . ($_SERVER['REQUEST_METHOD'] ?? "NULL") . ' '; if (function_exists('memory_get_usage')) { $info['memory_total'] = memory_get_usage(); $info['memory_growth'] = memory_get_usage() - $PERF->startmemory; $info['html'] .= '<li class="memoryused col-sm-4">RAM: '.display_size($info['memory_total']).'</li> '; $info['txt'] .= 'memory_total: '.$info['memory_total'].'B (' . display_size($info['memory_total']).') memory_growth: '. $info['memory_growth'].'B ('.display_size($info['memory_growth']).') '; } if (function_exists('memory_get_peak_usage')) { $info['memory_peak'] = memory_get_peak_usage(); $info['html'] .= '<li class="memoryused col-sm-4">RAM peak: '.display_size($info['memory_peak']).'</li> '; $info['txt'] .= 'memory_peak: '.$info['memory_peak'].'B (' . display_size($info['memory_peak']).') '; } $info['html'] .= '</ul><ul class="list-unstyled row mx-md-0">'; $inc = get_included_files(); $info['includecount'] = count($inc); $info['html'] .= '<li class="included col-sm-4">Included '.$info['includecount'].' files</li> '; $info['txt'] .= 'includecount: '.$info['includecount'].' '; if (!empty($CFG->early_install_lang) or empty($PAGE)) { // We can not track more performance before installation or before PAGE init, sorry. return $info; } $filtermanager = filter_manager::instance(); if (method_exists($filtermanager, 'get_performance_summary')) { list($filterinfo, $nicenames) = $filtermanager->get_performance_summary(); $info = array_merge($filterinfo, $info); foreach ($filterinfo as $key => $value) { $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; $info['txt'] .= "$key: $value "; } } $stringmanager = get_string_manager(); if (method_exists($stringmanager, 'get_performance_summary')) { list($filterinfo, $nicenames) = $stringmanager->get_performance_summary(); $info = array_merge($filterinfo, $info); foreach ($filterinfo as $key => $value) { $info['html'] .= "<li class='$key col-sm-4'>$nicenames[$key]: $value </li> "; $info['txt'] .= "$key: $value "; } }
< if (!empty($PERF->logwrites)) { < $info['logwrites'] = $PERF->logwrites; < $info['html'] .= '<li class="logwrites col-sm-4">Log DB writes '.$info['logwrites'].'</li> '; < $info['txt'] .= 'logwrites: '.$info['logwrites'].' '; < } < < $info['dbqueries'] = $DB->perf_get_reads().'/'.($DB->perf_get_writes() - $PERF->logwrites);
> $info['dbqueries'] = $DB->perf_get_reads().'/'.$DB->perf_get_writes();
$info['html'] .= '<li class="dbqueries col-sm-4">DB reads/writes: '.$info['dbqueries'].'</li> '; $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' '; if ($DB->want_read_slave()) { $info['dbreads_slave'] = $DB->perf_get_reads_slave(); $info['html'] .= '<li class="dbqueries col-sm-4">DB reads from slave: '.$info['dbreads_slave'].'</li> '; $info['txt'] .= 'db reads from slave: '.$info['dbreads_slave'].' '; } $info['dbtime'] = round($DB->perf_get_queries_time(), 5); $info['html'] .= '<li class="dbtime col-sm-4">DB queries time: '.$info['dbtime'].' secs</li> '; $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's '; if (function_exists('posix_times')) { $ptimes = posix_times(); if (is_array($ptimes)) { foreach ($ptimes as $key => $val) { $info[$key] = $ptimes[$key] - $PERF->startposixtimes[$key]; } $info['html'] .= "<li class=\"posixtimes col-sm-4\">ticks: $info[ticks] user: $info[utime]"; $info['html'] .= "sys: $info[stime] cuser: $info[cutime] csys: $info[cstime]</li> "; $info['txt'] .= "ticks: $info[ticks] user: $info[utime] sys: $info[stime] cuser: $info[cutime] csys: $info[cstime] "; } } // Grab the load average for the last minute. // /proc will only work under some linux configurations // while uptime is there under MacOSX/Darwin and other unices. if (is_readable('/proc/loadavg') && $loadavg = @file('/proc/loadavg')) { list($serverload) = explode(' ', $loadavg[0]); unset($loadavg); } else if ( function_exists('is_executable') && is_executable('/usr/bin/uptime') && $loadavg = `/usr/bin/uptime` ) { if (preg_match('/load averages?: (\d+[\.,:]\d+)/', $loadavg, $matches)) { $serverload = $matches[1]; } else { trigger_error('Could not parse uptime output!'); } } if (!empty($serverload)) { $info['serverload'] = $serverload; $info['html'] .= '<li class="serverload col-sm-4">Load average: '.$info['serverload'].'</li> '; $info['txt'] .= "serverload: {$info['serverload']} "; } // Display size of session if session started. if ($si = \core\session\manager::get_performance_info()) { $info['sessionsize'] = $si['size']; $info['html'] .= "<li class=\"serverload col-sm-4\">" . $si['html'] . "</li>"; $info['txt'] .= $si['txt']; }
> // Display time waiting for session if applicable. $info['html'] .= '</ul>'; > if (!empty($PERF->sessionlock['wait'])) { $html = ''; > $sessionwait = number_format($PERF->sessionlock['wait'], 3) . ' secs'; if ($stats = cache_helper::get_stats()) { > $info['html'] .= html_writer::tag('li', 'Session wait: ' . $sessionwait, [ > 'class' => 'sessionwait col-sm-4' $table = new html_table(); > ]); $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered'; > $info['txt'] .= 'sessionwait: ' . $sessionwait . ' '; $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S']; > } $table->data = []; >
< $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S'];
> $table->head = ['Mode', 'Cache item', 'Static', 'H', 'M', get_string('mappingprimary', 'cache'), 'H', 'M', 'S', 'I/O'];
< $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right'];
> $table->align = ['left', 'left', 'left', 'right', 'right', 'left', 'right', 'right', 'right', 'right'];
$text = 'Caches used (hits/misses/sets): '; $hits = 0; $misses = 0; $sets = 0; $maxstores = 0; // We want to align static caches into their own column. $hasstatic = false; foreach ($stats as $definition => $details) { $numstores = count($details['stores']); $first = key($details['stores']); if ($first !== cache_store::STATIC_ACCEL) { $numstores++; // Add a blank space for the missing static store. } $maxstores = max($maxstores, $numstores); } $storec = 0; while ($storec++ < ($maxstores - 2)) { if ($storec == ($maxstores - 2)) { $table->head[] = get_string('mappingfinal', 'cache'); } else { $table->head[] = "Store $storec"; } $table->align[] = 'left'; $table->align[] = 'right'; $table->align[] = 'right'; $table->align[] = 'right';
> $table->align[] = 'right';
$table->head[] = 'H'; $table->head[] = 'M'; $table->head[] = 'S';
> $table->head[] = 'I/O';
} ksort($stats); foreach ($stats as $definition => $details) { switch ($details['mode']) { case cache_store::MODE_APPLICATION: $modeclass = 'application'; $mode = ' <span title="application cache">App</span>'; break; case cache_store::MODE_SESSION: $modeclass = 'session'; $mode = ' <span title="session cache">Ses</span>'; break; case cache_store::MODE_REQUEST: $modeclass = 'request'; $mode = ' <span title="request cache">Req</span>'; break; } $row = [$mode, $definition]; $text .= "$definition {"; $storec = 0; foreach ($details['stores'] as $store => $data) { if ($storec == 0 && $store !== cache_store::STATIC_ACCEL) { $row[] = ''; $row[] = ''; $row[] = ''; $storec++; } $hits += $data['hits']; $misses += $data['misses']; $sets += $data['sets']; if ($data['hits'] == 0 and $data['misses'] > 0) { $cachestoreclass = 'nohits bg-danger'; } else if ($data['hits'] < $data['misses']) { $cachestoreclass = 'lowhits bg-warning text-dark'; } else { $cachestoreclass = 'hihits'; } $text .= "$store($data[hits]/$data[misses]/$data[sets]) "; $cell = new html_table_cell($store); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; $cell = new html_table_cell($data['hits']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; $cell = new html_table_cell($data['misses']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; if ($store !== cache_store::STATIC_ACCEL) { // The static cache is never set. $cell = new html_table_cell($data['sets']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell;
> } > if ($data['hits'] || $data['sets']) { $storec++; > if ($data['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) { } > $size = '-'; while ($storec++ < $maxstores) { > } else { $row[] = ''; > $size = display_size($data['iobytes'], 1, 'KB'); $row[] = ''; > if ($data['iobytes'] >= 10 * 1024) { $row[] = ''; > $cachestoreclass = ' bg-warning text-dark'; $row[] = ''; > } } > } $text .= '} '; > } else { > $size = ''; $table->data[] = $row; > } } > $cell = new html_table_cell($size); > $cell->attributes = ['class' => $cachestoreclass]; $html .= html_writer::table($table); > $row[] = $cell;
> $row[] = '';
// Now lets also show sub totals for each cache store. $storetotals = [];
< $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0];
> $storetotal = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
foreach ($stats as $definition => $details) { foreach ($details['stores'] as $store => $data) { if (!array_key_exists($store, $storetotals)) {
< $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0];
> $storetotals[$store] = ['hits' => 0, 'misses' => 0, 'sets' => 0, 'iobytes' => 0];
} $storetotals[$store]['class'] = $data['class']; $storetotals[$store]['hits'] += $data['hits']; $storetotals[$store]['misses'] += $data['misses']; $storetotals[$store]['sets'] += $data['sets']; $storetotal['hits'] += $data['hits']; $storetotal['misses'] += $data['misses']; $storetotal['sets'] += $data['sets'];
> if ($data['iobytes'] !== cache_store::IO_BYTES_NOT_SUPPORTED) { } > $storetotals[$store]['iobytes'] += $data['iobytes']; } > $storetotal['iobytes'] += $data['iobytes']; > }
$table = new html_table(); $table->attributes['class'] = 'cachesused table table-dark table-sm w-auto table-bordered';
< $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S'];
> $table->head = [get_string('storename', 'cache'), get_string('type_cachestore', 'plugin'), 'H', 'M', 'S', 'I/O'];
$table->data = [];
< $table->align = ['left', 'left', 'right', 'right', 'right'];
> $table->align = ['left', 'left', 'right', 'right', 'right', 'right'];
ksort($storetotals); foreach ($storetotals as $store => $data) { $row = []; if ($data['hits'] == 0 and $data['misses'] > 0) { $cachestoreclass = 'nohits bg-danger'; } else if ($data['hits'] < $data['misses']) { $cachestoreclass = 'lowhits bg-warning text-dark'; } else { $cachestoreclass = 'hihits'; } $cell = new html_table_cell($store); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; $cell = new html_table_cell($data['class']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; $cell = new html_table_cell($data['hits']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; $cell = new html_table_cell($data['misses']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell; $cell = new html_table_cell($data['sets']); $cell->attributes = ['class' => $cachestoreclass]; $row[] = $cell;
> if ($data['hits'] || $data['sets']) { $table->data[] = $row; > if ($data['iobytes']) { } > $size = display_size($data['iobytes'], 1, 'KB'); $row = [ > } else { get_string('total'), > $size = '-'; '', > } $storetotal['hits'], > } else { $storetotal['misses'], > $size = ''; $storetotal['sets'], > } ]; > $cell = new html_table_cell($size); $table->data[] = $row; > $cell->attributes = ['class' => $cachestoreclass]; > $row[] = $cell;
$html .= html_writer::table($table);
> if (!empty($storetotal['iobytes'])) { > $size = display_size($storetotal['iobytes'], 1, 'KB'); $info['cachesused'] = "$hits / $misses / $sets"; > } else if (!empty($storetotal['hits']) || !empty($storetotal['sets'])) { $info['html'] .= $html; > $size = '-'; $info['txt'] .= $text.'. '; > } else { } else { > $size = ''; $info['cachesused'] = '0 / 0 / 0'; > }
$info['html'] .= '<div class="cachesused">Caches used (hits/misses/sets): 0/0/0</div>';
> $size,
$info['txt'] .= 'Caches used (hits/misses/sets): 0/0/0 '; }
< $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto mt-3">'.$info['html'].'</div>';
> // Display lock information if any. > if (!empty($PERF->locks)) { > $table = new html_table(); > $table->attributes['class'] = 'locktimings table table-dark table-sm w-auto table-bordered'; > $table->head = ['Lock', 'Waited (s)', 'Obtained', 'Held for (s)']; > $table->align = ['left', 'right', 'center', 'right']; > $table->data = []; > $text = 'Locks (waited/obtained/held):'; > foreach ($PERF->locks as $locktiming) { > $row = []; > $row[] = s($locktiming->type . '/' . $locktiming->resource); > $text .= ' ' . $locktiming->type . '/' . $locktiming->resource . ' ('; > > // The time we had to wait to get the lock. > $roundedtime = number_format($locktiming->wait, 1); > $cell = new html_table_cell($roundedtime); > if ($locktiming->wait > 0.5) { > $cell->attributes = ['class' => 'bg-warning text-dark']; > } > $row[] = $cell; > $text .= $roundedtime . '/'; > > // Show a tick or cross for success. > $row[] = $locktiming->success ? '&#x2713;' : '&#x274c;'; > $text .= ($locktiming->success ? 'y' : 'n') . '/'; > > // If applicable, show how long we held the lock before releasing it. > if (property_exists($locktiming, 'held')) { > $roundedtime = number_format($locktiming->held, 1); > $cell = new html_table_cell($roundedtime); > if ($locktiming->held > 0.5) { > $cell->attributes = ['class' => 'bg-warning text-dark']; > } > $row[] = $cell; > $text .= $roundedtime; > } else { > $row[] = '-'; > $text .= '-'; > } > $text .= ')'; > > $table->data[] = $row; > } > $info['html'] .= html_writer::table($table); > $info['txt'] .= $text . '. '; > } > > $info['html'] = '<div class="performanceinfo siteinfo container-fluid px-md-0 overflow-auto pt-3">'.$info['html'].'</div>';
return $info; } /** * Renames a file or directory to a unique name within the same directory. * * This function is designed to avoid any potential race conditions, and select an unused name. * * @param string $filepath Original filepath * @param string $prefix Prefix to use for the temporary name * @return string|bool New file path or false if failed * @since Moodle 3.10 */ function rename_to_unused_name(string $filepath, string $prefix = '_temp_') { $dir = dirname($filepath); $basename = $dir . '/' . $prefix; $limit = 0; while ($limit < 100) { // Select a new name based on a random number. $newfilepath = $basename . md5(mt_rand()); // Attempt a rename to that new name. if (@rename($filepath, $newfilepath)) { return $newfilepath; } // The first time, do some sanity checks, maybe it is failing for a good reason and there // is no point trying 100 times if so. if ($limit === 0 && (!file_exists($filepath) || !is_writable($dir))) { return false; } $limit++; } return false; } /** * Delete directory or only its content * * @param string $dir directory path * @param bool $contentonly * @return bool success, true also if dir does not exist */ function remove_dir($dir, $contentonly=false) { if (!is_dir($dir)) { // Nothing to do. return true; } if (!$contentonly) { // Start by renaming the directory; this will guarantee that other processes don't write to it // while it is in the process of being deleted. $tempdir = rename_to_unused_name($dir); if ($tempdir) { // If the rename was successful then delete the $tempdir instead. $dir = $tempdir; } // If the rename fails, we will continue through and attempt to delete the directory // without renaming it since that is likely to at least delete most of the files. } if (!$handle = opendir($dir)) { return false; } $result = true; while (false!==($item = readdir($handle))) { if ($item != '.' && $item != '..') { if (is_dir($dir.'/'.$item)) { $result = remove_dir($dir.'/'.$item) && $result; } else { $result = unlink($dir.'/'.$item) && $result; } } } closedir($handle); if ($contentonly) { clearstatcache(); // Make sure file stat cache is properly invalidated. return $result; } $result = rmdir($dir); // If anything left the result will be false, no need for && $result. clearstatcache(); // Make sure file stat cache is properly invalidated. return $result; } /** * Detect if an object or a class contains a given property * will take an actual object or the name of a class *
< * @param mix $obj Name of class or real object to test
> * @param mixed $obj Name of class or real object to test
* @param string $property name of property to find * @return bool true if property exists */ function object_property_exists( $obj, $property ) { if (is_string( $obj )) { $properties = get_class_vars( $obj ); } else { $properties = get_object_vars( $obj ); } return array_key_exists( $property, $properties ); } /** * Converts an object into an associative array * * This function converts an object into an associative array by iterating * over its public properties. Because this function uses the foreach * construct, Iterators are respected. It works recursively on arrays of objects. * Arrays and simple values are returned as is. * * If class has magic properties, it can implement IteratorAggregate * and return all available properties in getIterator() * * @param mixed $var * @return array */ function convert_to_array($var) { $result = array(); // Loop over elements/properties. foreach ($var as $key => $value) { // Recursively convert objects. if (is_object($value) || is_array($value)) { $result[$key] = convert_to_array($value); } else { // Simple values are untouched. $result[$key] = $value; } } return $result; } /** * Detect a custom script replacement in the data directory that will * replace an existing moodle script * * @return string|bool full path name if a custom script exists, false if no custom script exists */ function custom_script_path() { global $CFG, $SCRIPT; if ($SCRIPT === null) { // Probably some weird external script. return false; } $scriptpath = $CFG->customscripts . $SCRIPT; // Check the custom script exists. if (file_exists($scriptpath) and is_file($scriptpath)) { return $scriptpath; } else { return false; } } /** * Returns whether or not the user object is a remote MNET user. This function * is in moodlelib because it does not rely on loading any of the MNET code. * * @param object $user A valid user object * @return bool True if the user is from a remote Moodle. */ function is_mnet_remote_user($user) { global $CFG; if (!isset($CFG->mnet_localhost_id)) { include_once($CFG->dirroot . '/mnet/lib.php'); $env = new mnet_environment(); $env->init(); unset($env); } return (!empty($user->mnethostid) && $user->mnethostid != $CFG->mnet_localhost_id); } /** * This function will search for browser prefereed languages, setting Moodle * to use the best one available if $SESSION->lang is undefined */ function setup_lang_from_browser() { global $CFG, $SESSION, $USER; if (!empty($SESSION->lang) or !empty($USER->lang) or empty($CFG->autolang)) { // Lang is defined in session or user profile, nothing to do. return; } if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { // There isn't list of browser langs, nothing to do. return; } // Extract and clean langs from headers. $rawlangs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; $rawlangs = str_replace('-', '_', $rawlangs); // We are using underscores. $rawlangs = explode(',', $rawlangs); // Convert to array. $langs = array(); $order = 1.0; foreach ($rawlangs as $lang) { if (strpos($lang, ';') === false) { $langs[(string)$order] = $lang; $order = $order-0.01; } else { $parts = explode(';', $lang); $pos = strpos($parts[1], '='); $langs[substr($parts[1], $pos+1)] = $parts[0]; } } krsort($langs, SORT_NUMERIC); // Look for such langs under standard locations. foreach ($langs as $lang) { // Clean it properly for include. $lang = strtolower(clean_param($lang, PARAM_SAFEDIR)); if (get_string_manager()->translation_exists($lang, false)) {
< // Lang exists, set it in session.
> // If the translation for this language exists then try to set it > // for the rest of the session, if this is a read only session then > // we can only set it temporarily in $CFG. > if (defined('READ_ONLY_SESSION') && !empty($CFG->enable_read_only_sessions)) { > $CFG->lang = $lang; > } else {
$SESSION->lang = $lang;
> }
// We have finished. Go out. break; } } return; } /** * Check if $url matches anything in proxybypass list * * Any errors just result in the proxy being used (least bad) * * @param string $url url to check * @return boolean true if we should bypass the proxy */ function is_proxybypass( $url ) { global $CFG; // Sanity check. if (empty($CFG->proxyhost) or empty($CFG->proxybypass)) { return false; } // Get the host part out of the url. if (!$host = parse_url( $url, PHP_URL_HOST )) { return false; } // Get the possible bypass hosts into an array. $matches = explode( ',', $CFG->proxybypass ); // Check for a exact match on the IP or in the domains. $isdomaininallowedlist = \core\ip_utils::is_domain_in_allowed_list($host, $matches); $isipinsubnetlist = \core\ip_utils::is_ip_in_subnet_list($host, $CFG->proxybypass, ','); if ($isdomaininallowedlist || $isipinsubnetlist) { return true; } // Nothing matched. return false; } /** * Check if the passed navigation is of the new style * * @param mixed $navigation * @return bool true for yes false for no */ function is_newnav($navigation) { if (is_array($navigation) && !empty($navigation['newnav'])) { return true; } else { return false; } } /** * Checks whether the given variable name is defined as a variable within the given object. * * This will NOT work with stdClass objects, which have no class variables. * * @param string $var The variable name * @param object $object The object to check * @return boolean */ function in_object_vars($var, $object) { $classvars = get_class_vars(get_class($object)); $classvars = array_keys($classvars); return in_array($var, $classvars); } /** * Returns an array without repeated objects. * This function is similar to array_unique, but for arrays that have objects as values * * @param array $array * @param bool $keepkeyassoc * @return array */ function object_array_unique($array, $keepkeyassoc = true) { $duplicatekeys = array(); $tmp = array(); foreach ($array as $key => $val) { // Convert objects to arrays, in_array() does not support objects. if (is_object($val)) { $val = (array)$val; } if (!in_array($val, $tmp)) { $tmp[] = $val; } else { $duplicatekeys[] = $key; } } foreach ($duplicatekeys as $key) { unset($array[$key]); } return $keepkeyassoc ? $array : array_values($array); } /** * Is a userid the primary administrator? * * @param int $userid int id of user to check * @return boolean */ function is_primary_admin($userid) { $primaryadmin = get_admin(); if ($userid == $primaryadmin->id) { return true; } else { return false; } } /** * Returns the site identifier * * @return string $CFG->siteidentifier, first making sure it is properly initialised. */ function get_site_identifier() { global $CFG; // Check to see if it is missing. If so, initialise it. if (empty($CFG->siteidentifier)) { set_config('siteidentifier', random_string(32) . $_SERVER['HTTP_HOST']); } // Return it. return $CFG->siteidentifier; } /** * Check whether the given password has no more than the specified * number of consecutive identical characters. * * @param string $password password to be checked against the password policy * @param integer $maxchars maximum number of consecutive identical characters * @return bool */ function check_consecutive_identical_characters($password, $maxchars) { if ($maxchars < 1) { return true; // Zero 0 is to disable this check. } if (strlen($password) <= $maxchars) { return true; // Too short to fail this test. } $previouschar = ''; $consecutivecount = 1; foreach (str_split($password) as $char) { if ($char != $previouschar) { $consecutivecount = 1; } else { $consecutivecount++; if ($consecutivecount > $maxchars) { return false; // Check failed already. } } $previouschar = $char; } return true; } /** * Helper function to do partial function binding. * so we can use it for preg_replace_callback, for example * this works with php functions, user functions, static methods and class methods * it returns you a callback that you can pass on like so: * * $callback = partial('somefunction', $arg1, $arg2); * or * $callback = partial(array('someclass', 'somestaticmethod'), $arg1, $arg2); * or even * $obj = new someclass(); * $callback = partial(array($obj, 'somemethod'), $arg1, $arg2); * * and then the arguments that are passed through at calltime are appended to the argument list. * * @param mixed $function a php callback * @param mixed $arg1,... $argv arguments to partially bind with * @return array Array callback */ function partial() { if (!class_exists('partial')) { /** * Used to manage function binding. * @copyright 2009 Penny Leach * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class partial{ /** @var array */ public $values = array(); /** @var string The function to call as a callback. */ public $func; /** * Constructor * @param string $func * @param array $args */ public function __construct($func, $args) { $this->values = $args; $this->func = $func; } /** * Calls the callback function. * @return mixed */ public function method() { $args = func_get_args(); return call_user_func_array($this->func, array_merge($this->values, $args)); } } } $args = func_get_args(); $func = array_shift($args); $p = new partial($func, $args); return array($p, 'method'); } /** * helper function to load up and initialise the mnet environment * this must be called before you use mnet functions. * * @return mnet_environment the equivalent of old $MNET global */ function get_mnet_environment() { global $CFG; require_once($CFG->dirroot . '/mnet/lib.php'); static $instance = null; if (empty($instance)) { $instance = new mnet_environment(); $instance->init(); } return $instance; } /** * during xmlrpc server code execution, any code wishing to access * information about the remote peer must use this to get it. * * @return mnet_remote_client the equivalent of old $MNETREMOTE_CLIENT global */ function get_mnet_remote_client() { if (!defined('MNET_SERVER')) { debugging(get_string('notinxmlrpcserver', 'mnet')); return false; } global $MNET_REMOTE_CLIENT; if (isset($MNET_REMOTE_CLIENT)) { return $MNET_REMOTE_CLIENT; } return false; } /** * during the xmlrpc server code execution, this will be called * to setup the object returned by {@link get_mnet_remote_client} * * @param mnet_remote_client $client the client to set up * @throws moodle_exception */ function set_mnet_remote_client($client) { if (!defined('MNET_SERVER')) { throw new moodle_exception('notinxmlrpcserver', 'mnet'); } global $MNET_REMOTE_CLIENT; $MNET_REMOTE_CLIENT = $client; } /** * return the jump url for a given remote user * this is used for rewriting forum post links in emails, etc * * @param stdclass $user the user to get the idp url for */ function mnet_get_idp_jump_url($user) { global $CFG; static $mnetjumps = array(); if (!array_key_exists($user->mnethostid, $mnetjumps)) { $idp = mnet_get_peer_host($user->mnethostid); $idpjumppath = mnet_get_app_jumppath($idp->applicationid); $mnetjumps[$user->mnethostid] = $idp->wwwroot . $idpjumppath . '?hostwwwroot=' . $CFG->wwwroot . '&wantsurl='; } return $mnetjumps[$user->mnethostid]; } /** * Gets the homepage to use for the current user * * @return int One of HOMEPAGE_* */ function get_home_page() { global $CFG; if (isloggedin() && !isguestuser() && !empty($CFG->defaulthomepage)) {
> // If dashboard is disabled, home will be set to default page. if ($CFG->defaulthomepage == HOMEPAGE_MY) { > $defaultpage = get_default_home_page();
return HOMEPAGE_MY;
> if (!empty($CFG->enabledashboard)) {
} else {
< return (int)get_user_preferences('user_home_page_preference', HOMEPAGE_MY);
> return $defaultpage; > } > } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) { > return HOMEPAGE_MYCOURSES; > } else { > $userhomepage = (int) get_user_preferences('user_home_page_preference', $defaultpage); > if (empty($CFG->enabledashboard) && $userhomepage == HOMEPAGE_MY) { > // If the user was using the dashboard but it's disabled, return the default home page. > $userhomepage = $defaultpage; > } > return $userhomepage;
} } return HOMEPAGE_SITE; } /**
> * Returns the default home page to display if current one is not defined or can't be applied. * Gets the name of a course to be displayed when showing a list of courses. > * The default behaviour is to return Dashboard if it's enabled or My courses page if it isn't. * By default this is just $course->fullname but user can configure it. The > * * result of this function should be passed through print_string. > * @return int The default home page. * @param stdClass|core_course_list_element $course Moodle course object > */ * @return string Display name of course (either fullname or short + fullname) > function get_default_home_page(): int { */ > global $CFG; function get_course_display_name_for_list($course) { > global $CFG; > return (!isset($CFG->enabledashboard) || $CFG->enabledashboard) ? HOMEPAGE_MY : HOMEPAGE_MYCOURSES; if (!empty($CFG->courselistshortnames)) { > } if (!($course instanceof stdClass)) { > $course = (object)convert_to_array($course); > /**
} return get_string('courseextendednamedisplay', '', $course); } else { return $course->fullname; } } /** * Safe analogue of unserialize() that can only parse arrays * * Arrays may contain only integers or strings as both keys and values. Nested arrays are allowed. * * @param string $expression * @return array|bool either parsed array or false if parsing was impossible. */ function unserialize_array($expression) { // Check the expression is an array. if (!preg_match('/^a:(\d+):/', $expression)) { return false; } $values = (array) unserialize_object($expression); // Callback that returns true if the given value is an unserialized object, executes recursively. $invalidvaluecallback = static function($value) use (&$invalidvaluecallback): bool { if (is_array($value)) { return (bool) array_filter($value, $invalidvaluecallback); } return ($value instanceof stdClass) || ($value instanceof __PHP_Incomplete_Class); }; // Iterate over the result to ensure there are no stray objects. if (array_filter($values, $invalidvaluecallback)) { return false; } return $values; } /** * Safe method for unserializing given input that is expected to contain only a serialized instance of an stdClass object * * If any class type other than stdClass is included in the input string, it will not be instantiated and will be cast to an * stdClass object. The initial cast to array, then back to object is to ensure we are always returning the correct type, * otherwise we would return an instances of {@see __PHP_Incomplete_class} for malformed strings * * @param string $input * @return stdClass */ function unserialize_object(string $input): stdClass { $instance = (array) unserialize($input, ['allowed_classes' => [stdClass::class]]); return (object) $instance; } /** * The lang_string class * * This special class is used to create an object representation of a string request. * It is special because processing doesn't occur until the object is first used. * The class was created especially to aid performance in areas where strings were * required to be generated but were not necessarily used. * As an example the admin tree when generated uses over 1500 strings, of which * normally only 1/3 are ever actually printed at any time. * The performance advantage is achieved by not actually processing strings that * arn't being used, as such reducing the processing required for the page. * * How to use the lang_string class? * There are two methods of using the lang_string class, first through the * forth argument of the get_string function, and secondly directly. * The following are examples of both. * 1. Through get_string calls e.g. * $string = get_string($identifier, $component, $a, true); * $string = get_string('yes', 'moodle', null, true); * 2. Direct instantiation * $string = new lang_string($identifier, $component, $a, $lang); * $string = new lang_string('yes'); * * How do I use a lang_string object? * The lang_string object makes use of a magic __toString method so that you * are able to use the object exactly as you would use a string in most cases. * This means you are able to collect it into a variable and then directly * echo it, or concatenate it into another string, or similar. * The other thing you can do is manually get the string by calling the * lang_strings out method e.g. * $string = new lang_string('yes'); * $string->out(); * Also worth noting is that the out method can take one argument, $lang which * allows the developer to change the language on the fly. * * When should I use a lang_string object? * The lang_string object is designed to be used in any situation where a * string may not be needed, but needs to be generated. * The admin tree is a good example of where lang_string objects should be * used. * A more practical example would be any class that requries strings that may * not be printed (after all classes get renderer by renderers and who knows * what they will do ;)) * * When should I not use a lang_string object? * Don't use lang_strings when you are going to use a string immediately. * There is no need as it will be processed immediately and there will be no * advantage, and in fact perhaps a negative hit as a class has to be * instantiated for a lang_string object, however get_string won't require * that. * * Limitations: * 1. You cannot use a lang_string object as an array offset. Doing so will * result in PHP throwing an error. (You can use it as an object property!) * * @package core * @category string * @copyright 2011 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class lang_string { /** @var string The strings identifier */ protected $identifier; /** @var string The strings component. Default '' */ protected $component = ''; /** @var array|stdClass Any arguments required for the string. Default null */ protected $a = null; /** @var string The language to use when processing the string. Default null */ protected $lang = null; /** @var string The processed string (once processed) */ protected $string = null; /** * A special boolean. If set to true then the object has been woken up and * cannot be regenerated. If this is set then $this->string MUST be used. * @var bool */ protected $forcedstring = false; /** * Constructs a lang_string object * * This function should do as little processing as possible to ensure the best * performance for strings that won't be used. * * @param string $identifier The strings identifier * @param string $component The strings component
< * @param stdClass|array $a Any arguments the string requires
> * @param stdClass|array|mixed $a Any arguments the string requires
* @param string $lang The language to use when processing the string. * @throws coding_exception */ public function __construct($identifier, $component = '', $a = null, $lang = null) { if (empty($component)) { $component = 'moodle'; } $this->identifier = $identifier; $this->component = $component; $this->lang = $lang; // We MUST duplicate $a to ensure that it if it changes by reference those // changes are not carried across. // To do this we always ensure $a or its properties/values are strings // and that any properties/values that arn't convertable are forgotten. if ($a !== null) { if (is_scalar($a)) { $this->a = $a; } else if ($a instanceof lang_string) { $this->a = $a->out(); } else if (is_object($a) or is_array($a)) { $a = (array)$a; $this->a = array(); foreach ($a as $key => $value) { // Make sure conversion errors don't get displayed (results in ''). if (is_array($value)) { $this->a[$key] = ''; } else if (is_object($value)) { if (method_exists($value, '__toString')) { $this->a[$key] = $value->__toString(); } else { $this->a[$key] = ''; } } else { $this->a[$key] = (string)$value; } } } } if (debugging(false, DEBUG_DEVELOPER)) { if (clean_param($this->identifier, PARAM_STRINGID) == '') { throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition'); } if (!empty($this->component) && clean_param($this->component, PARAM_COMPONENT) == '') { throw new coding_exception('Invalid string compontent. Please check your string definition'); } if (!get_string_manager()->string_exists($this->identifier, $this->component)) { debugging('String does not exist. Please check your string definition for '.$this->identifier.'/'.$this->component, DEBUG_DEVELOPER); } } } /** * Processes the string. * * This function actually processes the string, stores it in the string property * and then returns it. * You will notice that this function is VERY similar to the get_string method. * That is because it is pretty much doing the same thing. * However as this function is an upgrade it isn't as tolerant to backwards * compatibility. * * @return string * @throws coding_exception */ protected function get_string() { global $CFG; // Check if we need to process the string. if ($this->string === null) { // Check the quality of the identifier. if ($CFG->debugdeveloper && clean_param($this->identifier, PARAM_STRINGID) === '') { throw new coding_exception('Invalid string identifier. Most probably some illegal character is part of the string identifier. Please check your string definition', DEBUG_DEVELOPER); } // Process the string. $this->string = get_string_manager()->get_string($this->identifier, $this->component, $this->a, $this->lang); // Debugging feature lets you display string identifier and component. if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) { $this->string .= ' {' . $this->identifier . '/' . $this->component . '}'; } } // Return the string. return $this->string; } /** * Returns the string * * @param string $lang The langauge to use when processing the string * @return string */ public function out($lang = null) { if ($lang !== null && $lang != $this->lang && ($this->lang == null && $lang != current_language())) { if ($this->forcedstring) { debugging('lang_string objects that have been used cannot be printed in another language. ('.$this->lang.' used)', DEBUG_DEVELOPER); return $this->get_string(); } $translatedstring = new lang_string($this->identifier, $this->component, $this->a, $lang); return $translatedstring->out(); } return $this->get_string(); } /** * Magic __toString method for printing a string * * @return string */ public function __toString() { return $this->get_string(); } /** * Magic __set_state method used for var_export * * @param array $array * @return self */ public static function __set_state(array $array): self { $tmp = new lang_string($array['identifier'], $array['component'], $array['a'], $array['lang']); $tmp->string = $array['string']; $tmp->forcedstring = $array['forcedstring']; return $tmp; } /** * Prepares the lang_string for sleep and stores only the forcedstring and * string properties... the string cannot be regenerated so we need to ensure * it is generated for this. * * @return string */ public function __sleep() { $this->get_string(); $this->forcedstring = true; return array('forcedstring', 'string', 'lang'); } /** * Returns the identifier. * * @return string */ public function get_identifier() { return $this->identifier; } /** * Returns the component. * * @return string */ public function get_component() { return $this->component; } } /** * Get human readable name describing the given callable. * * This performs syntax check only to see if the given param looks like a valid function, method or closure. * It does not check if the callable actually exists. * * @param callable|string|array $callable * @return string|bool Human readable name of callable, or false if not a valid callable. */ function get_callable_name($callable) { if (!is_callable($callable, true, $name)) { return false; } else { return $name; } } /** * Tries to guess if $CFG->wwwroot is publicly accessible or not. * Never put your faith on this function and rely on its accuracy as there might be false positives. * It just performs some simple checks, and mainly is used for places where we want to hide some options * such as site registration when $CFG->wwwroot is not publicly accessible. * Good thing is there is no false negative. * Note that it's possible to force the result of this check by specifying $CFG->site_is_public in config.php * * @return bool */ function site_is_public() { global $CFG; // Return early if site admin has forced this setting. if (isset($CFG->site_is_public)) { return (bool)$CFG->site_is_public; } $host = parse_url($CFG->wwwroot, PHP_URL_HOST); if ($host === 'localhost' || preg_match('|^127\.\d+\.\d+\.\d+$|', $host)) { $ispublic = false; } else if (\core\ip_utils::is_ip_address($host) && !ip_is_public($host)) { $ispublic = false; } else if (($address = \core\ip_utils::get_ip_address($host)) && !ip_is_public($address)) { $ispublic = false; } else { $ispublic = true; } return $ispublic;
> } } > > /** > * Validates user's password length. > * > * @param string $password > * @param int $pepperlength The length of the used peppers > * @return bool > */ > function exceeds_password_length(string $password, int $pepperlength = 0): bool { > return (strlen($password) > (MAX_PASSWORD_CHARACTERS + $pepperlength));