';
+ $ascii .= sprintf("[$bar] %3.1f%% %-22s\n", $percent, $estimate);
+ $ascii .= sprintf("%-80s\n", $msg);
+ return cli_ansi_format($ascii);
+ }
+
+ // If we are not rendering to a tty, ie when piped to another command
+ // or on windows we need to progressively render the progress bar
+ // which can only ever go forwards.
+ $done = round($percent * $size * 0.01);
+ $delta = max(0, $done - $this->progressmaximums[$id]);
+
+ $ascii .= str_repeat('#', $delta);
+ if ($percent >= 100 && $delta > 0) {
+ $ascii .= sprintf("] %3.1f%%", $percent) . "\n$msg\n";
+ }
+ $this->progressmaximums[$id] += $delta;
+ return $ascii;
+ }
+
+ /**
+ * Returns a template fragment representing a Heading.
+ *
+ * @param string $text The text of the heading
+ * @param int $level The level of importance of the heading
+ * @param string $classes A space-separated list of CSS classes
+ * @param string $id An optional ID
+ * @return string A template fragment for a heading
+ */
+ public function heading($text, $level = 2, $classes = 'main', $id = null) {
+ $text .= "\n";
+ switch ($level) {
+ case 1:
+ return '=>' . $text;
+ case 2:
+ return '-->' . $text;
+ default:
+ return $text;
+ }
+ }
+
+ /**
+ * Returns a template fragment representing a fatal error.
+ *
+ * @param string $message The message to output
+ * @param string $moreinfourl URL where more info can be found about the error
+ * @param string $link Link for the Continue button
+ * @param array $backtrace The execution backtrace
+ * @param string $debuginfo Debugging information
+ * @return string A template fragment for a fatal error
+ */
+ public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
+ global $CFG;
+
+ $output = "!!! $message !!!\n";
+
+ if ($CFG->debugdeveloper) {
+ if (!empty($debuginfo)) {
+ $output .= $this->notification($debuginfo, 'notifytiny');
+ }
+ if (!empty($backtrace)) {
+ $output .= $this->notification('Stack trace: ' . format_backtrace($backtrace, true), 'notifytiny');
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Returns a template fragment representing a notification.
+ *
+ * @param string $message The message to print out.
+ * @param string $type The type of notification. See constants on \core\output\notification.
+ * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
+ * @return string A template fragment for a notification
+ */
+ public function notification($message, $type = null, $closebutton = true) {
+ $message = clean_text($message);
+ if ($type === 'notifysuccess' || $type === 'success') {
+ return "++ $message ++\n";
+ }
+ return "!! $message !!\n";
+ }
+
+ /**
+ * There is no footer for a cli request, however we must override the
+ * footer method to prevent the default footer.
+ */
+ public function footer() {}
+
+ /**
+ * Render a notification (that is, a status message about something that has
+ * just happened).
+ *
+ * @param \core\output\notification $notification the notification to print out
+ * @return string plain text output
+ */
+ public function render_notification(\core\output\notification $notification) {
+ return $this->notification($notification->get_message(), $notification->get_message_type());
+ }
+}
diff --git a/lib/classes/output/core_renderer_maintenance.php b/lib/classes/output/core_renderer_maintenance.php
new file mode 100644
index 00000000000..641922cb368
--- /dev/null
+++ b/lib/classes/output/core_renderer_maintenance.php
@@ -0,0 +1,229 @@
+.
+
+/**
+ * The maintenance renderer.
+ *
+ * The purpose of this renderer is to block out the core renderer methods that are not usable when the site
+ * is running a maintenance related task.
+ * It must always extend the core_renderer as we switch from the core_renderer to this renderer in a couple of places.
+ *
+ * @since Moodle 2.6
+ * @package core
+ * @category output
+ * @copyright 2013 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_renderer_maintenance extends core_renderer {
+ /**
+ * Initialises the renderer instance.
+ *
+ * @param moodle_page $page
+ * @param string $target
+ * @throws coding_exception
+ */
+ public function __construct(moodle_page $page, $target) {
+ if ($target !== RENDERER_TARGET_MAINTENANCE || $page->pagelayout !== 'maintenance') {
+ throw new coding_exception('Invalid request for the maintenance renderer.');
+ }
+ parent::__construct($page, $target);
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce blocks.
+ *
+ * @param block_contents $bc
+ * @param string $region
+ * @return string
+ */
+ public function block(block_contents $bc, $region) {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce blocks.
+ *
+ * @param string $region
+ * @param array $classes
+ * @param string $tag
+ * @param boolean $fakeblocksonly
+ * @return string
+ */
+ public function blocks($region, $classes = array(), $tag = 'aside', $fakeblocksonly = false) {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce blocks.
+ *
+ * @param string $region
+ * @param boolean $fakeblocksonly Output fake block only.
+ * @return string
+ */
+ public function blocks_for_region($region, $fakeblocksonly = false) {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce a course content header.
+ *
+ * @param bool $onlyifnotcalledbefore
+ * @return string
+ */
+ public function course_content_header($onlyifnotcalledbefore = false) {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce a course content footer.
+ *
+ * @param bool $onlyifnotcalledbefore
+ * @return string
+ */
+ public function course_content_footer($onlyifnotcalledbefore = false) {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce a course header.
+ *
+ * @return string
+ */
+ public function course_header() {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce a course footer.
+ *
+ * @return string
+ */
+ public function course_footer() {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce a custom menu.
+ *
+ * @param string $custommenuitems
+ * @return string
+ */
+ public function custom_menu($custommenuitems = '') {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce a file picker.
+ *
+ * @param array $options
+ * @return string
+ */
+ public function file_picker($options) {
+ return '';
+ }
+
+ /**
+ * Overridden confirm message for upgrades.
+ *
+ * @param string $message The question to ask the user
+ * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer.
+ * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer.
+ * @param array $displayoptions optional extra display options
+ * @return string HTML fragment
+ */
+ public function confirm($message, $continue, $cancel, array $displayoptions = []) {
+ // We need plain styling of confirm boxes on upgrade because we don't know which stylesheet we have (it could be
+ // from any previous version of Moodle).
+ if ($continue instanceof single_button) {
+ $continue->type = single_button::BUTTON_PRIMARY;
+ } else if (is_string($continue)) {
+ $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post',
+ $displayoptions['type'] ?? single_button::BUTTON_PRIMARY);
+ } else if ($continue instanceof moodle_url) {
+ $continue = new single_button($continue, get_string('continue'), 'post',
+ $displayoptions['type'] ?? single_button::BUTTON_PRIMARY);
+ } else {
+ throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL' .
+ ' (string/moodle_url) or a single_button instance.');
+ }
+
+ if ($cancel instanceof single_button) {
+ $output = '';
+ } else if (is_string($cancel)) {
+ $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
+ } else if ($cancel instanceof moodle_url) {
+ $cancel = new single_button($cancel, get_string('cancel'), 'get');
+ } else {
+ throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL' .
+ ' (string/moodle_url) or a single_button instance.');
+ }
+
+ $output = $this->box_start('generalbox', 'notice');
+ $output .= html_writer::tag('h4', get_string('confirm'));
+ $output .= html_writer::tag('p', $message);
+ $output .= html_writer::tag('div', $this->render($cancel) . $this->render($continue), ['class' => 'buttons']);
+ $output .= $this->box_end();
+ return $output;
+ }
+
+ /**
+ * Does nothing. The maintenance renderer does not support JS.
+ *
+ * @param block_contents $bc
+ */
+ public function init_block_hider_js(block_contents $bc) {
+ // Does nothing.
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce language menus.
+ *
+ * @return string
+ */
+ public function lang_menu() {
+ return '';
+ }
+
+ /**
+ * Does nothing. The maintenance renderer has no need for login information.
+ *
+ * @param mixed $withlinks
+ * @return string
+ */
+ public function login_info($withlinks = null) {
+ return '';
+ }
+
+ /**
+ * Secure login info.
+ *
+ * @return string
+ */
+ public function secure_login_info() {
+ return $this->login_info(false);
+ }
+
+ /**
+ * Does nothing. The maintenance renderer cannot produce user pictures.
+ *
+ * @param stdClass $user
+ * @param array $options
+ * @return string
+ */
+ public function user_picture(stdClass $user, array $options = null) {
+ return '';
+ }
+}
diff --git a/lib/classes/output/custom_menu.php b/lib/classes/output/custom_menu.php
new file mode 100644
index 00000000000..1ed34eea659
--- /dev/null
+++ b/lib/classes/output/custom_menu.php
@@ -0,0 +1,179 @@
+.
+
+/**
+ * Custom menu class
+ *
+ * This class is used to operate a custom menu that can be rendered for the page.
+ * The custom menu is built using $CFG->custommenuitems and is a structured collection
+ * of custom_menu_item nodes that can be rendered by the core renderer.
+ *
+ * To configure the custom menu:
+ * Settings: Administration > Appearance > Advanced theme settings
+ *
+ * @copyright 2010 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class custom_menu extends custom_menu_item {
+
+ /**
+ * @var string The language we should render for, null disables multilang support.
+ */
+ protected $currentlanguage = null;
+
+ /**
+ * Creates the custom menu
+ *
+ * @param string $definition the menu items definition in syntax required by {@link convert_text_to_menu_nodes()}
+ * @param string $currentlanguage the current language code, null disables multilang support
+ */
+ public function __construct($definition = '', $currentlanguage = null) {
+ $this->currentlanguage = $currentlanguage;
+ parent::__construct('root'); // create virtual root element of the menu
+ if (!empty($definition)) {
+ $this->override_children(self::convert_text_to_menu_nodes($definition, $currentlanguage));
+ }
+ }
+
+ /**
+ * Overrides the children of this custom menu. Useful when getting children
+ * from $CFG->custommenuitems
+ *
+ * @param array $children
+ */
+ public function override_children(array $children) {
+ $this->children = array();
+ foreach ($children as $child) {
+ if ($child instanceof custom_menu_item) {
+ $this->children[] = $child;
+ }
+ }
+ }
+
+ /**
+ * Converts a string into a structured array of custom_menu_items which can
+ * then be added to a custom menu.
+ *
+ * Structure:
+ * text|url|title|langs
+ * The number of hyphens at the start determines the depth of the item. The
+ * languages are optional, comma separated list of languages the line is for.
+ *
+ * Example structure:
+ * First level first item|http://www.moodle.com/
+ * -Second level first item|http://www.moodle.com/partners/
+ * -Second level second item|http://www.moodle.com/hq/
+ * --Third level first item|http://www.moodle.com/jobs/
+ * -Second level third item|http://www.moodle.com/development/
+ * First level second item|http://www.moodle.com/feedback/
+ * First level third item
+ * English only|http://moodle.com|English only item|en
+ * German only|http://moodle.de|Deutsch|de,de_du,de_kids
+ *
+ *
+ * @static
+ * @param string $text the menu items definition
+ * @param string $language the language code, null disables multilang support
+ * @return array
+ */
+ public static function convert_text_to_menu_nodes($text, $language = null) {
+ $root = new custom_menu();
+ $lastitem = $root;
+ $lastdepth = 0;
+ $hiddenitems = array();
+ $lines = explode("\n", $text);
+ foreach ($lines as $linenumber => $line) {
+ $line = trim($line);
+ if (strlen($line) == 0) {
+ continue;
+ }
+ // Parse item settings.
+ $itemtext = null;
+ $itemurl = null;
+ $itemtitle = null;
+ $itemvisible = true;
+ $settings = explode('|', $line);
+ foreach ($settings as $i => $setting) {
+ $setting = trim($setting);
+ if ($setting !== '') {
+ switch ($i) {
+ case 0: // Menu text.
+ $itemtext = ltrim($setting, '-');
+ break;
+ case 1: // URL.
+ try {
+ $itemurl = new moodle_url($setting);
+ } catch (moodle_exception $exception) {
+ // We're not actually worried about this, we don't want to mess up the display
+ // just for a wrongly entered URL.
+ $itemurl = null;
+ }
+ break;
+ case 2: // Title attribute.
+ $itemtitle = $setting;
+ break;
+ case 3: // Language.
+ if (!empty($language)) {
+ $itemlanguages = array_map('trim', explode(',', $setting));
+ $itemvisible &= in_array($language, $itemlanguages);
+ }
+ break;
+ }
+ }
+ }
+ // Get depth of new item.
+ preg_match('/^(\-*)/', $line, $match);
+ $itemdepth = strlen($match[1]) + 1;
+ // Find parent item for new item.
+ while (($lastdepth - $itemdepth) >= 0) {
+ $lastitem = $lastitem->get_parent();
+ $lastdepth--;
+ }
+ $lastitem = $lastitem->add($itemtext, $itemurl, $itemtitle, $linenumber + 1);
+ $lastdepth++;
+ if (!$itemvisible) {
+ $hiddenitems[] = $lastitem;
+ }
+ }
+ foreach ($hiddenitems as $item) {
+ $item->parent->remove_child($item);
+ }
+ return $root->get_children();
+ }
+
+ /**
+ * Sorts two custom menu items
+ *
+ * This function is designed to be used with the usort method
+ * usort($this->children, array('custom_menu','sort_custom_menu_items'));
+ *
+ * @static
+ * @param custom_menu_item $itema
+ * @param custom_menu_item $itemb
+ * @return int
+ */
+ public static function sort_custom_menu_items(custom_menu_item $itema, custom_menu_item $itemb) {
+ $itema = $itema->get_sort_order();
+ $itemb = $itemb->get_sort_order();
+ if ($itema == $itemb) {
+ return 0;
+ }
+ return ($itema > $itemb) ? +1 : -1;
+ }
+}
diff --git a/lib/classes/output/custom_menu_item.php b/lib/classes/output/custom_menu_item.php
new file mode 100644
index 00000000000..560cf2a3ec6
--- /dev/null
+++ b/lib/classes/output/custom_menu_item.php
@@ -0,0 +1,261 @@
+.
+
+/**
+ * Custom menu item
+ *
+ * This class is used to represent one item within a custom menu that may or may
+ * not have children.
+ *
+ * @copyright 2010 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class custom_menu_item implements renderable, templatable {
+
+ /**
+ * @var string The text to show for the item
+ */
+ protected $text;
+
+ /**
+ * @var moodle_url The link to give the icon if it has no children
+ */
+ protected $url;
+
+ /**
+ * @var string A title to apply to the item. By default the text
+ */
+ protected $title;
+
+ /**
+ * @var int A sort order for the item, not necessary if you order things in
+ * the CFG var.
+ */
+ protected $sort;
+
+ /**
+ * @var custom_menu_item A reference to the parent for this item or NULL if
+ * it is a top level item
+ */
+ protected $parent;
+
+ /**
+ * @var array A array in which to store children this item has.
+ */
+ protected $children = array();
+
+ /**
+ * @var int A reference to the sort var of the last child that was added
+ */
+ protected $lastsort = 0;
+
+ /** @var array Array of other HTML attributes for the custom menu item. */
+ protected $attributes = [];
+
+ /**
+ * Constructs the new custom menu item
+ *
+ * @param string $text
+ * @param moodle_url $url A moodle url to apply as the link for this item [Optional]
+ * @param string $title A title to apply to this item [Optional]
+ * @param int $sort A sort or to use if we need to sort differently [Optional]
+ * @param custom_menu_item $parent A reference to the parent custom_menu_item this child
+ * belongs to, only if the child has a parent. [Optional]
+ * @param array $attributes Array of other HTML attributes for the custom menu item.
+ */
+ public function __construct($text, moodle_url $url = null, $title = null, $sort = null, custom_menu_item $parent = null,
+ array $attributes = []) {
+
+ // Use class setter method for text to ensure it's always a string type.
+ $this->set_text($text);
+
+ $this->url = $url;
+ $this->title = $title;
+ $this->sort = (int)$sort;
+ $this->parent = $parent;
+ $this->attributes = $attributes;
+ }
+
+ /**
+ * Adds a custom menu item as a child of this node given its properties.
+ *
+ * @param string $text
+ * @param moodle_url $url
+ * @param string $title
+ * @param int $sort
+ * @param array $attributes Array of other HTML attributes for the custom menu item.
+ * @return custom_menu_item
+ */
+ public function add($text, moodle_url $url = null, $title = null, $sort = null, $attributes = []) {
+ $key = count($this->children);
+ if (empty($sort)) {
+ $sort = $this->lastsort + 1;
+ }
+ $this->children[$key] = new custom_menu_item($text, $url, $title, $sort, $this, $attributes);
+ $this->lastsort = (int)$sort;
+ return $this->children[$key];
+ }
+
+ /**
+ * Removes a custom menu item that is a child or descendant to the current menu.
+ *
+ * Returns true if child was found and removed.
+ *
+ * @param custom_menu_item $menuitem
+ * @return bool
+ */
+ public function remove_child(custom_menu_item $menuitem) {
+ $removed = false;
+ if (($key = array_search($menuitem, $this->children)) !== false) {
+ unset($this->children[$key]);
+ $this->children = array_values($this->children);
+ $removed = true;
+ } else {
+ foreach ($this->children as $child) {
+ if ($removed = $child->remove_child($menuitem)) {
+ break;
+ }
+ }
+ }
+ return $removed;
+ }
+
+ /**
+ * Returns the text for this item
+ * @return string
+ */
+ public function get_text() {
+ return $this->text;
+ }
+
+ /**
+ * Returns the url for this item
+ * @return moodle_url
+ */
+ public function get_url() {
+ return $this->url;
+ }
+
+ /**
+ * Returns the title for this item
+ * @return string
+ */
+ public function get_title() {
+ return $this->title;
+ }
+
+ /**
+ * Sorts and returns the children for this item
+ * @return array
+ */
+ public function get_children() {
+ $this->sort();
+ return $this->children;
+ }
+
+ /**
+ * Gets the sort order for this child
+ * @return int
+ */
+ public function get_sort_order() {
+ return $this->sort;
+ }
+
+ /**
+ * Gets the parent this child belong to
+ * @return custom_menu_item
+ */
+ public function get_parent() {
+ return $this->parent;
+ }
+
+ /**
+ * Sorts the children this item has
+ */
+ public function sort() {
+ usort($this->children, array('custom_menu','sort_custom_menu_items'));
+ }
+
+ /**
+ * Returns true if this item has any children
+ * @return bool
+ */
+ public function has_children() {
+ return (count($this->children) > 0);
+ }
+
+ /**
+ * Sets the text for the node
+ * @param string $text
+ */
+ public function set_text($text) {
+ $this->text = (string)$text;
+ }
+
+ /**
+ * Sets the title for the node
+ * @param string $title
+ */
+ public function set_title($title) {
+ $this->title = (string)$title;
+ }
+
+ /**
+ * Sets the url for the node
+ * @param moodle_url $url
+ */
+ public function set_url(moodle_url $url) {
+ $this->url = $url;
+ }
+
+ /**
+ * Export this data so it can be used as the context for a mustache template.
+ *
+ * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ $syscontext = context_system::instance();
+
+ $context = new stdClass();
+ $context->moremenuid = uniqid();
+ $context->text = \core_external\util::format_string($this->text, $syscontext->id);
+ $context->url = $this->url ? $this->url->out() : null;
+ // No need for the title if it's the same with text.
+ if ($this->text !== $this->title) {
+ // Show the title attribute only if it's different from the text.
+ $context->title = \core_external\util::format_string($this->title, $syscontext->id);
+ }
+ $context->sort = $this->sort;
+ if (!empty($this->attributes)) {
+ $context->attributes = $this->attributes;
+ }
+ $context->children = array();
+ if (preg_match("/^#+$/", $this->text)) {
+ $context->divider = true;
+ }
+ $context->haschildren = !empty($this->children) && (count($this->children) > 0);
+ foreach ($this->children as $child) {
+ $child = $child->export_for_template($output);
+ array_push($context->children, $child);
+ }
+
+ return $context;
+ }
+}
diff --git a/lib/classes/output/file_picker.php b/lib/classes/output/file_picker.php
new file mode 100644
index 00000000000..785b0d9a69f
--- /dev/null
+++ b/lib/classes/output/file_picker.php
@@ -0,0 +1,93 @@
+.
+
+/**
+ * Data structure representing a file picker.
+ *
+ * @copyright 2010 Dongsheng Cai
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class file_picker implements renderable {
+
+ /**
+ * @var stdClass An object containing options for the file picker
+ */
+ public $options;
+
+ /**
+ * Constructs a file picker object.
+ *
+ * The following are possible options for the filepicker:
+ * - accepted_types (*)
+ * - return_types (FILE_INTERNAL)
+ * - env (filepicker)
+ * - client_id (uniqid)
+ * - itemid (0)
+ * - maxbytes (-1)
+ * - maxfiles (1)
+ * - buttonname (false)
+ *
+ * @param stdClass $options An object containing options for the file picker.
+ */
+ public function __construct(stdClass $options) {
+ global $CFG, $USER, $PAGE;
+ require_once($CFG->dirroot. '/repository/lib.php');
+ $defaults = array(
+ 'accepted_types'=>'*',
+ 'return_types'=>FILE_INTERNAL,
+ 'env' => 'filepicker',
+ 'client_id' => uniqid(),
+ 'itemid' => 0,
+ 'maxbytes'=>-1,
+ 'maxfiles'=>1,
+ 'buttonname'=>false
+ );
+ foreach ($defaults as $key=>$value) {
+ if (empty($options->$key)) {
+ $options->$key = $value;
+ }
+ }
+
+ $options->currentfile = '';
+ if (!empty($options->itemid)) {
+ $fs = get_file_storage();
+ $usercontext = context_user::instance($USER->id);
+ if (empty($options->filename)) {
+ if ($files = $fs->get_area_files($usercontext->id, 'user', 'draft', $options->itemid, 'id DESC', false)) {
+ $file = reset($files);
+ }
+ } else {
+ $file = $fs->get_file($usercontext->id, 'user', 'draft', $options->itemid, $options->filepath, $options->filename);
+ }
+ if (!empty($file)) {
+ $options->currentfile = html_writer::link(moodle_url::make_draftfile_url($file->get_itemid(), $file->get_filepath(), $file->get_filename()), $file->get_filename());
+ }
+ }
+
+ // initilise options, getting files in root path
+ $this->options = initialise_filepicker($options);
+
+ // copying other options
+ foreach ($options as $name=>$value) {
+ if (!isset($this->options->$name)) {
+ $this->options->$name = $value;
+ }
+ }
+ }
+}
diff --git a/lib/classes/output/help_icon.php b/lib/classes/output/help_icon.php
new file mode 100644
index 00000000000..9fee559cad5
--- /dev/null
+++ b/lib/classes/output/help_icon.php
@@ -0,0 +1,118 @@
+.
+
+/**
+ * Data structure representing a help icon.
+ *
+ * @copyright 2010 Petr Skoda (info@skodak.org)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class help_icon implements renderable, templatable {
+
+ /**
+ * @var string lang pack identifier (without the "_help" suffix),
+ * both get_string($identifier, $component) and get_string($identifier.'_help', $component)
+ * must exist.
+ */
+ public $identifier;
+
+ /**
+ * @var string Component name, the same as in get_string()
+ */
+ public $component;
+
+ /**
+ * @var string Extra descriptive text next to the icon
+ */
+ public $linktext = null;
+
+ /**
+ * @var mixed An object, string or number that can be used within translation strings
+ */
+ public $a = null;
+
+ /**
+ * Constructor
+ *
+ * @param string $identifier string for help page title,
+ * string with _help suffix is used for the actual help text.
+ * string with _link suffix is used to create a link to further info (if it exists)
+ * @param string $component
+ * @param string|object|array|int $a An object, string or number that can be used
+ * within translation strings
+ */
+ public function __construct($identifier, $component, $a = null) {
+ $this->identifier = $identifier;
+ $this->component = $component;
+ $this->a = $a;
+ }
+
+ /**
+ * Verifies that both help strings exists, shows debug warnings if not
+ */
+ public function diag_strings() {
+ $sm = get_string_manager();
+ if (!$sm->string_exists($this->identifier, $this->component)) {
+ debugging("Help title string does not exist: [$this->identifier, $this->component]");
+ }
+ if (!$sm->string_exists($this->identifier.'_help', $this->component)) {
+ debugging("Help contents string does not exist: [{$this->identifier}_help, $this->component]");
+ }
+ }
+
+ /**
+ * Export this data so it can be used as the context for a mustache template.
+ *
+ * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ global $CFG;
+
+ $title = get_string($this->identifier, $this->component, $this->a);
+
+ if (empty($this->linktext)) {
+ $alt = get_string('helpprefix2', '', trim($title, ". \t"));
+ } else {
+ $alt = get_string('helpwiththis');
+ }
+
+ $data = get_formatted_help_string($this->identifier, $this->component, false, $this->a);
+
+ $data->alt = $alt;
+ $data->icon = (new pix_icon('help', $alt, 'core', ['class' => 'iconhelp']))->export_for_template($output);
+ $data->linktext = $this->linktext;
+ $data->title = get_string('helpprefix2', '', trim($title, ". \t"));
+
+ $options = [
+ 'component' => $this->component,
+ 'identifier' => $this->identifier,
+ 'lang' => current_language()
+ ];
+
+ // Debugging feature lets you display string identifier and component.
+ if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
+ $options['strings'] = 1;
+ }
+
+ $data->url = (new moodle_url('/help.php', $options))->out(false);
+ $data->ltr = !right_to_left();
+ return $data;
+ }
+}
diff --git a/lib/classes/output/html_writer.php b/lib/classes/output/html_writer.php
new file mode 100644
index 00000000000..9d595335d32
--- /dev/null
+++ b/lib/classes/output/html_writer.php
@@ -0,0 +1,870 @@
+.
+
+/**
+ * Simple html output class
+ *
+ * @copyright 2009 Tim Hunt, 2010 Petr Skoda
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class html_writer {
+
+ /**
+ * Outputs a tag with attributes and contents
+ *
+ * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
+ * @param string $contents What goes between the opening and closing tags
+ * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
+ * @return string HTML fragment
+ */
+ public static function tag($tagname, $contents, array $attributes = null) {
+ return self::start_tag($tagname, $attributes) . $contents . self::end_tag($tagname);
+ }
+
+ /**
+ * Outputs an opening tag with attributes
+ *
+ * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
+ * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
+ * @return string HTML fragment
+ */
+ public static function start_tag($tagname, array $attributes = null) {
+ return '<' . $tagname . self::attributes($attributes) . '>';
+ }
+
+ /**
+ * Outputs a closing tag
+ *
+ * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
+ * @return string HTML fragment
+ */
+ public static function end_tag($tagname) {
+ return '' . $tagname . '>';
+ }
+
+ /**
+ * Outputs an empty tag with attributes
+ *
+ * @param string $tagname The name of tag ('input', 'img', 'br' etc.)
+ * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
+ * @return string HTML fragment
+ */
+ public static function empty_tag($tagname, array $attributes = null) {
+ return '<' . $tagname . self::attributes($attributes) . ' />';
+ }
+
+ /**
+ * Outputs a tag, but only if the contents are not empty
+ *
+ * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
+ * @param string $contents What goes between the opening and closing tags
+ * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
+ * @return string HTML fragment
+ */
+ public static function nonempty_tag($tagname, $contents, array $attributes = null) {
+ if ($contents === '' || is_null($contents)) {
+ return '';
+ }
+ return self::tag($tagname, $contents, $attributes);
+ }
+
+ /**
+ * Outputs a HTML attribute and value
+ *
+ * @param string $name The name of the attribute ('src', 'href', 'class' etc.)
+ * @param string $value The value of the attribute. The value will be escaped with {@link s()}
+ * @return string HTML fragment
+ */
+ public static function attribute($name, $value) {
+ if ($value instanceof moodle_url) {
+ return ' ' . $name . '="' . $value->out() . '"';
+ }
+
+ // special case, we do not want these in output
+ if ($value === null) {
+ return '';
+ }
+
+ // no sloppy trimming here!
+ return ' ' . $name . '="' . s($value) . '"';
+ }
+
+ /**
+ * Outputs a list of HTML attributes and values
+ *
+ * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
+ * The values will be escaped with {@link s()}
+ * @return string HTML fragment
+ */
+ public static function attributes(array $attributes = null) {
+ $attributes = (array)$attributes;
+ $output = '';
+ foreach ($attributes as $name => $value) {
+ $output .= self::attribute($name, $value);
+ }
+ return $output;
+ }
+
+ /**
+ * Generates a simple image tag with attributes.
+ *
+ * @param string $src The source of image
+ * @param string $alt The alternate text for image
+ * @param array $attributes The tag attributes (array('height' => $max_height, 'class' => 'class1') etc.)
+ * @return string HTML fragment
+ */
+ public static function img($src, $alt, array $attributes = null) {
+ $attributes = (array)$attributes;
+ $attributes['src'] = $src;
+ // In case a null alt text is provided, set it to an empty string.
+ $attributes['alt'] = $alt ?? '';
+ if (array_key_exists('role', $attributes) && core_text::strtolower($attributes['role']) === 'presentation') {
+ // A presentation role is not necessary for the img tag.
+ // If a non-empty alt text is provided, the presentation role will conflict with the alt text.
+ // An empty alt text denotes a decorative image. The presence of a presentation role is redundant.
+ unset($attributes['role']);
+ debugging('The presentation role is not necessary for an img tag.', DEBUG_DEVELOPER);
+ }
+
+ return self::empty_tag('img', $attributes);
+ }
+
+ /**
+ * Generates random html element id.
+ *
+ * @staticvar int $counter
+ * @staticvar string $uniq
+ * @param string $base A string fragment that will be included in the random ID.
+ * @return string A unique ID
+ */
+ public static function random_id($base='random') {
+ static $counter = 0;
+ static $uniq;
+
+ if (!isset($uniq)) {
+ $uniq = uniqid();
+ }
+
+ $counter++;
+ return $base.$uniq.$counter;
+ }
+
+ /**
+ * Generates a simple html link
+ *
+ * @param string|moodle_url $url The URL
+ * @param string $text The text
+ * @param array $attributes HTML attributes
+ * @return string HTML fragment
+ */
+ public static function link($url, $text, array $attributes = null) {
+ $attributes = (array)$attributes;
+ $attributes['href'] = $url;
+ return self::tag('a', $text, $attributes);
+ }
+
+ /**
+ * Generates a simple checkbox with optional label
+ *
+ * @param string $name The name of the checkbox
+ * @param string $value The value of the checkbox
+ * @param bool $checked Whether the checkbox is checked
+ * @param string $label The label for the checkbox
+ * @param array $attributes Any attributes to apply to the checkbox
+ * @param array $labelattributes Any attributes to apply to the label, if present
+ * @return string html fragment
+ */
+ public static function checkbox($name, $value, $checked = true, $label = '',
+ array $attributes = null, array $labelattributes = null) {
+ $attributes = (array) $attributes;
+ $output = '';
+
+ if ($label !== '' and !is_null($label)) {
+ if (empty($attributes['id'])) {
+ $attributes['id'] = self::random_id('checkbox_');
+ }
+ }
+ $attributes['type'] = 'checkbox';
+ $attributes['value'] = $value;
+ $attributes['name'] = $name;
+ $attributes['checked'] = $checked ? 'checked' : null;
+
+ $output .= self::empty_tag('input', $attributes);
+
+ if ($label !== '' and !is_null($label)) {
+ $labelattributes = (array) $labelattributes;
+ $labelattributes['for'] = $attributes['id'];
+ $output .= self::tag('label', $label, $labelattributes);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Generates a simple select yes/no form field
+ *
+ * @param string $name name of select element
+ * @param bool $selected
+ * @param array $attributes - html select element attributes
+ * @return string HTML fragment
+ */
+ public static function select_yes_no($name, $selected=true, array $attributes = null) {
+ $options = array('1'=>get_string('yes'), '0'=>get_string('no'));
+ return self::select($options, $name, $selected, null, $attributes);
+ }
+
+ /**
+ * Generates a simple select form field
+ *
+ * Note this function does HTML escaping on the optgroup labels, but not on the choice labels.
+ *
+ * @param array $options associative array value=>label ex.:
+ * array(1=>'One, 2=>Two)
+ * it is also possible to specify optgroup as complex label array ex.:
+ * array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
+ * array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
+ * @param string $name name of select element
+ * @param string|array $selected value or array of values depending on multiple attribute
+ * @param array|bool|null $nothing add nothing selected option, or false of not added
+ * @param array $attributes html select element attributes
+ * @return string HTML fragment
+ */
+ public static function select(array $options, $name, $selected = '', $nothing = array('' => 'choosedots'), array $attributes = null) {
+ $attributes = (array)$attributes;
+ if (is_array($nothing)) {
+ foreach ($nothing as $k=>$v) {
+ if ($v === 'choose' or $v === 'choosedots') {
+ $nothing[$k] = get_string('choosedots');
+ }
+ }
+ $options = $nothing + $options; // keep keys, do not override
+
+ } else if (is_string($nothing) and $nothing !== '') {
+ // BC
+ $options = array(''=>$nothing) + $options;
+ }
+
+ // we may accept more values if multiple attribute specified
+ $selected = (array)$selected;
+ foreach ($selected as $k=>$v) {
+ $selected[$k] = (string)$v;
+ }
+
+ if (!isset($attributes['id'])) {
+ $id = 'menu'.$name;
+ // name may contaion [], which would make an invalid id. e.g. numeric question type editing form, assignment quickgrading
+ $id = str_replace('[', '', $id);
+ $id = str_replace(']', '', $id);
+ $attributes['id'] = $id;
+ }
+
+ if (!isset($attributes['class'])) {
+ $class = 'menu'.$name;
+ // name may contaion [], which would make an invalid class. e.g. numeric question type editing form, assignment quickgrading
+ $class = str_replace('[', '', $class);
+ $class = str_replace(']', '', $class);
+ $attributes['class'] = $class;
+ }
+ $attributes['class'] = 'select custom-select ' . $attributes['class']; // Add 'select' selector always.
+
+ $attributes['name'] = $name;
+
+ if (!empty($attributes['disabled'])) {
+ $attributes['disabled'] = 'disabled';
+ } else {
+ unset($attributes['disabled']);
+ }
+
+ $output = '';
+ foreach ($options as $value=>$label) {
+ if (is_array($label)) {
+ // ignore key, it just has to be unique
+ $output .= self::select_optgroup(key($label), current($label), $selected);
+ } else {
+ $output .= self::select_option($label, $value, $selected);
+ }
+ }
+ return self::tag('select', $output, $attributes);
+ }
+
+ /**
+ * Returns HTML to display a select box option.
+ *
+ * @param string $label The label to display as the option.
+ * @param string|int $value The value the option represents
+ * @param array $selected An array of selected options
+ * @return string HTML fragment
+ */
+ private static function select_option($label, $value, array $selected) {
+ $attributes = array();
+ $value = (string)$value;
+ if (in_array($value, $selected, true)) {
+ $attributes['selected'] = 'selected';
+ }
+ $attributes['value'] = $value;
+ return self::tag('option', $label, $attributes);
+ }
+
+ /**
+ * Returns HTML to display a select box option group.
+ *
+ * @param string $groupname The label to use for the group
+ * @param array $options The options in the group
+ * @param array $selected An array of selected values.
+ * @return string HTML fragment.
+ */
+ private static function select_optgroup($groupname, $options, array $selected) {
+ if (empty($options)) {
+ return '';
+ }
+ $attributes = array('label'=>$groupname);
+ $output = '';
+ foreach ($options as $value=>$label) {
+ $output .= self::select_option($label, $value, $selected);
+ }
+ return self::tag('optgroup', $output, $attributes);
+ }
+
+ /**
+ * This is a shortcut for making an hour selector menu.
+ *
+ * @param string $type The type of selector (years, months, days, hours, minutes)
+ * @param string $name fieldname
+ * @param int $currenttime A default timestamp in GMT
+ * @param int $step minute spacing
+ * @param array $attributes - html select element attributes
+ * @param float|int|string $timezone the timezone to use to calculate the time
+ * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
+ * @return string HTML fragment
+ */
+ public static function select_time($type, $name, $currenttime = 0, $step = 5, array $attributes = null, $timezone = 99) {
+ global $OUTPUT;
+
+ if (!$currenttime) {
+ $currenttime = time();
+ }
+ $calendartype = \core_calendar\type_factory::get_calendar_instance();
+ $currentdate = $calendartype->timestamp_to_date_array($currenttime, $timezone);
+
+ $userdatetype = $type;
+ $timeunits = array();
+
+ switch ($type) {
+ case 'years':
+ $timeunits = $calendartype->get_years();
+ $userdatetype = 'year';
+ break;
+ case 'months':
+ $timeunits = $calendartype->get_months();
+ $userdatetype = 'month';
+ $currentdate['month'] = (int)$currentdate['mon'];
+ break;
+ case 'days':
+ $timeunits = $calendartype->get_days();
+ $userdatetype = 'mday';
+ break;
+ case 'hours':
+ for ($i=0; $i<=23; $i++) {
+ $timeunits[$i] = sprintf("%02d",$i);
+ }
+ break;
+ case 'minutes':
+ if ($step != 1) {
+ $currentdate['minutes'] = ceil($currentdate['minutes']/$step)*$step;
+ }
+
+ for ($i=0; $i<=59; $i+=$step) {
+ $timeunits[$i] = sprintf("%02d",$i);
+ }
+ break;
+ default:
+ throw new coding_exception("Time type $type is not supported by html_writer::select_time().");
+ }
+
+ $attributes = (array) $attributes;
+ $data = (object) [
+ 'name' => $name,
+ 'id' => !empty($attributes['id']) ? $attributes['id'] : self::random_id('ts_'),
+ 'label' => get_string(substr($type, 0, -1), 'form'),
+ 'options' => array_map(function($value) use ($timeunits, $currentdate, $userdatetype) {
+ return [
+ 'name' => $timeunits[$value],
+ 'value' => $value,
+ 'selected' => $currentdate[$userdatetype] == $value
+ ];
+ }, array_keys($timeunits)),
+ ];
+
+ unset($attributes['id']);
+ unset($attributes['name']);
+ $data->attributes = array_map(function($name) use ($attributes) {
+ return [
+ 'name' => $name,
+ 'value' => $attributes[$name]
+ ];
+ }, array_keys($attributes));
+
+ return $OUTPUT->render_from_template('core/select_time', $data);
+ }
+
+ /**
+ * Shortcut for quick making of lists
+ *
+ * Note: 'list' is a reserved keyword ;-)
+ *
+ * @param array $items
+ * @param array $attributes
+ * @param string $tag ul or ol
+ * @return string
+ */
+ public static function alist(array $items, array $attributes = null, $tag = 'ul') {
+ $output = html_writer::start_tag($tag, $attributes)."\n";
+ foreach ($items as $item) {
+ $output .= html_writer::tag('li', $item)."\n";
+ }
+ $output .= html_writer::end_tag($tag);
+ return $output;
+ }
+
+ /**
+ * Returns hidden input fields created from url parameters.
+ *
+ * @param moodle_url $url
+ * @param array $exclude list of excluded parameters
+ * @return string HTML fragment
+ */
+ public static function input_hidden_params(moodle_url $url, array $exclude = null) {
+ $exclude = (array)$exclude;
+ $params = $url->params();
+ foreach ($exclude as $key) {
+ unset($params[$key]);
+ }
+
+ $output = '';
+ foreach ($params as $key => $value) {
+ $attributes = array('type'=>'hidden', 'name'=>$key, 'value'=>$value);
+ $output .= self::empty_tag('input', $attributes)."\n";
+ }
+ return $output;
+ }
+
+ /**
+ * Generate a script tag containing the the specified code.
+ *
+ * @param string $jscode the JavaScript code
+ * @param moodle_url|string $url optional url of the external script, $code ignored if specified
+ * @return string HTML, the code wrapped in ';
+ } else {
+ $code = '';
+ foreach ($baserollups as $rollup) {
+ $code .= '';
+ }
+ return $code;
+ }
+
+ }
+
+ /**
+ * Returns html tags needed for inclusion of theme CSS.
+ *
+ * @return string
+ */
+ protected function get_css_code() {
+ // First of all the theme CSS, then any custom CSS
+ // Please note custom CSS is strongly discouraged,
+ // because it can not be overridden by themes!
+ // It is suitable only for things like mod/data which accepts CSS from teachers.
+ $attributes = array('rel'=>'stylesheet', 'type'=>'text/css');
+
+ // Add the YUI code first. We want this to be overridden by any Moodle CSS.
+ $code = $this->get_yui3lib_headcss();
+
+ // This line of code may look funny but it is currently required in order
+ // to avoid MASSIVE display issues in Internet Explorer.
+ // As of IE8 + YUI3.1.1 the reference stylesheet (firstthemesheet) gets
+ // ignored whenever another resource is added until such time as a redraw
+ // is forced, usually by moving the mouse over the affected element.
+ $code .= html_writer::tag('script', '/** Required in order to fix style inclusion problems in IE with YUI **/', array('id'=>'firstthemesheet', 'type'=>'text/css'));
+
+ $urls = $this->cssthemeurls + $this->cssurls;
+ foreach ($urls as $url) {
+ $attributes['href'] = $url;
+ $code .= html_writer::empty_tag('link', $attributes) . "\n";
+ // This id is needed in first sheet only so that theme may override YUI sheets loaded on the fly.
+ unset($attributes['id']);
+ }
+
+ return $code;
+ }
+
+ /**
+ * Adds extra modules specified after printing of page header.
+ *
+ * @return string
+ */
+ protected function get_extra_modules_code() {
+ if (empty($this->extramodules)) {
+ return '';
+ }
+ return html_writer::script(js_writer::function_call('M.yui.add_module', array($this->extramodules)));
+ }
+
+ /**
+ * Generate any HTML that needs to go inside the tag.
+ *
+ * Normally, this method is called automatically by the code that prints the
+ * tag. You should not normally need to call it in your own code.
+ *
+ * @param moodle_page $page
+ * @param core_renderer $renderer
+ * @return string the HTML code to to inside the tag.
+ */
+ public function get_head_code(moodle_page $page, core_renderer $renderer) {
+ global $CFG;
+
+ // Note: the $page and $output are not stored here because it would
+ // create circular references in memory which prevents garbage collection.
+ $this->init_requirements_data($page, $renderer);
+
+ $output = '';
+
+ // Add all standard CSS for this page.
+ $output .= $this->get_css_code();
+
+ // Set up the M namespace.
+ $js = "var M = {}; M.yui = {};\n";
+
+ // Capture the time now ASAP during page load. This minimises the lag when
+ // we try to relate times on the server to times in the browser.
+ // An example of where this is used is the quiz countdown timer.
+ $js .= "M.pageloadstarttime = new Date();\n";
+
+ // Add a subset of Moodle configuration to the M namespace.
+ $js .= js_writer::set_variable('M.cfg', $this->M_cfg, false);
+
+ // Set up global YUI3 loader object - this should contain all code needed by plugins.
+ // Note: in JavaScript just use "YUI().use('overlay', function(Y) { .... });",
+ // this needs to be done before including any other script.
+ $js .= $this->YUI_config->get_config_functions();
+ $js .= js_writer::set_variable('YUI_config', $this->YUI_config, false) . "\n";
+ $js .= "M.yui.loader = {modules: {}};\n"; // Backwards compatibility only, not used any more.
+ $js = $this->YUI_config->update_header_js($js);
+
+ $output .= html_writer::script($js);
+
+ // Add variables.
+ if ($this->jsinitvariables['head']) {
+ $js = '';
+ foreach ($this->jsinitvariables['head'] as $data) {
+ list($var, $value) = $data;
+ $js .= js_writer::set_variable($var, $value, true);
+ }
+ $output .= html_writer::script($js);
+ }
+
+ // Mark head sending done, it is not possible to anything there.
+ $this->headdone = true;
+
+ return $output;
+ }
+
+ /**
+ * Generate any HTML that needs to go at the start of the tag.
+ *
+ * Normally, this method is called automatically by the code that prints the
+ * tag. You should not normally need to call it in your own code.
+ *
+ * @param renderer_base $renderer
+ * @return string the HTML code to go at the start of the tag.
+ */
+ public function get_top_of_body_code(renderer_base $renderer) {
+ global $CFG;
+
+ // First the skip links.
+ $output = $renderer->render_skip_links($this->skiplinks);
+
+ // Include the Polyfills.
+ $output .= html_writer::script('', $this->js_fix_url('/lib/polyfills/polyfill.js'));
+
+ // YUI3 JS needs to be loaded early in the body. It should be cached well by the browser.
+ $output .= $this->get_yui3lib_headcode();
+
+ // Add hacked jQuery support, it is not intended for standard Moodle distribution!
+ $output .= $this->get_jquery_headcode();
+
+ // Link our main JS file, all core stuff should be there.
+ $output .= html_writer::script('', $this->js_fix_url('/lib/javascript-static.js'));
+
+ // All the other linked things from HEAD - there should be as few as possible.
+ if ($this->jsincludes['head']) {
+ foreach ($this->jsincludes['head'] as $url) {
+ $output .= html_writer::script('', $url);
+ }
+ }
+
+ // Then the clever trick for hiding of things not needed when JS works.
+ $output .= html_writer::script("document.body.className += ' jsenabled';") . "\n";
+ $this->topofbodydone = true;
+ return $output;
+ }
+
+ /**
+ * Generate any HTML that needs to go at the end of the page.
+ *
+ * Normally, this method is called automatically by the code that prints the
+ * page footer. You should not normally need to call it in your own code.
+ *
+ * @return string the HTML code to to at the end of the page.
+ */
+ public function get_end_code() {
+ global $CFG;
+ $output = '';
+
+ // Set the log level for the JS logging.
+ $logconfig = new stdClass();
+ $logconfig->level = 'warn';
+ if ($CFG->debugdeveloper) {
+ $logconfig->level = 'trace';
+ }
+ $this->js_call_amd('core/log', 'setConfig', array($logconfig));
+ // Add any global JS that needs to run on all pages.
+ $this->js_call_amd('core/page_global', 'init');
+ $this->js_call_amd('core/utility');
+
+ // Call amd init functions.
+ $output .= $this->get_amd_footercode();
+
+ // Add other requested modules.
+ $output .= $this->get_extra_modules_code();
+
+ $this->js_init_code('M.util.js_complete("init");', true);
+
+ // All the other linked scripts - there should be as few as possible.
+ if ($this->jsincludes['footer']) {
+ foreach ($this->jsincludes['footer'] as $url) {
+ $output .= html_writer::script('', $url);
+ }
+ }
+
+ // Add all needed strings.
+ // First add core strings required for some dialogues.
+ $this->strings_for_js(array(
+ 'confirm',
+ 'yes',
+ 'no',
+ 'areyousure',
+ 'closebuttontitle',
+ 'unknownerror',
+ 'error',
+ 'file',
+ 'url',
+ // TODO MDL-70830 shortforms should preload the collapseall/expandall strings properly.
+ 'collapseall',
+ 'expandall',
+ ), 'moodle');
+ $this->strings_for_js(array(
+ 'debuginfo',
+ 'line',
+ 'stacktrace',
+ ), 'debug');
+ $this->string_for_js('labelsep', 'langconfig');
+ if (!empty($this->stringsforjs)) {
+ $strings = array();
+ foreach ($this->stringsforjs as $component=>$v) {
+ foreach($v as $indentifier => $langstring) {
+ $strings[$component][$indentifier] = $langstring->out();
+ }
+ }
+ $output .= html_writer::script(js_writer::set_variable('M.str', $strings));
+ }
+
+ // Add variables.
+ if ($this->jsinitvariables['footer']) {
+ $js = '';
+ foreach ($this->jsinitvariables['footer'] as $data) {
+ list($var, $value) = $data;
+ $js .= js_writer::set_variable($var, $value, true);
+ }
+ $output .= html_writer::script($js);
+ }
+
+ $inyuijs = $this->get_javascript_code(false);
+ $ondomreadyjs = $this->get_javascript_code(true);
+ $jsinit = $this->get_javascript_init_code();
+ $handlersjs = $this->get_event_handler_code();
+
+ // There is a global Y, make sure it is available in your scope.
+ $js = "(function() {{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}})();";
+
+ $output .= html_writer::script($js);
+
+ return $output;
+ }
+
+ /**
+ * Have we already output the code in the tag?
+ *
+ * @return bool
+ */
+ public function is_head_done() {
+ return $this->headdone;
+ }
+
+ /**
+ * Have we already output the code at the start of the tag?
+ *
+ * @return bool
+ */
+ public function is_top_of_body_done() {
+ return $this->topofbodydone;
+ }
+
+ /**
+ * Should we generate a bit of content HTML that is only required once on
+ * this page (e.g. the contents of the modchooser), now? Basically, we call
+ * {@link has_one_time_item_been_created()}, and if the thing has not already
+ * been output, we return true to tell the caller to generate it, and also
+ * call {@link set_one_time_item_created()} to record the fact that it is
+ * about to be generated.
+ *
+ * That is, a typical usage pattern (in a renderer method) is:
+ *
+ * if (!$this->page->requires->should_create_one_time_item_now($thing)) {
+ * return '';
+ * }
+ * // Else generate it.
+ *
+ *
+ * @param string $thing identifier for the bit of content. Should be of the form
+ * frankenstyle_things, e.g. core_course_modchooser.
+ * @return bool if true, the caller should generate that bit of output now, otherwise don't.
+ */
+ public function should_create_one_time_item_now($thing) {
+ if ($this->has_one_time_item_been_created($thing)) {
+ return false;
+ }
+
+ $this->set_one_time_item_created($thing);
+ return true;
+ }
+
+ /**
+ * Has a particular bit of HTML that is only required once on this page
+ * (e.g. the contents of the modchooser) already been generated?
+ *
+ * Normally, you can use the {@link should_create_one_time_item_now()} helper
+ * method rather than calling this method directly.
+ *
+ * @param string $thing identifier for the bit of content. Should be of the form
+ * frankenstyle_things, e.g. core_course_modchooser.
+ * @return bool whether that bit of output has been created.
+ */
+ public function has_one_time_item_been_created($thing) {
+ return isset($this->onetimeitemsoutput[$thing]);
+ }
+
+ /**
+ * Indicate that a particular bit of HTML that is only required once on this
+ * page (e.g. the contents of the modchooser) has been generated (or is about to be)?
+ *
+ * Normally, you can use the {@link should_create_one_time_item_now()} helper
+ * method rather than calling this method directly.
+ *
+ * @param string $thing identifier for the bit of content. Should be of the form
+ * frankenstyle_things, e.g. core_course_modchooser.
+ */
+ public function set_one_time_item_created($thing) {
+ if ($this->has_one_time_item_been_created($thing)) {
+ throw new coding_exception($thing . ' is only supposed to be ouput ' .
+ 'once per page, but it seems to be being output again.');
+ }
+ return $this->onetimeitemsoutput[$thing] = true;
+ }
+}
diff --git a/lib/classes/output/requirements/yui.php b/lib/classes/output/requirements/yui.php
new file mode 100644
index 00000000000..272753aa4fe
--- /dev/null
+++ b/lib/classes/output/requirements/yui.php
@@ -0,0 +1,351 @@
+.
+
+/**
+ * This class represents the YUI configuration.
+ *
+ * @copyright 2013 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.5
+ * @package core
+ * @category output
+ */
+class YUI_config {
+ /**
+ * These settings must be public so that when the object is converted to json they are exposed.
+ * Note: Some of these are camelCase because YUI uses camelCase variable names.
+ *
+ * The settings are described and documented in the YUI API at:
+ * - http://yuilibrary.com/yui/docs/api/classes/config.html
+ * - http://yuilibrary.com/yui/docs/api/classes/Loader.html
+ */
+ public $debug = false;
+ public $base;
+ public $comboBase;
+ public $combine;
+ public $filter = null;
+ public $insertBefore = 'firstthemesheet';
+ public $groups = array();
+ public $modules = array();
+ /** @var array The log sources that should be not be logged. */
+ public $logInclude = [];
+ /** @var array Tog sources that should be logged. */
+ public $logExclude = [];
+ /** @var string The minimum log level for YUI logging statements. */
+ public $logLevel;
+
+ /**
+ * @var array List of functions used by the YUI Loader group pattern recognition.
+ */
+ protected $jsconfigfunctions = array();
+
+ /**
+ * Create a new group within the YUI_config system.
+ *
+ * @param string $name The name of the group. This must be unique and
+ * not previously used.
+ * @param array $config The configuration for this group.
+ * @return void
+ */
+ public function add_group($name, $config) {
+ if (isset($this->groups[$name])) {
+ throw new coding_exception("A YUI configuration group for '{$name}' already exists. To make changes to this group use YUI_config->update_group().");
+ }
+ $this->groups[$name] = $config;
+ }
+
+ /**
+ * Update an existing group configuration
+ *
+ * Note, any existing configuration for that group will be wiped out.
+ * This includes module configuration.
+ *
+ * @param string $name The name of the group. This must be unique and
+ * not previously used.
+ * @param array $config The configuration for this group.
+ * @return void
+ */
+ public function update_group($name, $config) {
+ if (!isset($this->groups[$name])) {
+ throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.');
+ }
+ $this->groups[$name] = $config;
+ }
+
+ /**
+ * Set the value of a configuration function used by the YUI Loader's pattern testing.
+ *
+ * Only the body of the function should be passed, and not the whole function wrapper.
+ *
+ * The JS function your write will be passed a single argument 'name' containing the
+ * name of the module being loaded.
+ *
+ * @param $function String the body of the JavaScript function. This should be used i
+ * @return string the name of the function to use in the group pattern configuration.
+ */
+ public function set_config_function($function) {
+ $configname = 'yui' . (count($this->jsconfigfunctions) + 1) . 'ConfigFn';
+ if (isset($this->jsconfigfunctions[$configname])) {
+ throw new coding_exception("A YUI config function with this name already exists. Config function names must be unique.");
+ }
+ $this->jsconfigfunctions[$configname] = $function;
+ return '@' . $configname . '@';
+ }
+
+ /**
+ * Allow setting of the config function described in {@see set_config_function} from a file.
+ * The contents of this file are then passed to set_config_function.
+ *
+ * When jsrev is positive, the function is minified and stored in a MUC cache for subsequent uses.
+ *
+ * @param $file The path to the JavaScript function used for YUI configuration.
+ * @return string the name of the function to use in the group pattern configuration.
+ */
+ public function set_config_source($file) {
+ global $CFG;
+ $cache = cache::make('core', 'yuimodules');
+
+ // Attempt to get the metadata from the cache.
+ $keyname = 'configfn_' . $file;
+ $fullpath = $CFG->dirroot . '/' . $file;
+ if (!isset($CFG->jsrev) || $CFG->jsrev == -1) {
+ $cache->delete($keyname);
+ $configfn = file_get_contents($fullpath);
+ } else {
+ $configfn = $cache->get($keyname);
+ if ($configfn === false) {
+ require_once($CFG->libdir . '/jslib.php');
+ $configfn = core_minify::js_files(array($fullpath));
+ $cache->set($keyname, $configfn);
+ }
+ }
+ return $this->set_config_function($configfn);
+ }
+
+ /**
+ * Retrieve the list of JavaScript functions for YUI_config groups.
+ *
+ * @return string The complete set of config functions
+ */
+ public function get_config_functions() {
+ $configfunctions = '';
+ foreach ($this->jsconfigfunctions as $functionname => $function) {
+ $configfunctions .= "var {$functionname} = function(me) {";
+ $configfunctions .= $function;
+ $configfunctions .= "};\n";
+ }
+ return $configfunctions;
+ }
+
+ /**
+ * Update the header JavaScript with any required modification for the YUI Loader.
+ *
+ * @param $js String The JavaScript to manipulate.
+ * @return string the modified JS string.
+ */
+ public function update_header_js($js) {
+ // Update the names of the the configFn variables.
+ // The PHP json_encode function cannot handle literal names so we have to wrap
+ // them in @ and then replace them with literals of the same function name.
+ foreach ($this->jsconfigfunctions as $functionname => $function) {
+ $js = str_replace('"@' . $functionname . '@"', $functionname, $js);
+ }
+ return $js;
+ }
+
+ /**
+ * Add configuration for a specific module.
+ *
+ * @param string $name The name of the module to add configuration for.
+ * @param array $config The configuration for the specified module.
+ * @param string $group The name of the group to add configuration for.
+ * If not specified, then this module is added to the global
+ * configuration.
+ * @return void
+ */
+ public function add_module_config($name, $config, $group = null) {
+ if ($group) {
+ if (!isset($this->groups[$name])) {
+ throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.');
+ }
+ if (!isset($this->groups[$group]['modules'])) {
+ $this->groups[$group]['modules'] = array();
+ }
+ $modules = &$this->groups[$group]['modules'];
+ } else {
+ $modules = &$this->modules;
+ }
+ $modules[$name] = $config;
+ }
+
+ /**
+ * Add the moodle YUI module metadata for the moodle group to the YUI_config instance.
+ *
+ * If js caching is disabled, metadata will not be served causing YUI to calculate
+ * module dependencies as each module is loaded.
+ *
+ * If metadata does not exist it will be created and stored in a MUC entry.
+ *
+ * @return void
+ */
+ public function add_moodle_metadata() {
+ global $CFG;
+ if (!isset($this->groups['moodle'])) {
+ throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.');
+ }
+
+ if (!isset($this->groups['moodle']['modules'])) {
+ $this->groups['moodle']['modules'] = array();
+ }
+
+ $cache = cache::make('core', 'yuimodules');
+ if (!isset($CFG->jsrev) || $CFG->jsrev == -1) {
+ $metadata = array();
+ $metadata = $this->get_moodle_metadata();
+ $cache->delete('metadata');
+ } else {
+ // Attempt to get the metadata from the cache.
+ if (!$metadata = $cache->get('metadata')) {
+ $metadata = $this->get_moodle_metadata();
+ $cache->set('metadata', $metadata);
+ }
+ }
+
+ // Merge with any metadata added specific to this page which was added manually.
+ $this->groups['moodle']['modules'] = array_merge($this->groups['moodle']['modules'],
+ $metadata);
+ }
+
+ /**
+ * Determine the module metadata for all moodle YUI modules.
+ *
+ * This works through all modules capable of serving YUI modules, and attempts to get
+ * metadata for each of those modules.
+ *
+ * @return array of module metadata
+ */
+ private function get_moodle_metadata() {
+ $moodlemodules = array();
+ // Core isn't a plugin type or subsystem - handle it seperately.
+ if ($module = $this->get_moodle_path_metadata(core_component::get_component_directory('core'))) {
+ $moodlemodules = array_merge($moodlemodules, $module);
+ }
+
+ // Handle other core subsystems.
+ $subsystems = core_component::get_core_subsystems();
+ foreach ($subsystems as $subsystem => $path) {
+ if (is_null($path)) {
+ continue;
+ }
+ if ($module = $this->get_moodle_path_metadata($path)) {
+ $moodlemodules = array_merge($moodlemodules, $module);
+ }
+ }
+
+ // And finally the plugins.
+ $plugintypes = core_component::get_plugin_types();
+ foreach ($plugintypes as $plugintype => $pathroot) {
+ $pluginlist = core_component::get_plugin_list($plugintype);
+ foreach ($pluginlist as $plugin => $path) {
+ if ($module = $this->get_moodle_path_metadata($path)) {
+ $moodlemodules = array_merge($moodlemodules, $module);
+ }
+ }
+ }
+
+ return $moodlemodules;
+ }
+
+ /**
+ * Helper function process and return the YUI metadata for all of the modules under the specified path.
+ *
+ * @param string $path the UNC path to the YUI src directory.
+ * @return array the complete array for frankenstyle directory.
+ */
+ private function get_moodle_path_metadata($path) {
+ // Add module metadata is stored in frankenstyle_modname/yui/src/yui_modname/meta/yui_modname.json.
+ $baseyui = $path . '/yui/src';
+ $modules = array();
+ if (is_dir($baseyui)) {
+ $items = new DirectoryIterator($baseyui);
+ foreach ($items as $item) {
+ if ($item->isDot() or !$item->isDir()) {
+ continue;
+ }
+ $metafile = realpath($baseyui . '/' . $item . '/meta/' . $item . '.json');
+ if (!is_readable($metafile)) {
+ continue;
+ }
+ $metadata = file_get_contents($metafile);
+ $modules = array_merge($modules, (array) json_decode($metadata));
+ }
+ }
+ return $modules;
+ }
+
+ /**
+ * Define YUI modules which we have been required to patch between releases.
+ *
+ * We must do this because we aggressively cache content on the browser, and we must also override use of the
+ * external CDN which will serve the true authoritative copy of the code without our patches.
+ *
+ * @param string $combobase The local combobase
+ * @param string $yuiversion The current YUI version
+ * @param int $patchlevel The patch level we're working to for YUI
+ * @param array $patchedmodules An array containing the names of the patched modules
+ * @return void
+ */
+ public function define_patched_core_modules($combobase, $yuiversion, $patchlevel, $patchedmodules) {
+ // The version we use is suffixed with a patchlevel so that we can get additional revisions between YUI releases.
+ $subversion = $yuiversion . '_' . $patchlevel;
+
+ if ($this->comboBase == $combobase) {
+ // If we are using the local combobase in the loader, we can add a group and still make use of the combo
+ // loader. We just need to specify a different root which includes a slightly different YUI version number
+ // to include our patchlevel.
+ $patterns = array();
+ $modules = array();
+ foreach ($patchedmodules as $modulename) {
+ // We must define the pattern and module here so that the loader uses our group configuration instead of
+ // the standard module definition. We may lose some metadata provided by upstream but this will be
+ // loaded when the module is loaded anyway.
+ $patterns[$modulename] = array(
+ 'group' => 'yui-patched',
+ );
+ $modules[$modulename] = array();
+ }
+
+ // Actually add the patch group here.
+ $this->add_group('yui-patched', array(
+ 'combine' => true,
+ 'root' => $subversion . '/',
+ 'patterns' => $patterns,
+ 'modules' => $modules,
+ ));
+
+ } else {
+ // The CDN is in use - we need to instead use the local combobase for this module and override the modules
+ // definition. We cannot use the local base - we must use the combobase because we cannot invalidate the
+ // local base in browser caches.
+ $fullpathbase = $combobase . $subversion . '/';
+ foreach ($patchedmodules as $modulename) {
+ $this->modules[$modulename] = array(
+ 'fullpath' => $fullpathbase . $modulename . '/' . $modulename . '-min.js'
+ );
+ }
+ }
+ }
+}
diff --git a/lib/classes/output/single_button.php b/lib/classes/output/single_button.php
new file mode 100644
index 00000000000..83de95439b3
--- /dev/null
+++ b/lib/classes/output/single_button.php
@@ -0,0 +1,266 @@
+.
+
+/**
+ * Data structure representing a simple form with only one button.
+ *
+ * @copyright 2009 Petr Skoda
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class single_button implements renderable {
+
+ /**
+ * Possible button types. From boostrap.
+ */
+ const BUTTON_TYPES = [
+ self::BUTTON_PRIMARY,
+ self::BUTTON_SECONDARY,
+ self::BUTTON_SUCCESS,
+ self::BUTTON_DANGER,
+ self::BUTTON_WARNING,
+ self::BUTTON_INFO
+ ];
+
+ /**
+ * Possible button types - Primary.
+ */
+ const BUTTON_PRIMARY = 'primary';
+ /**
+ * Possible button types - Secondary.
+ */
+ const BUTTON_SECONDARY = 'secondary';
+ /**
+ * Possible button types - Danger.
+ */
+ const BUTTON_DANGER = 'danger';
+ /**
+ * Possible button types - Success.
+ */
+ const BUTTON_SUCCESS = 'success';
+ /**
+ * Possible button types - Warning.
+ */
+ const BUTTON_WARNING = 'warning';
+ /**
+ * Possible button types - Info.
+ */
+ const BUTTON_INFO = 'info';
+
+ /**
+ * @var moodle_url Target url
+ */
+ public $url;
+
+ /**
+ * @var string Button label
+ */
+ public $label;
+
+ /**
+ * @var string Form submit method post or get
+ */
+ public $method = 'post';
+
+ /**
+ * @var string Wrapping div class
+ */
+ public $class = 'singlebutton';
+
+ /**
+ * @var string Type of button (from defined types). Used for styling.
+ */
+ protected $type;
+
+ /**
+ * @var bool True if button is primary button. Used for styling.
+ * @deprecated since Moodle 4.2
+ */
+ private $primary = false;
+
+ /**
+ * @var bool True if button disabled, false if normal
+ */
+ public $disabled = false;
+
+ /**
+ * @var string Button tooltip
+ */
+ public $tooltip = null;
+
+ /**
+ * @var string Form id
+ */
+ public $formid;
+
+ /**
+ * @var array List of attached actions
+ */
+ public $actions = array();
+
+ /**
+ * @var array $params URL Params
+ */
+ public $params;
+
+ /**
+ * @var string Action id
+ */
+ public $actionid;
+
+ /**
+ * @var array
+ */
+ protected $attributes = [];
+
+ /**
+ * Constructor
+ *
+ * @param moodle_url $url
+ * @param string $label button text
+ * @param string $method get or post submit method
+ * @param string $type whether this is a primary button or another type, used for styling
+ * @param array $attributes Attributes for the HTML button tag
+ */
+ public function __construct(moodle_url $url, $label, $method = 'post', $type = self::BUTTON_SECONDARY,
+ $attributes = []) {
+ if (is_bool($type)) {
+ debugging('The boolean $primary is deprecated and replaced by $type,
+ use single_button::BUTTON_PRIMARY or self::BUTTON_SECONDARY instead');
+ $type = $type ? self::BUTTON_PRIMARY : self::BUTTON_SECONDARY;
+ }
+ $this->url = clone($url);
+ $this->label = $label;
+ $this->method = $method;
+ $this->type = $type;
+ $this->attributes = $attributes;
+ }
+
+ /**
+ * Shortcut for adding a JS confirm dialog when the button is clicked.
+ * The message must be a yes/no question.
+ *
+ * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
+ */
+ public function add_confirm_action($confirmmessage) {
+ $this->add_action(new confirm_action($confirmmessage));
+ }
+
+ /**
+ * Add action to the button.
+ * @param component_action $action
+ */
+ public function add_action(component_action $action) {
+ $this->actions[] = $action;
+ }
+
+ /**
+ * Sets an attribute for the HTML button tag.
+ *
+ * @param string $name The attribute name
+ * @param mixed $value The value
+ * @return null
+ */
+ public function set_attribute($name, $value) {
+ $this->attributes[$name] = $value;
+ }
+
+ /**
+ * Magic setter method.
+ *
+ * This method manages access to some properties and will display deprecation message when accessing 'primary' property.
+ *
+ * @param string $name
+ * @param mixed $value
+ */
+ public function __set($name, $value) {
+ switch ($name) {
+ case 'primary':
+ debugging('The primary field is deprecated, use the type field instead');
+ // Here just in case we modified the primary field from outside {@see \mod_quiz_renderer::summary_page_controls}.
+ $this->type = $value ? self::BUTTON_PRIMARY : self::BUTTON_SECONDARY;
+ break;
+ case 'type':
+ $this->type = in_array($value, self::BUTTON_TYPES) ? $value : self::BUTTON_SECONDARY;
+ break;
+ default:
+ $this->$name = $value;
+ }
+ }
+
+ /**
+ * Magic method getter.
+ *
+ * This method manages access to some properties and will display deprecation message when accessing 'primary' property.
+ *
+ * @param string $name
+ * @return mixed
+ */
+ public function __get($name) {
+ switch ($name) {
+ case 'primary':
+ debugging('The primary field is deprecated, use type field instead');
+ return $this->type == self::BUTTON_PRIMARY;
+ case 'type':
+ return $this->type;
+ default:
+ return $this->$name;
+ }
+ }
+
+ /**
+ * Export data.
+ *
+ * @param renderer_base $output Renderer.
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ $url = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
+
+ $data = new stdClass();
+ $data->id = html_writer::random_id('single_button');
+ $data->formid = $this->formid;
+ $data->method = $this->method;
+ $data->url = $url === '' ? '#' : $url;
+ $data->label = $this->label;
+ $data->classes = $this->class;
+ $data->disabled = $this->disabled;
+ $data->tooltip = $this->tooltip;
+ $data->type = $this->type;
+ $data->attributes = [];
+ foreach ($this->attributes as $key => $value) {
+ $data->attributes[] = ['name' => $key, 'value' => $value];
+ }
+
+ // Form parameters.
+ $actionurl = new moodle_url($this->url);
+ if ($this->method === 'post') {
+ $actionurl->param('sesskey', sesskey());
+ }
+ $data->params = $actionurl->export_params_for_template();
+
+ // Button actions.
+ $actions = $this->actions;
+ $data->actions = array_map(function($action) use ($output) {
+ return $action->export_for_template($output);
+ }, $actions);
+ $data->hasactions = !empty($data->actions);
+
+ return $data;
+ }
+}
diff --git a/lib/classes/output/single_select.php b/lib/classes/output/single_select.php
new file mode 100644
index 00000000000..4645e7bd8b7
--- /dev/null
+++ b/lib/classes/output/single_select.php
@@ -0,0 +1,289 @@
+.
+
+/**
+ * Simple form with just one select field that gets submitted automatically.
+ *
+ * If JS not enabled small go button is printed too.
+ *
+ * @copyright 2009 Petr Skoda
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class single_select implements renderable, templatable {
+
+ /**
+ * @var moodle_url Target url - includes hidden fields
+ */
+ var $url;
+
+ /**
+ * @var string Name of the select element.
+ */
+ var $name;
+
+ /**
+ * @var array $options associative array value=>label ex.: array(1=>'One, 2=>Two)
+ * it is also possible to specify optgroup as complex label array ex.:
+ * array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
+ * array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
+ */
+ var $options;
+
+ /**
+ * @var string Selected option
+ */
+ var $selected;
+
+ /**
+ * @var array Nothing selected
+ */
+ var $nothing;
+
+ /**
+ * @var array Extra select field attributes
+ */
+ var $attributes = array();
+
+ /**
+ * @var string Button label
+ */
+ var $label = '';
+
+ /**
+ * @var array Button label's attributes
+ */
+ var $labelattributes = array();
+
+ /**
+ * @var string Form submit method post or get
+ */
+ var $method = 'get';
+
+ /**
+ * @var string Wrapping div class
+ */
+ var $class = 'singleselect';
+
+ /**
+ * @var bool True if button disabled, false if normal
+ */
+ var $disabled = false;
+
+ /**
+ * @var string Button tooltip
+ */
+ var $tooltip = null;
+
+ /**
+ * @var string Form id
+ */
+ var $formid = null;
+
+ /**
+ * @var help_icon The help icon for this element.
+ */
+ var $helpicon = null;
+
+ /** @var component_action[] component action. */
+ public $actions = [];
+
+ /**
+ * Constructor
+ * @param moodle_url $url form action target, includes hidden fields
+ * @param string $name name of selection field - the changing parameter in url
+ * @param array $options list of options
+ * @param string $selected selected element
+ * @param ?array $nothing
+ * @param string $formid
+ */
+ public function __construct(moodle_url $url, $name, array $options, $selected = '', $nothing = array('' => 'choosedots'), $formid = null) {
+ $this->url = $url;
+ $this->name = $name;
+ $this->options = $options;
+ $this->selected = $selected;
+ $this->nothing = $nothing;
+ $this->formid = $formid;
+ }
+
+ /**
+ * Shortcut for adding a JS confirm dialog when the button is clicked.
+ * The message must be a yes/no question.
+ *
+ * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
+ */
+ public function add_confirm_action($confirmmessage) {
+ $this->add_action(new component_action('submit', 'M.util.show_confirm_dialog', array('message' => $confirmmessage)));
+ }
+
+ /**
+ * Add action to the button.
+ *
+ * @param component_action $action
+ */
+ public function add_action(component_action $action) {
+ $this->actions[] = $action;
+ }
+
+ /**
+ * Adds help icon.
+ *
+ * @deprecated since Moodle 2.0
+ */
+ public function set_old_help_icon($helppage, $title, $component = 'moodle') {
+ throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
+ }
+
+ /**
+ * Adds help icon.
+ *
+ * @param string $identifier The keyword that defines a help page
+ * @param string $component
+ */
+ public function set_help_icon($identifier, $component = 'moodle') {
+ $this->helpicon = new help_icon($identifier, $component);
+ }
+
+ /**
+ * Sets select's label
+ *
+ * @param string $label
+ * @param array $attributes (optional)
+ */
+ public function set_label($label, $attributes = array()) {
+ $this->label = $label;
+ $this->labelattributes = $attributes;
+
+ }
+
+ /**
+ * Export data.
+ *
+ * @param renderer_base $output Renderer.
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ $attributes = $this->attributes;
+
+ $data = new stdClass();
+ $data->name = $this->name;
+ $data->method = $this->method;
+ $data->action = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
+ $data->classes = $this->class;
+ $data->label = $this->label;
+ $data->disabled = $this->disabled;
+ $data->title = $this->tooltip;
+ $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('single_select_f');
+ $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('single_select');
+
+ // Select element attributes.
+ // Unset attributes that are already predefined in the template.
+ unset($attributes['id']);
+ unset($attributes['class']);
+ unset($attributes['name']);
+ unset($attributes['title']);
+ unset($attributes['disabled']);
+
+ // Map the attributes.
+ $data->attributes = array_map(function($key) use ($attributes) {
+ return ['name' => $key, 'value' => $attributes[$key]];
+ }, array_keys($attributes));
+
+ // Form parameters.
+ $actionurl = new moodle_url($this->url);
+ if ($this->method === 'post') {
+ $actionurl->param('sesskey', sesskey());
+ }
+ $data->params = $actionurl->export_params_for_template();
+
+ // Select options.
+ $hasnothing = false;
+ if (is_string($this->nothing) && $this->nothing !== '') {
+ $nothing = ['' => $this->nothing];
+ $hasnothing = true;
+ $nothingkey = '';
+ } else if (is_array($this->nothing)) {
+ $nothingvalue = reset($this->nothing);
+ if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
+ $nothing = [key($this->nothing) => get_string('choosedots')];
+ } else {
+ $nothing = $this->nothing;
+ }
+ $hasnothing = true;
+ $nothingkey = key($this->nothing);
+ }
+ if ($hasnothing) {
+ $options = $nothing + $this->options;
+ } else {
+ $options = $this->options;
+ }
+
+ foreach ($options as $value => $name) {
+ if (is_array($options[$value])) {
+ foreach ($options[$value] as $optgroupname => $optgroupvalues) {
+ $sublist = [];
+ foreach ($optgroupvalues as $optvalue => $optname) {
+ $option = [
+ 'value' => $optvalue,
+ 'name' => $optname,
+ 'selected' => strval($this->selected) === strval($optvalue),
+ ];
+
+ if ($hasnothing && $nothingkey === $optvalue) {
+ $option['ignore'] = 'data-ignore';
+ }
+
+ $sublist[] = $option;
+ }
+ $data->options[] = [
+ 'name' => $optgroupname,
+ 'optgroup' => true,
+ 'options' => $sublist
+ ];
+ }
+ } else {
+ $option = [
+ 'value' => $value,
+ 'name' => $options[$value],
+ 'selected' => strval($this->selected) === strval($value),
+ 'optgroup' => false
+ ];
+
+ if ($hasnothing && $nothingkey === $value) {
+ $option['ignore'] = 'data-ignore';
+ }
+
+ $data->options[] = $option;
+ }
+ }
+
+ // Label attributes.
+ $data->labelattributes = [];
+ // Unset label attributes that are already in the template.
+ unset($this->labelattributes['for']);
+ // Map the label attributes.
+ foreach ($this->labelattributes as $key => $value) {
+ $data->labelattributes[] = ['name' => $key, 'value' => $value];
+ }
+
+ // Help icon.
+ $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
+
+ return $data;
+ }
+}
diff --git a/lib/classes/output/tabobject.php b/lib/classes/output/tabobject.php
new file mode 100644
index 00000000000..305d5b30a75
--- /dev/null
+++ b/lib/classes/output/tabobject.php
@@ -0,0 +1,140 @@
+.
+
+/**
+ * Stores one tab
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package core
+ */
+class tabobject implements renderable, templatable {
+ /** @var string unique id of the tab in this tree, it is used to find selected and/or inactive tabs */
+ var $id;
+ /** @var moodle_url|string link */
+ var $link;
+ /** @var string text on the tab */
+ var $text;
+ /** @var string title under the link, by defaul equals to text */
+ var $title;
+ /** @var bool whether to display a link under the tab name when it's selected */
+ var $linkedwhenselected = false;
+ /** @var bool whether the tab is inactive */
+ var $inactive = false;
+ /** @var bool indicates that this tab's child is selected */
+ var $activated = false;
+ /** @var bool indicates that this tab is selected */
+ var $selected = false;
+ /** @var array stores children tabobjects */
+ var $subtree = array();
+ /** @var int level of tab in the tree, 0 for root (instance of tabtree), 1 for the first row of tabs */
+ var $level = 1;
+
+ /**
+ * Constructor
+ *
+ * @param string $id unique id of the tab in this tree, it is used to find selected and/or inactive tabs
+ * @param string|moodle_url $link
+ * @param string $text text on the tab
+ * @param string $title title under the link, by defaul equals to text
+ * @param bool $linkedwhenselected whether to display a link under the tab name when it's selected
+ */
+ public function __construct($id, $link = null, $text = '', $title = '', $linkedwhenselected = false) {
+ $this->id = $id;
+ $this->link = $link;
+ $this->text = $text;
+ $this->title = $title ? $title : $text;
+ $this->linkedwhenselected = $linkedwhenselected;
+ }
+
+ /**
+ * Travels through tree and finds the tab to mark as selected, all parents are automatically marked as activated
+ *
+ * @param string $selected the id of the selected tab (whatever row it's on),
+ * if null marks all tabs as unselected
+ * @return bool whether this tab is selected or contains selected tab in its subtree
+ */
+ protected function set_selected($selected) {
+ if ((string)$selected === (string)$this->id) {
+ $this->selected = true;
+ // This tab is selected. No need to travel through subtree.
+ return true;
+ }
+ foreach ($this->subtree as $subitem) {
+ if ($subitem->set_selected($selected)) {
+ // This tab has child that is selected. Mark it as activated. No need to check other children.
+ $this->activated = true;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Travels through tree and finds a tab with specified id
+ *
+ * @param string $id
+ * @return tabtree|null
+ */
+ public function find($id) {
+ if ((string)$this->id === (string)$id) {
+ return $this;
+ }
+ foreach ($this->subtree as $tab) {
+ if ($obj = $tab->find($id)) {
+ return $obj;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Allows to mark each tab's level in the tree before rendering.
+ *
+ * @param int $level
+ */
+ protected function set_level($level) {
+ $this->level = $level;
+ foreach ($this->subtree as $tab) {
+ $tab->set_level($level + 1);
+ }
+ }
+
+ /**
+ * Export for template.
+ *
+ * @param renderer_base $output Renderer.
+ * @return object
+ */
+ public function export_for_template(renderer_base $output) {
+ if ($this->inactive || ($this->selected && !$this->linkedwhenselected) || $this->activated) {
+ $link = null;
+ } else {
+ $link = $this->link;
+ }
+ $active = $this->activated || $this->selected;
+
+ return (object) [
+ 'id' => $this->id,
+ 'link' => is_object($link) ? $link->out(false) : $link,
+ 'text' => $this->text,
+ 'title' => $this->title,
+ 'inactive' => !$active && $this->inactive,
+ 'active' => $active,
+ 'level' => $this->level,
+ ];
+ }
+
+}
diff --git a/lib/classes/output/tabtree.php b/lib/classes/output/tabtree.php
new file mode 100644
index 00000000000..ca31e32ea7e
--- /dev/null
+++ b/lib/classes/output/tabtree.php
@@ -0,0 +1,91 @@
+.
+
+/**
+ * Stores tabs list
+ *
+ * Example how to print a single line tabs:
+ * $rows = array(
+ * new tabobject(...),
+ * new tabobject(...)
+ * );
+ * echo $OUTPUT->tabtree($rows, $selectedid);
+ *
+ * Multiple row tabs may not look good on some devices but if you want to use them
+ * you can specify ->subtree for the active tabobject.
+ *
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.5
+ * @package core
+ * @category output
+ */
+class tabtree extends tabobject {
+ /**
+ * Constuctor
+ *
+ * It is highly recommended to call constructor when list of tabs is already
+ * populated, this way you ensure that selected and inactive tabs are located
+ * and attribute level is set correctly.
+ *
+ * @param array $tabs array of tabs, each of them may have it's own ->subtree
+ * @param string|null $selected which tab to mark as selected, all parent tabs will
+ * automatically be marked as activated
+ * @param array|string|null $inactive list of ids of inactive tabs, regardless of
+ * their level. Note that you can as weel specify tabobject::$inactive for separate instances
+ */
+ public function __construct($tabs, $selected = null, $inactive = null) {
+ $this->subtree = $tabs;
+ if ($selected !== null) {
+ $this->set_selected($selected);
+ }
+ if ($inactive !== null) {
+ if (is_array($inactive)) {
+ foreach ($inactive as $id) {
+ if ($tab = $this->find($id)) {
+ $tab->inactive = true;
+ }
+ }
+ } else if ($tab = $this->find($inactive)) {
+ $tab->inactive = true;
+ }
+ }
+ $this->set_level(0);
+ }
+
+ /**
+ * Export for template.
+ *
+ * @param renderer_base $output Renderer.
+ * @return object
+ */
+ public function export_for_template(renderer_base $output) {
+ $tabs = [];
+ $secondrow = false;
+
+ foreach ($this->subtree as $tab) {
+ $tabs[] = $tab->export_for_template($output);
+ if (!empty($tab->subtree) && ($tab->level == 0 || $tab->selected || $tab->activated)) {
+ $secondrow = new tabtree($tab->subtree);
+ }
+ }
+
+ return (object) [
+ 'tabs' => $tabs,
+ 'secondrow' => $secondrow ? $secondrow->export_for_template($output) : false
+ ];
+ }
+}
diff --git a/lib/classes/output/templatable.php b/lib/classes/output/templatable.php
new file mode 100644
index 00000000000..8a05b61e43e
--- /dev/null
+++ b/lib/classes/output/templatable.php
@@ -0,0 +1,37 @@
+.
+
+/**
+ * Interface marking other classes having the ability to export their data for use by templates.
+ *
+ * @copyright 2015 Damyon Wiese
+ * @package core
+ * @category output
+ * @since 2.9
+ */
+interface templatable {
+
+ /**
+ * Function to export the renderer data in a format that is suitable for a
+ * mustache template. This means:
+ * 1. No complex types - only stdClass, array, int, string, float, bool
+ * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks).
+ *
+ * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
+ * @return stdClass|array
+ */
+ public function export_for_template(renderer_base $output);
+}
diff --git a/lib/classes/output/theme_config.php b/lib/classes/output/theme_config.php
new file mode 100644
index 00000000000..d162a454fea
--- /dev/null
+++ b/lib/classes/output/theme_config.php
@@ -0,0 +1,2312 @@
+.
+
+/**
+ * This class represents the configuration variables of a Moodle theme.
+ *
+ * All the variables with access: public below (with a few exceptions that are marked)
+ * are the properties you can set in your themes config.php file.
+ *
+ * There are also some methods and protected variables that are part of the inner
+ * workings of Moodle's themes system. If you are just editing a themes config.php
+ * file, you can just ignore those, and the following information for developers.
+ *
+ * Normally, to create an instance of this class, you should use the
+ * {@link theme_config::load()} factory method to load a themes config.php file.
+ * However, normally you don't need to bother, because moodle_page (that is, $PAGE)
+ * will create one for you, accessible as $PAGE->theme.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package core
+ * @category output
+ */
+class theme_config {
+
+ /**
+ * @var string Default theme, used when requested theme not found.
+ */
+ const DEFAULT_THEME = 'boost';
+
+ /** The key under which the SCSS file is stored amongst the CSS files. */
+ const SCSS_KEY = '__SCSS__';
+
+ /**
+ * @var array You can base your theme on other themes by linking to the other theme as
+ * parents. This lets you use the CSS and layouts from the other themes
+ * (see {@link theme_config::$layouts}).
+ * That makes it easy to create a new theme that is similar to another one
+ * but with a few changes. In this themes CSS you only need to override
+ * those rules you want to change.
+ */
+ public $parents;
+
+ /**
+ * @var array The names of all the stylesheets from this theme that you would
+ * like included, in order. Give the names of the files without .css.
+ */
+ public $sheets = array();
+
+ /**
+ * @var array The names of all the stylesheets from parents that should be excluded.
+ * true value may be used to specify all parents or all themes from one parent.
+ * If no value specified value from parent theme used.
+ */
+ public $parents_exclude_sheets = null;
+
+ /**
+ * @var array List of plugin sheets to be excluded.
+ * If no value specified value from parent theme used.
+ */
+ public $plugins_exclude_sheets = null;
+
+ /**
+ * @var array List of style sheets that are included in the text editor bodies.
+ * Sheets from parent themes are used automatically and can not be excluded.
+ */
+ public $editor_sheets = array();
+
+ /**
+ * @var bool Whether a fallback version of the stylesheet will be used
+ * whilst the final version is generated.
+ */
+ public $usefallback = false;
+
+ /**
+ * @var array The names of all the javascript files this theme that you would
+ * like included from head, in order. Give the names of the files without .js.
+ */
+ public $javascripts = array();
+
+ /**
+ * @var array The names of all the javascript files this theme that you would
+ * like included from footer, in order. Give the names of the files without .js.
+ */
+ public $javascripts_footer = array();
+
+ /**
+ * @var array The names of all the javascript files from parents that should
+ * be excluded. true value may be used to specify all parents or all themes
+ * from one parent.
+ * If no value specified value from parent theme used.
+ */
+ public $parents_exclude_javascripts = null;
+
+ /**
+ * @var array Which file to use for each page layout.
+ *
+ * This is an array of arrays. The keys of the outer array are the different layouts.
+ * Pages in Moodle are using several different layouts like 'normal', 'course', 'home',
+ * 'popup', 'form', .... The most reliable way to get a complete list is to look at
+ * {@link http://cvs.moodle.org/moodle/theme/base/config.php?view=markup the base theme config.php file}.
+ * That file also has a good example of how to set this setting.
+ *
+ * For each layout, the value in the outer array is an array that describes
+ * how you want that type of page to look. For example
+ *
+ * $THEME->layouts = array(
+ * // Most pages - if we encounter an unknown or a missing page type, this one is used.
+ * 'standard' => array(
+ * 'theme' = 'mytheme',
+ * 'file' => 'normal.php',
+ * 'regions' => array('side-pre', 'side-post'),
+ * 'defaultregion' => 'side-post'
+ * ),
+ * // The site home page.
+ * 'home' => array(
+ * 'theme' = 'mytheme',
+ * 'file' => 'home.php',
+ * 'regions' => array('side-pre', 'side-post'),
+ * 'defaultregion' => 'side-post'
+ * ),
+ * // ...
+ * );
+ *
+ *
+ * 'theme' name of the theme where is the layout located
+ * 'file' is the layout file to use for this type of page.
+ * layout files are stored in layout subfolder
+ * 'regions' This lists the regions on the page where blocks may appear. For
+ * each region you list here, your layout file must include a call to
+ *
+ * echo $OUTPUT->blocks_for_region($regionname);
+ *
+ * or equivalent so that the blocks are actually visible.
+ *
+ * 'defaultregion' If the list of regions is non-empty, then you must pick
+ * one of the one of them as 'default'. This has two meanings. First, this is
+ * where new blocks are added. Second, if there are any blocks associated with
+ * the page, but in non-existent regions, they appear here. (Imaging, for example,
+ * that someone added blocks using a different theme that used different region
+ * names, and then switched to this theme.)
+ */
+ public $layouts = array();
+
+ /**
+ * @var string Name of the renderer factory class to use. Must implement the
+ * {@link renderer_factory} interface.
+ *
+ * This is an advanced feature. Moodle output is generated by 'renderers',
+ * you can customise the HTML that is output by writing custom renderers,
+ * and then you need to specify 'renderer factory' so that Moodle can find
+ * your renderers.
+ *
+ * There are some renderer factories supplied with Moodle. Please follow these
+ * links to see what they do.
+ *
+ * - {@link standard_renderer_factory} - the default.
+ * - {@link theme_overridden_renderer_factory} - use this if you want to write
+ * your own custom renderers in a lib.php file in this theme (or the parent theme).
+ *
+ */
+ public $rendererfactory = 'standard_renderer_factory';
+
+ /**
+ * @var string Function to do custom CSS post-processing.
+ *
+ * This is an advanced feature. If you want to do custom post-processing on the
+ * CSS before it is output (for example, to replace certain variable names
+ * with particular values) you can give the name of a function here.
+ */
+ public $csspostprocess = null;
+
+ /**
+ * @var string Function to do custom CSS post-processing on a parsed CSS tree.
+ *
+ * This is an advanced feature. If you want to do custom post-processing on the
+ * CSS before it is output, you can provide the name of the function here. The
+ * function will receive a CSS tree document as first parameter, and the theme_config
+ * object as second parameter. A return value is not required, the tree can
+ * be edited in place.
+ */
+ public $csstreepostprocessor = null;
+
+ /**
+ * @var string Accessibility: Right arrow-like character is
+ * used in the breadcrumb trail, course navigation menu
+ * (previous/next activity), calendar, and search forum block.
+ * If the theme does not set characters, appropriate defaults
+ * are set automatically. Please DO NOT
+ * use < > » - these are confusing for blind users.
+ */
+ public $rarrow = null;
+
+ /**
+ * @var string Accessibility: Left arrow-like character is
+ * used in the breadcrumb trail, course navigation menu
+ * (previous/next activity), calendar, and search forum block.
+ * If the theme does not set characters, appropriate defaults
+ * are set automatically. Please DO NOT
+ * use < > » - these are confusing for blind users.
+ */
+ public $larrow = null;
+
+ /**
+ * @var string Accessibility: Up arrow-like character is used in
+ * the book heirarchical navigation.
+ * If the theme does not set characters, appropriate defaults
+ * are set automatically. Please DO NOT
+ * use ^ - this is confusing for blind users.
+ */
+ public $uarrow = null;
+
+ /**
+ * @var string Accessibility: Down arrow-like character.
+ * If the theme does not set characters, appropriate defaults
+ * are set automatically.
+ */
+ public $darrow = null;
+
+ /**
+ * @var bool Some themes may want to disable ajax course editing.
+ */
+ public $enablecourseajax = true;
+
+ /**
+ * @var string Determines served document types
+ * - 'html5' the only officially supported doctype in Moodle
+ * - 'xhtml5' may be used in development for validation (not intended for production servers!)
+ * - 'xhtml' XHTML 1.0 Strict for legacy themes only
+ */
+ public $doctype = 'html5';
+
+ /**
+ * @var string|false requiredblocks If set to a string, will list the block types that cannot be deleted. Defaults to
+ * navigation and settings.
+ */
+ public $requiredblocks = false;
+
+ //==Following properties are not configurable from theme config.php==
+
+ /**
+ * @var string The name of this theme. Set automatically when this theme is
+ * loaded. This can not be set in theme config.php
+ */
+ public $name;
+
+ /**
+ * @var string The folder where this themes files are stored. This is set
+ * automatically. This can not be set in theme config.php
+ */
+ public $dir;
+
+ /**
+ * @var stdClass Theme settings stored in config_plugins table.
+ * This can not be set in theme config.php
+ */
+ public $settings = null;
+
+ /**
+ * @var bool If set to true and the theme enables the dock then blocks will be able
+ * to be moved to the special dock
+ */
+ public $enable_dock = false;
+
+ /**
+ * @var bool If set to true then this theme will not be shown in the theme selector unless
+ * theme designer mode is turned on.
+ */
+ public $hidefromselector = false;
+
+ /**
+ * @var array list of YUI CSS modules to be included on each page. This may be used
+ * to remove cssreset and use cssnormalise module instead.
+ */
+ public $yuicssmodules = array('cssreset', 'cssfonts', 'cssgrids', 'cssbase');
+
+ /**
+ * An associative array of block manipulations that should be made if the user is using an rtl language.
+ * The key is the original block region, and the value is the block region to change to.
+ * This is used when displaying blocks for regions only.
+ * @var array
+ */
+ public $blockrtlmanipulations = array();
+
+ /**
+ * @var renderer_factory Instance of the renderer_factory implementation
+ * we are using. Implementation detail.
+ */
+ protected $rf = null;
+
+ /**
+ * @var array List of parent config objects.
+ **/
+ protected $parent_configs = array();
+
+ /**
+ * Used to determine whether we can serve SVG images or not.
+ * @var bool
+ */
+ private $usesvg = null;
+
+ /**
+ * Whether in RTL mode or not.
+ * @var bool
+ */
+ protected $rtlmode = false;
+
+ /**
+ * The SCSS file to compile (without .scss), located in the scss/ folder of the theme.
+ * Or a Closure, which receives the theme_config as argument and must
+ * return the SCSS content.
+ * @var string|Closure
+ */
+ public $scss = false;
+
+ /**
+ * Local cache of the SCSS property.
+ * @var false|array
+ */
+ protected $scsscache = null;
+
+ /**
+ * The name of the function to call to get the SCSS code to inject.
+ * @var string
+ */
+ public $extrascsscallback = null;
+
+ /**
+ * The name of the function to call to get SCSS to prepend.
+ * @var string
+ */
+ public $prescsscallback = null;
+
+ /**
+ * Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php
+ * Defaults to {@link core_renderer::blocks_for_region()}
+ * @var string
+ */
+ public $blockrendermethod = null;
+
+ /**
+ * Remember the results of icon remapping for the current page.
+ * @var array
+ */
+ public $remapiconcache = [];
+
+ /**
+ * The name of the function to call to get precompiled CSS.
+ * @var string
+ */
+ public $precompiledcsscallback = null;
+
+ /**
+ * Whether the theme uses course index.
+ * @var bool
+ */
+ public $usescourseindex = false;
+
+ /**
+ * Configuration for the page activity header
+ * @var array
+ */
+ public $activityheaderconfig = [];
+
+ /**
+ * For backward compatibility with old themes.
+ * BLOCK_ADDBLOCK_POSITION_DEFAULT, BLOCK_ADDBLOCK_POSITION_FLATNAV.
+ * @var int
+ */
+ public $addblockposition;
+
+ /**
+ * editor_scss file(s) provided by this theme.
+ * @var array
+ */
+ public $editor_scss;
+
+ /**
+ * Name of the class extending \core\output\icon_system.
+ * @var string
+ */
+ public $iconsystem;
+
+ /**
+ * Theme defines its own editing mode switch.
+ * @var bool
+ */
+ public $haseditswitch = false;
+
+ /**
+ * Allows a theme to customise primary navigation by specifying the list of items to remove.
+ * @var array
+ */
+ public $removedprimarynavitems = [];
+
+ /**
+ * Load the config.php file for a particular theme, and return an instance
+ * of this class. (That is, this is a factory method.)
+ *
+ * @param string $themename the name of the theme.
+ * @return theme_config an instance of this class.
+ */
+ public static function load($themename) {
+ global $CFG;
+
+ // load theme settings from db
+ try {
+ $settings = get_config('theme_'.$themename);
+ } catch (dml_exception $e) {
+ // most probably moodle tables not created yet
+ $settings = new stdClass();
+ }
+
+ if ($config = theme_config::find_theme_config($themename, $settings)) {
+ return new theme_config($config);
+
+ } else if ($themename == theme_config::DEFAULT_THEME) {
+ throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!');
+
+ } else if ($config = theme_config::find_theme_config($CFG->theme, $settings)) {
+ debugging('This page should be using theme ' . $themename .
+ ' which cannot be initialised. Falling back to the site theme ' . $CFG->theme, DEBUG_NORMAL);
+ return new theme_config($config);
+
+ } else {
+ // bad luck, the requested theme has some problems - admin see details in theme config
+ debugging('This page should be using theme ' . $themename .
+ ' which cannot be initialised. Nor can the site theme ' . $CFG->theme .
+ '. Falling back to ' . theme_config::DEFAULT_THEME, DEBUG_NORMAL);
+ return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings));
+ }
+ }
+
+ /**
+ * Theme diagnostic code. It is very problematic to send debug output
+ * to the actual CSS file, instead this functions is supposed to
+ * diagnose given theme and highlights all potential problems.
+ * This information should be available from the theme selection page
+ * or some other debug page for theme designers.
+ *
+ * @param string $themename
+ * @return array description of problems
+ */
+ public static function diagnose($themename) {
+ //TODO: MDL-21108
+ return array();
+ }
+
+ /**
+ * Private constructor, can be called only from the factory method.
+ * @param stdClass $config
+ */
+ private function __construct($config) {
+ global $CFG; //needed for included lib.php files
+
+ $this->settings = $config->settings;
+ $this->name = $config->name;
+ $this->dir = $config->dir;
+
+ if ($this->name != self::DEFAULT_THEME) {
+ $baseconfig = self::find_theme_config(self::DEFAULT_THEME, $this->settings);
+ } else {
+ $baseconfig = $config;
+ }
+
+ // Ensure that each of the configurable properties defined below are also defined at the class level.
+ $configurable = [
+ 'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback',
+ 'javascripts', 'javascripts_footer', 'parents_exclude_javascripts',
+ 'layouts', 'enablecourseajax', 'requiredblocks',
+ 'rendererfactory', 'csspostprocess', 'editor_sheets', 'editor_scss', 'rarrow', 'larrow', 'uarrow', 'darrow',
+ 'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations', 'blockrendermethod',
+ 'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition',
+ 'iconsystem', 'precompiledcsscallback', 'haseditswitch', 'usescourseindex', 'activityheaderconfig',
+ 'removedprimarynavitems',
+ ];
+
+ foreach ($config as $key=>$value) {
+ if (in_array($key, $configurable)) {
+ $this->$key = $value;
+ }
+ }
+
+ // verify all parents and load configs and renderers
+ foreach ($this->parents as $parent) {
+ if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) {
+ // this is not good - better exclude faulty parents
+ continue;
+ }
+ $libfile = $parent_config->dir.'/lib.php';
+ if (is_readable($libfile)) {
+ // theme may store various function here
+ include_once($libfile);
+ }
+ $renderersfile = $parent_config->dir.'/renderers.php';
+ if (is_readable($renderersfile)) {
+ // may contain core and plugin renderers and renderer factory
+ include_once($renderersfile);
+ }
+ $this->parent_configs[$parent] = $parent_config;
+ }
+ $libfile = $this->dir.'/lib.php';
+ if (is_readable($libfile)) {
+ // theme may store various function here
+ include_once($libfile);
+ }
+ $rendererfile = $this->dir.'/renderers.php';
+ if (is_readable($rendererfile)) {
+ // may contain core and plugin renderers and renderer factory
+ include_once($rendererfile);
+ } else {
+ // check if renderers.php file is missnamed renderer.php
+ if (is_readable($this->dir.'/renderer.php')) {
+ debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php.
+ See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER);
+ }
+ }
+
+ // cascade all layouts properly
+ foreach ($baseconfig->layouts as $layout=>$value) {
+ if (!isset($this->layouts[$layout])) {
+ foreach ($this->parent_configs as $parent_config) {
+ if (isset($parent_config->layouts[$layout])) {
+ $this->layouts[$layout] = $parent_config->layouts[$layout];
+ continue 2;
+ }
+ }
+ $this->layouts[$layout] = $value;
+ }
+ }
+
+ //fix arrows if needed
+ $this->check_theme_arrows();
+ }
+
+ /**
+ * Let the theme initialise the page object (usually $PAGE).
+ *
+ * This may be used for example to request jQuery in add-ons.
+ *
+ * @param moodle_page $page
+ */
+ public function init_page(moodle_page $page) {
+ $themeinitfunction = 'theme_'.$this->name.'_page_init';
+ if (function_exists($themeinitfunction)) {
+ $themeinitfunction($page);
+ }
+ }
+
+ /**
+ * Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow, $THEME->darrow have been set (theme/-/config.php).
+ * If not it applies sensible defaults.
+ *
+ * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
+ * search forum block, etc. Important: these are 'silent' in a screen-reader
+ * (unlike > »), and must be accompanied by text.
+ */
+ private function check_theme_arrows() {
+ if (!isset($this->rarrow) and !isset($this->larrow)) {
+ // Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8...
+ // Also OK in Win 9x/2K/IE 5.x
+ $this->rarrow = '►';
+ $this->larrow = '◄';
+ $this->uarrow = '▲';
+ $this->darrow = '▼';
+ if (empty($_SERVER['HTTP_USER_AGENT'])) {
+ $uagent = '';
+ } else {
+ $uagent = $_SERVER['HTTP_USER_AGENT'];
+ }
+ if (false !== strpos($uagent, 'Opera')
+ || false !== strpos($uagent, 'Mac')) {
+ // Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari.
+ // Not broken in Mac/IE 5, Mac/Netscape 7 (?).
+ $this->rarrow = '▶︎';
+ $this->larrow = '◀︎';
+ }
+ elseif ((false !== strpos($uagent, 'Konqueror'))
+ || (false !== strpos($uagent, 'Android'))) {
+ // The fonts on Android don't include the characters required for this to work as expected.
+ // So we use the same ones Konqueror uses.
+ $this->rarrow = '→';
+ $this->larrow = '←';
+ $this->uarrow = '↑';
+ $this->darrow = '↓';
+ }
+ elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
+ && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
+ // (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.)
+ // To be safe, non-Unicode browsers!
+ $this->rarrow = '>';
+ $this->larrow = '<';
+ $this->uarrow = '^';
+ $this->darrow = 'v';
+ }
+
+ // RTL support - in RTL languages, swap r and l arrows
+ if (right_to_left()) {
+ $t = $this->rarrow;
+ $this->rarrow = $this->larrow;
+ $this->larrow = $t;
+ }
+ }
+ }
+
+ /**
+ * Returns output renderer prefixes, these are used when looking
+ * for the overridden renderers in themes.
+ *
+ * @return array
+ */
+ public function renderer_prefixes() {
+ global $CFG; // just in case the included files need it
+
+ $prefixes = array('theme_'.$this->name);
+
+ foreach ($this->parent_configs as $parent) {
+ $prefixes[] = 'theme_'.$parent->name;
+ }
+
+ return $prefixes;
+ }
+
+ /**
+ * Returns the stylesheet URL of this editor content
+ *
+ * @param bool $encoded false means use & and true use & in URLs
+ * @return moodle_url
+ */
+ public function editor_css_url($encoded=true) {
+ global $CFG;
+ $rev = theme_get_revision();
+ $type = 'editor';
+ if (right_to_left()) {
+ $type .= '-rtl';
+ }
+
+ if ($rev > -1) {
+ $themesubrevision = theme_get_sub_revision_for_theme($this->name);
+
+ // Provide the sub revision to allow us to invalidate cached theme CSS
+ // on a per theme basis, rather than globally.
+ if ($themesubrevision && $themesubrevision > 0) {
+ $rev .= "_{$themesubrevision}";
+ }
+
+ $url = new moodle_url("/theme/styles.php");
+ if (!empty($CFG->slasharguments)) {
+ $url->set_slashargument("/{$this->name}/{$rev}/{$type}", 'noparam', true);
+ } else {
+ $url->params([
+ 'theme' => $this->name,
+ 'rev' => $rev,
+ 'type' => $type,
+ ]);
+ }
+ } else {
+ $url = new moodle_url('/theme/styles_debug.php', [
+ 'theme' => $this->name,
+ 'type' => $type,
+ ]);
+ }
+ return $url;
+ }
+
+ /**
+ * Returns the content of the CSS to be used in editor content
+ *
+ * @return array
+ */
+ public function editor_css_files() {
+ $files = array();
+
+ // First editor plugins.
+ $plugins = core_component::get_plugin_list('editor');
+ foreach ($plugins as $plugin => $fulldir) {
+ $sheetfile = "$fulldir/editor_styles.css";
+ if (is_readable($sheetfile)) {
+ $files['plugin_'.$plugin] = $sheetfile;
+ }
+
+ $subplugintypes = core_component::get_subplugins("editor_{$plugin}") ?? [];
+ // Fetch sheets for any editor subplugins.
+ foreach ($subplugintypes as $plugintype => $subplugins) {
+ foreach ($subplugins as $subplugin) {
+ $plugindir = core_component::get_plugin_directory($plugintype, $subplugin);
+ $sheetfile = "{$plugindir}/editor_styles.css";
+ if (is_readable($sheetfile)) {
+ $files["{$plugintype}_{$subplugin}"] = $sheetfile;
+ }
+ }
+ }
+ }
+
+ // Then parent themes - base first, the immediate parent last.
+ foreach (array_reverse($this->parent_configs) as $parent_config) {
+ if (empty($parent_config->editor_sheets)) {
+ continue;
+ }
+ foreach ($parent_config->editor_sheets as $sheet) {
+ $sheetfile = "$parent_config->dir/style/$sheet.css";
+ if (is_readable($sheetfile)) {
+ $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile;
+ }
+ }
+ }
+ // Finally this theme.
+ if (!empty($this->editor_sheets)) {
+ foreach ($this->editor_sheets as $sheet) {
+ $sheetfile = "$this->dir/style/$sheet.css";
+ if (is_readable($sheetfile)) {
+ $files['theme_'.$sheet] = $sheetfile;
+ }
+ }
+ }
+
+ return $files;
+ }
+
+ /**
+ * Compiles and returns the content of the SCSS to be used in editor content
+ *
+ * @return string Compiled CSS from the editor SCSS
+ */
+ public function editor_scss_to_css() {
+ $css = '';
+ $dir = $this->dir;
+ $filenames = [];
+
+ // Use editor_scss file(s) provided by this theme if set.
+ if (!empty($this->editor_scss)) {
+ $filenames = $this->editor_scss;
+ } else {
+ // If no editor_scss set, move up theme hierarchy until one is found (if at all).
+ // This is so child themes only need to set editor_scss if an override is required.
+ foreach (array_reverse($this->parent_configs) as $parentconfig) {
+ if (!empty($parentconfig->editor_scss)) {
+ $dir = $parentconfig->dir;
+ $filenames = $parentconfig->editor_scss;
+
+ // Config found, stop looking.
+ break;
+ }
+ }
+ }
+
+ if (!empty($filenames)) {
+ $compiler = new core_scss();
+
+ foreach ($filenames as $filename) {
+ $compiler->set_file("{$dir}/scss/{$filename}.scss");
+
+ try {
+ $css .= $compiler->to_css();
+ } catch (\Exception $e) {
+ debugging('Error while compiling editor SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);
+ }
+ }
+ }
+
+ return $css;
+ }
+
+ /**
+ * Get the stylesheet URL of this theme.
+ *
+ * @param moodle_page $page Not used... deprecated?
+ * @return moodle_url[]
+ */
+ public function css_urls(moodle_page $page) {
+ global $CFG;
+
+ $rev = theme_get_revision();
+
+ $urls = array();
+
+ $svg = $this->use_svg_icons();
+ $separate = (core_useragent::is_ie() && !core_useragent::check_ie_version('10'));
+
+ if ($rev > -1) {
+ $filename = right_to_left() ? 'all-rtl' : 'all';
+ $url = new moodle_url("/theme/styles.php");
+ $themesubrevision = theme_get_sub_revision_for_theme($this->name);
+
+ // Provide the sub revision to allow us to invalidate cached theme CSS
+ // on a per theme basis, rather than globally.
+ if ($themesubrevision && $themesubrevision > 0) {
+ $rev .= "_{$themesubrevision}";
+ }
+
+ if (!empty($CFG->slasharguments)) {
+ $slashargs = '';
+ if (!$svg) {
+ // We add a simple /_s to the start of the path.
+ // The underscore is used to ensure that it isn't a valid theme name.
+ $slashargs .= '/_s'.$slashargs;
+ }
+ $slashargs .= '/'.$this->name.'/'.$rev.'/'.$filename;
+ if ($separate) {
+ $slashargs .= '/chunk0';
+ }
+ $url->set_slashargument($slashargs, 'noparam', true);
+ } else {
+ $params = array('theme' => $this->name, 'rev' => $rev, 'type' => $filename);
+ if (!$svg) {
+ // We add an SVG param so that we know not to serve SVG images.
+ // We do this because all modern browsers support SVG and this param will one day be removed.
+ $params['svg'] = '0';
+ }
+ if ($separate) {
+ $params['chunk'] = '0';
+ }
+ $url->params($params);
+ }
+ $urls[] = $url;
+
+ } else {
+ $baseurl = new moodle_url('/theme/styles_debug.php');
+
+ $css = $this->get_css_files(true);
+ if (!$svg) {
+ // We add an SVG param so that we know not to serve SVG images.
+ // We do this because all modern browsers support SVG and this param will one day be removed.
+ $baseurl->param('svg', '0');
+ }
+ if (right_to_left()) {
+ $baseurl->param('rtl', 1);
+ }
+ if ($separate) {
+ // We might need to chunk long files.
+ $baseurl->param('chunk', '0');
+ }
+ if (core_useragent::is_ie()) {
+ // Lalala, IE does not allow more than 31 linked CSS files from main document.
+ $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));
+ foreach ($css['parents'] as $parent=>$sheets) {
+ // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).
+ $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
+ }
+ if ($this->get_scss_property()) {
+ // No need to define the type as IE here.
+ $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
+ }
+ $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));
+
+ } else {
+ foreach ($css['plugins'] as $plugin=>$unused) {
+ $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin));
+ }
+ foreach ($css['parents'] as $parent=>$sheets) {
+ foreach ($sheets as $sheet=>$unused2) {
+ $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet));
+ }
+ }
+ foreach ($css['theme'] as $sheet => $filename) {
+ if ($sheet === self::SCSS_KEY) {
+ // This is the theme SCSS file.
+ $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
+ } else {
+ // Sheet first in order to make long urls easier to read.
+ $urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme'));
+ }
+ }
+ }
+ }
+
+ // Allow themes to change the css url to something like theme/mytheme/mycss.php.
+ component_callback('theme_' . $this->name, 'alter_css_urls', [&$urls]);
+ return $urls;
+ }
+
+ /**
+ * Get the whole css stylesheet for production mode.
+ *
+ * NOTE: this method is not expected to be used from any addons.
+ *
+ * @return string CSS markup compressed
+ */
+ public function get_css_content() {
+
+ $csscontent = '';
+ foreach ($this->get_css_files(false) as $type => $value) {
+ foreach ($value as $identifier => $val) {
+ if (is_array($val)) {
+ foreach ($val as $v) {
+ $csscontent .= file_get_contents($v) . "\n";
+ }
+ } else {
+ if ($type === 'theme' && $identifier === self::SCSS_KEY) {
+ // We need the content from SCSS because this is the SCSS file from the theme.
+ if ($compiled = $this->get_css_content_from_scss(false)) {
+ $csscontent .= $compiled;
+ } else {
+ // The compiler failed so default back to any precompiled css that might
+ // exist.
+ $csscontent .= $this->get_precompiled_css_content();
+ }
+ } else {
+ $csscontent .= file_get_contents($val) . "\n";
+ }
+ }
+ }
+ }
+ $csscontent = $this->post_process($csscontent);
+ $csscontent = core_minify::css($csscontent);
+
+ return $csscontent;
+ }
+ /**
+ * Set post processed CSS content cache.
+ *
+ * @param string $csscontent The post processed CSS content.
+ * @return bool True if the content was successfully cached.
+ */
+ public function set_css_content_cache($csscontent) {
+
+ $cache = cache::make('core', 'postprocessedcss');
+ $key = $this->get_css_cache_key();
+
+ return $cache->set($key, $csscontent);
+ }
+
+ /**
+ * Return whether the post processed CSS content has been cached.
+ *
+ * @return bool Whether the post-processed CSS is available in the cache.
+ */
+ public function has_css_cached_content() {
+
+ $key = $this->get_css_cache_key();
+ $cache = cache::make('core', 'postprocessedcss');
+
+ return $cache->has($key);
+ }
+
+ /**
+ * Return cached post processed CSS content.
+ *
+ * @return bool|string The cached css content or false if not found.
+ */
+ public function get_css_cached_content() {
+
+ $key = $this->get_css_cache_key();
+ $cache = cache::make('core', 'postprocessedcss');
+
+ return $cache->get($key);
+ }
+
+ /**
+ * Generate the css content cache key.
+ *
+ * @return string The post processed css cache key.
+ */
+ public function get_css_cache_key() {
+ $nosvg = (!$this->use_svg_icons()) ? 'nosvg_' : '';
+ $rtlmode = ($this->rtlmode == true) ? 'rtl' : 'ltr';
+
+ return $nosvg . $this->name . '_' . $rtlmode;
+ }
+
+ /**
+ * Get the theme designer css markup,
+ * the parameters are coming from css_urls().
+ *
+ * NOTE: this method is not expected to be used from any addons.
+ *
+ * @param string $type
+ * @param string $subtype
+ * @param string $sheet
+ * @return string CSS markup
+ */
+ public function get_css_content_debug($type, $subtype, $sheet) {
+ if ($type === 'scss') {
+ // The SCSS file of the theme is requested.
+ $csscontent = $this->get_css_content_from_scss(true);
+ if ($csscontent !== false) {
+ return $this->post_process($csscontent);
+ }
+ return '';
+ }
+
+ $cssfiles = array();
+ $css = $this->get_css_files(true);
+
+ if ($type === 'ie') {
+ // IE is a sloppy browser with weird limits, sorry.
+ if ($subtype === 'plugins') {
+ $cssfiles = $css['plugins'];
+
+ } else if ($subtype === 'parents') {
+ if (empty($sheet)) {
+ // Do not bother with the empty parent here.
+ } else {
+ // Build up the CSS for that parent so we can serve it as one file.
+ foreach ($css[$subtype][$sheet] as $parent => $css) {
+ $cssfiles[] = $css;
+ }
+ }
+ } else if ($subtype === 'theme') {
+ $cssfiles = $css['theme'];
+ foreach ($cssfiles as $key => $value) {
+ if (in_array($key, [self::SCSS_KEY])) {
+ // Remove the SCSS file from the theme CSS files.
+ // The SCSS files use the type 'scss', not 'ie'.
+ unset($cssfiles[$key]);
+ }
+ }
+ }
+
+ } else if ($type === 'plugin') {
+ if (isset($css['plugins'][$subtype])) {
+ $cssfiles[] = $css['plugins'][$subtype];
+ }
+
+ } else if ($type === 'parent') {
+ if (isset($css['parents'][$subtype][$sheet])) {
+ $cssfiles[] = $css['parents'][$subtype][$sheet];
+ }
+
+ } else if ($type === 'theme') {
+ if (isset($css['theme'][$sheet])) {
+ $cssfiles[] = $css['theme'][$sheet];
+ }
+ }
+
+ $csscontent = '';
+ foreach ($cssfiles as $file) {
+ $contents = file_get_contents($file);
+ $contents = $this->post_process($contents);
+ $comment = "/** Path: $type $subtype $sheet.' **/\n";
+ $stats = '';
+ $csscontent .= $comment.$stats.$contents."\n\n";
+ }
+
+ return $csscontent;
+ }
+
+ /**
+ * Get the whole css stylesheet for editor iframe.
+ *
+ * NOTE: this method is not expected to be used from any addons.
+ *
+ * @return string CSS markup
+ */
+ public function get_css_content_editor() {
+ $css = '';
+ $cssfiles = $this->editor_css_files();
+
+ // If editor has static CSS, include it.
+ foreach ($cssfiles as $file) {
+ $css .= file_get_contents($file)."\n";
+ }
+
+ // If editor has SCSS, compile and include it.
+ if (($convertedscss = $this->editor_scss_to_css())) {
+ $css .= $convertedscss;
+ }
+
+ $output = $this->post_process($css);
+
+ return $output;
+ }
+
+ /**
+ * Returns an array of organised CSS files required for this output.
+ *
+ * @param bool $themedesigner
+ * @return array nested array of file paths
+ */
+ protected function get_css_files($themedesigner) {
+ global $CFG;
+
+ $cache = null;
+ $cachekey = 'cssfiles';
+ if ($themedesigner) {
+ require_once($CFG->dirroot.'/lib/csslib.php');
+ // We need some kind of caching here because otherwise the page navigation becomes
+ // way too slow in theme designer mode. Feel free to create full cache definition later...
+ $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name));
+ if ($files = $cache->get($cachekey)) {
+ if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {
+ unset($files['created']);
+ return $files;
+ }
+ }
+ }
+
+ $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());
+
+ // Get all plugin sheets.
+ $excludes = $this->resolve_excludes('plugins_exclude_sheets');
+ if ($excludes !== true) {
+ foreach (core_component::get_plugin_types() as $type=>$unused) {
+ if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) {
+ continue;
+ }
+ $plugins = core_component::get_plugin_list($type);
+ foreach ($plugins as $plugin=>$fulldir) {
+ if (!empty($excludes[$type]) and is_array($excludes[$type])
+ and in_array($plugin, $excludes[$type])) {
+ continue;
+ }
+
+ // Get the CSS from the plugin.
+ $sheetfile = "$fulldir/styles.css";
+ if (is_readable($sheetfile)) {
+ $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile;
+ }
+
+ // Create a list of candidate sheets from parents (direct parent last) and current theme.
+ $candidates = array();
+ foreach (array_reverse($this->parent_configs) as $parent_config) {
+ $candidates[] = $parent_config->name;
+ }
+ $candidates[] = $this->name;
+
+ // Add the sheets found.
+ foreach ($candidates as $candidate) {
+ $sheetthemefile = "$fulldir/styles_{$candidate}.css";
+ if (is_readable($sheetthemefile)) {
+ $cssfiles['plugins'][$type.'_'.$plugin.'_'.$candidate] = $sheetthemefile;
+ }
+ }
+ }
+ }
+ }
+
+ // Find out wanted parent sheets.
+ $excludes = $this->resolve_excludes('parents_exclude_sheets');
+ if ($excludes !== true) {
+ foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
+ $parent = $parent_config->name;
+ if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {
+ continue;
+ }
+ foreach ($parent_config->sheets as $sheet) {
+ if (!empty($excludes[$parent]) && is_array($excludes[$parent])
+ && in_array($sheet, $excludes[$parent])) {
+ continue;
+ }
+
+ // We never refer to the parent LESS files.
+ $sheetfile = "$parent_config->dir/style/$sheet.css";
+ if (is_readable($sheetfile)) {
+ $cssfiles['parents'][$parent][$sheet] = $sheetfile;
+ }
+ }
+ }
+ }
+
+
+ // Current theme sheets.
+ // We first add the SCSS file because we want the CSS ones to
+ // be included after the SCSS code.
+ if ($this->get_scss_property()) {
+ $cssfiles['theme'][self::SCSS_KEY] = true;
+ }
+ if (is_array($this->sheets)) {
+ foreach ($this->sheets as $sheet) {
+ $sheetfile = "$this->dir/style/$sheet.css";
+ if (is_readable($sheetfile) && !isset($cssfiles['theme'][$sheet])) {
+ $cssfiles['theme'][$sheet] = $sheetfile;
+ }
+ }
+ }
+
+ if ($cache) {
+ $files = $cssfiles;
+ $files['created'] = time();
+ $cache->set($cachekey, $files);
+ }
+ return $cssfiles;
+ }
+
+ /**
+ * Return the CSS content generated from the SCSS file.
+ *
+ * @param bool $themedesigner True if theme designer is enabled.
+ * @return bool|string Return false when the compilation failed. Else the compiled string.
+ */
+ protected function get_css_content_from_scss($themedesigner) {
+ global $CFG;
+
+ list($paths, $scss) = $this->get_scss_property();
+ if (!$scss) {
+ throw new coding_exception('The theme did not define a SCSS file, or it is not readable.');
+ }
+
+ // We might need more memory/time to do this, so let's play safe.
+ raise_memory_limit(MEMORY_EXTRA);
+ core_php_time_limit::raise(300);
+
+ // TODO: MDL-62757 When changing anything in this method please do not forget to check
+ // if the validate() method in class admin_setting_configthemepreset needs updating too.
+
+ $cachedir = make_localcache_directory('scsscache-' . $this->name, false);
+ $cacheoptions = [];
+ if ($themedesigner) {
+ $cacheoptions = array(
+ 'cacheDir' => $cachedir,
+ 'prefix' => 'scssphp_',
+ 'forceRefresh' => false,
+ );
+ } else {
+ if (file_exists($cachedir)) {
+ remove_dir($cachedir);
+ }
+ }
+
+ // Set-up the compiler.
+ $compiler = new core_scss($cacheoptions);
+
+ if ($this->supports_source_maps($themedesigner)) {
+ // Enable source maps.
+ $compiler->setSourceMapOptions([
+ 'sourceMapBasepath' => str_replace('\\', '/', $CFG->dirroot),
+ 'sourceMapRootpath' => $CFG->wwwroot . '/'
+ ]);
+ $compiler->setSourceMap($compiler::SOURCE_MAP_INLINE);
+ }
+
+ $compiler->prepend_raw_scss($this->get_pre_scss_code());
+ if (is_string($scss)) {
+ $compiler->set_file($scss);
+ } else {
+ $compiler->append_raw_scss($scss($this));
+ $compiler->setImportPaths($paths);
+ }
+ $compiler->append_raw_scss($this->get_extra_scss_code());
+
+ try {
+ // Compile!
+ $compiled = $compiler->to_css();
+
+ } catch (\Exception $e) {
+ $compiled = false;
+ debugging('Error while compiling SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);
+ }
+
+ // Try to save memory.
+ $compiler = null;
+ unset($compiler);
+
+ return $compiled;
+ }
+
+ /**
+ * Return the precompiled CSS if the precompiledcsscallback exists.
+ *
+ * @return string Return compiled css.
+ */
+ public function get_precompiled_css_content() {
+ $configs = array_reverse($this->parent_configs) + [$this];
+ $css = '';
+
+ foreach ($configs as $config) {
+ if (isset($config->precompiledcsscallback)) {
+ $function = $config->precompiledcsscallback;
+ if (function_exists($function)) {
+ $css .= $function($this);
+ }
+ }
+ }
+ return $css;
+ }
+
+ /**
+ * Get the icon system to use.
+ *
+ * @return string
+ */
+ public function get_icon_system() {
+
+ // Getting all the candidate functions.
+ $system = false;
+ if (isset($this->iconsystem) && \core\output\icon_system::is_valid_system($this->iconsystem)) {
+ return $this->iconsystem;
+ }
+ foreach ($this->parent_configs as $parent_config) {
+ if (isset($parent_config->iconsystem) && \core\output\icon_system::is_valid_system($parent_config->iconsystem)) {
+ return $parent_config->iconsystem;
+ }
+ }
+ return \core\output\icon_system::STANDARD;
+ }
+
+ /**
+ * Return extra SCSS code to add when compiling.
+ *
+ * This is intended to be used by themes to inject some SCSS code
+ * before it gets compiled. If you want to inject variables you
+ * should use {@link self::get_scss_variables()}.
+ *
+ * @return string The SCSS code to inject.
+ */
+ public function get_extra_scss_code() {
+ $content = '';
+
+ // Getting all the candidate functions.
+ $candidates = array();
+ foreach (array_reverse($this->parent_configs) as $parent_config) {
+ if (!isset($parent_config->extrascsscallback)) {
+ continue;
+ }
+ $candidates[] = $parent_config->extrascsscallback;
+ }
+
+ if (isset($this->extrascsscallback)) {
+ $candidates[] = $this->extrascsscallback;
+ }
+
+ // Calling the functions.
+ foreach ($candidates as $function) {
+ if (function_exists($function)) {
+ $content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n";
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * SCSS code to prepend when compiling.
+ *
+ * This is intended to be used by themes to inject SCSS code before it gets compiled.
+ *
+ * @return string The SCSS code to inject.
+ */
+ public function get_pre_scss_code() {
+ $content = '';
+
+ // Getting all the candidate functions.
+ $candidates = array();
+ foreach (array_reverse($this->parent_configs) as $parent_config) {
+ if (!isset($parent_config->prescsscallback)) {
+ continue;
+ }
+ $candidates[] = $parent_config->prescsscallback;
+ }
+
+ if (isset($this->prescsscallback)) {
+ $candidates[] = $this->prescsscallback;
+ }
+
+ // Calling the functions.
+ foreach ($candidates as $function) {
+ if (function_exists($function)) {
+ $content .= "\n/** Pre-SCSS from $function **/\n" . $function($this) . "\n";
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Get the SCSS property.
+ *
+ * This resolves whether a SCSS file (or content) has to be used when generating
+ * the stylesheet for the theme. It will look at parents themes and check the
+ * SCSS properties there.
+ *
+ * @return array|false False when SCSS is not used.
+ * An array with the import paths, and the path to the SCSS file or Closure as second.
+ */
+ public function get_scss_property() {
+ if ($this->scsscache === null) {
+ $configs = [$this] + $this->parent_configs;
+ $scss = null;
+
+ foreach ($configs as $config) {
+ $path = "{$config->dir}/scss";
+
+ // We collect the SCSS property until we've found one.
+ if (empty($scss) && !empty($config->scss)) {
+ $candidate = is_string($config->scss) ? "{$path}/{$config->scss}.scss" : $config->scss;
+ if ($candidate instanceof Closure) {
+ $scss = $candidate;
+ } else if (is_string($candidate) && is_readable($candidate)) {
+ $scss = $candidate;
+ }
+ }
+
+ // We collect the import paths once we've found a SCSS property.
+ if ($scss && is_dir($path)) {
+ $paths[] = $path;
+ }
+
+ }
+
+ $this->scsscache = $scss !== null ? [$paths, $scss] : false;
+ }
+
+ return $this->scsscache;
+ }
+
+ /**
+ * Generate a URL to the file that serves theme JavaScript files.
+ *
+ * If we determine that the theme has no relevant files, then we return
+ * early with a null value.
+ *
+ * @param bool $inhead true means head url, false means footer
+ * @return moodle_url|null
+ */
+ public function javascript_url($inhead) {
+ global $CFG;
+
+ $rev = theme_get_revision();
+ $params = array('theme'=>$this->name,'rev'=>$rev);
+ $params['type'] = $inhead ? 'head' : 'footer';
+
+ // Return early if there are no files to serve
+ if (count($this->javascript_files($params['type'])) === 0) {
+ return null;
+ }
+
+ if (!empty($CFG->slasharguments) and $rev > 0) {
+ $url = new moodle_url("/theme/javascript.php");
+ $url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true);
+ return $url;
+ } else {
+ return new moodle_url('/theme/javascript.php', $params);
+ }
+ }
+
+ /**
+ * Get the URL's for the JavaScript files used by this theme.
+ * They won't be served directly, instead they'll be mediated through
+ * theme/javascript.php.
+ *
+ * @param string $type Either javascripts_footer, or javascripts
+ * @return array
+ */
+ public function javascript_files($type) {
+ if ($type === 'footer') {
+ $type = 'javascripts_footer';
+ } else {
+ $type = 'javascripts';
+ }
+
+ $js = array();
+ // find out wanted parent javascripts
+ $excludes = $this->resolve_excludes('parents_exclude_javascripts');
+ if ($excludes !== true) {
+ foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
+ $parent = $parent_config->name;
+ if (empty($parent_config->$type)) {
+ continue;
+ }
+ if (!empty($excludes[$parent]) and $excludes[$parent] === true) {
+ continue;
+ }
+ foreach ($parent_config->$type as $javascript) {
+ if (!empty($excludes[$parent]) and is_array($excludes[$parent])
+ and in_array($javascript, $excludes[$parent])) {
+ continue;
+ }
+ $javascriptfile = "$parent_config->dir/javascript/$javascript.js";
+ if (is_readable($javascriptfile)) {
+ $js[] = $javascriptfile;
+ }
+ }
+ }
+ }
+
+ // current theme javascripts
+ if (is_array($this->$type)) {
+ foreach ($this->$type as $javascript) {
+ $javascriptfile = "$this->dir/javascript/$javascript.js";
+ if (is_readable($javascriptfile)) {
+ $js[] = $javascriptfile;
+ }
+ }
+ }
+ return $js;
+ }
+
+ /**
+ * Resolves an exclude setting to the themes setting is applicable or the
+ * setting of its closest parent.
+ *
+ * @param string $variable The name of the setting the exclude setting to resolve
+ * @param string $default
+ * @return mixed
+ */
+ protected function resolve_excludes($variable, $default = null) {
+ $setting = $default;
+ if (is_array($this->{$variable}) or $this->{$variable} === true) {
+ $setting = $this->{$variable};
+ } else {
+ foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base last
+ if (!isset($parent_config->{$variable})) {
+ continue;
+ }
+ if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) {
+ $setting = $parent_config->{$variable};
+ break;
+ }
+ }
+ }
+ return $setting;
+ }
+
+ /**
+ * Returns the content of the one huge javascript file merged from all theme javascript files.
+ *
+ * @param bool $type
+ * @return string
+ */
+ public function javascript_content($type) {
+ $jsfiles = $this->javascript_files($type);
+ $js = '';
+ foreach ($jsfiles as $jsfile) {
+ $js .= file_get_contents($jsfile)."\n";
+ }
+ return $js;
+ }
+
+ /**
+ * Post processes CSS.
+ *
+ * This method post processes all of the CSS before it is served for this theme.
+ * This is done so that things such as image URL's can be swapped in and to
+ * run any specific CSS post process method the theme has requested.
+ * This allows themes to use CSS settings.
+ *
+ * @param string $css The CSS to process.
+ * @return string The processed CSS.
+ */
+ public function post_process($css) {
+ // now resolve all image locations
+ if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
+ $replaced = array();
+ foreach ($matches as $match) {
+ if (isset($replaced[$match[0]])) {
+ continue;
+ }
+ $replaced[$match[0]] = true;
+ $imagename = $match[2];
+ $component = rtrim($match[1], '|');
+ $imageurl = $this->image_url($imagename, $component)->out(false);
+ // we do not need full url because the image.php is always in the same dir
+ $imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl);
+ $css = str_replace($match[0], $imageurl, $css);
+ }
+ }
+
+ // Now resolve all font locations.
+ if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
+ $replaced = array();
+ foreach ($matches as $match) {
+ if (isset($replaced[$match[0]])) {
+ continue;
+ }
+ $replaced[$match[0]] = true;
+ $fontname = $match[2];
+ $component = rtrim($match[1], '|');
+ $fonturl = $this->font_url($fontname, $component)->out(false);
+ // We do not need full url because the font.php is always in the same dir.
+ $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl);
+ $css = str_replace($match[0], $fonturl, $css);
+ }
+ }
+
+ // Now resolve all theme settings or do any other postprocessing.
+ // This needs to be done before calling core parser, since the parser strips [[settings]] tags.
+ $csspostprocess = $this->csspostprocess;
+ if ($csspostprocess && function_exists($csspostprocess)) {
+ $css = $csspostprocess($css, $this);
+ }
+
+ // Post processing using an object representation of CSS.
+ $treeprocessor = $this->get_css_tree_post_processor();
+ $needsparsing = !empty($treeprocessor) || !empty($this->rtlmode);
+ if ($needsparsing) {
+
+ // We might need more memory/time to do this, so let's play safe.
+ raise_memory_limit(MEMORY_EXTRA);
+ core_php_time_limit::raise(300);
+
+ $parser = new core_cssparser($css);
+ $csstree = $parser->parse();
+ unset($parser);
+
+ if ($this->rtlmode) {
+ $this->rtlize($csstree);
+ }
+
+ if ($treeprocessor) {
+ $treeprocessor($csstree, $this);
+ }
+
+ $css = $csstree->render();
+ unset($csstree);
+ }
+
+ return $css;
+ }
+
+ /**
+ * Flip a stylesheet to RTL.
+ *
+ * @param mixed $csstree The parsed CSS tree structure to flip.
+ * @return void
+ */
+ protected function rtlize($csstree) {
+ $rtlcss = new core_rtlcss($csstree);
+ $rtlcss->flip();
+ }
+
+ /**
+ * Return the direct URL for an image from the pix folder.
+ *
+ * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
+ *
+ * @deprecated since Moodle 3.3
+ * @param string $imagename the name of the icon.
+ * @param string $component specification of one plugin like in get_string()
+ * @return moodle_url
+ */
+ public function pix_url($imagename, $component) {
+ debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
+ return $this->image_url($imagename, $component);
+ }
+
+ /**
+ * Return the direct URL for an image from the pix folder.
+ *
+ * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
+ *
+ * @param string $imagename the name of the icon.
+ * @param string $component specification of one plugin like in get_string()
+ * @return moodle_url
+ */
+ public function image_url($imagename, $component) {
+ global $CFG;
+
+ $params = array('theme'=>$this->name);
+ $svg = $this->use_svg_icons();
+
+ if (empty($component) or $component === 'moodle' or $component === 'core') {
+ $params['component'] = 'core';
+ } else {
+ $params['component'] = $component;
+ }
+
+ $rev = theme_get_revision();
+ if ($rev != -1) {
+ $params['rev'] = $rev;
+ }
+
+ $params['image'] = $imagename;
+
+ $url = new moodle_url("/theme/image.php");
+ if (!empty($CFG->slasharguments) and $rev > 0) {
+ $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image'];
+ if (!$svg) {
+ // We add a simple /_s to the start of the path.
+ // The underscore is used to ensure that it isn't a valid theme name.
+ $path = '/_s'.$path;
+ }
+ $url->set_slashargument($path, 'noparam', true);
+ } else {
+ if (!$svg) {
+ // We add an SVG param so that we know not to serve SVG images.
+ // We do this because all modern browsers support SVG and this param will one day be removed.
+ $params['svg'] = '0';
+ }
+ $url->params($params);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Return the URL for a font
+ *
+ * @param string $font the name of the font (including extension).
+ * @param string $component specification of one plugin like in get_string()
+ * @return moodle_url
+ */
+ public function font_url($font, $component) {
+ global $CFG;
+
+ $params = array('theme'=>$this->name);
+
+ if (empty($component) or $component === 'moodle' or $component === 'core') {
+ $params['component'] = 'core';
+ } else {
+ $params['component'] = $component;
+ }
+
+ $rev = theme_get_revision();
+ if ($rev != -1) {
+ $params['rev'] = $rev;
+ }
+
+ $params['font'] = $font;
+
+ $url = new moodle_url("/theme/font.php");
+ if (!empty($CFG->slasharguments) and $rev > 0) {
+ $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font'];
+ $url->set_slashargument($path, 'noparam', true);
+ } else {
+ $url->params($params);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns URL to the stored file via pluginfile.php.
+ *
+ * Note the theme must also implement pluginfile.php handler,
+ * theme revision is used instead of the itemid.
+ *
+ * @param string $setting
+ * @param string $filearea
+ * @return string protocol relative URL or null if not present
+ */
+ public function setting_file_url($setting, $filearea) {
+ global $CFG;
+
+ if (empty($this->settings->$setting)) {
+ return null;
+ }
+
+ $component = 'theme_'.$this->name;
+ $itemid = theme_get_revision();
+ $filepath = $this->settings->$setting;
+ $syscontext = context_system::instance();
+
+ $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", "/$syscontext->id/$component/$filearea/$itemid".$filepath);
+
+ // Now this is tricky because the we can not hardcode http or https here, lets use the relative link.
+ // Note: unfortunately moodle_url does not support //urls yet.
+
+ $url = preg_replace('|^https?://|i', '//', $url->out(false));
+
+ return $url;
+ }
+
+ /**
+ * Serve the theme setting file.
+ *
+ * @param string $filearea
+ * @param array $args
+ * @param bool $forcedownload
+ * @param array $options
+ * @return bool may terminate if file not found or donotdie not specified
+ */
+ public function setting_file_serve($filearea, $args, $forcedownload, $options) {
+ global $CFG;
+ require_once("$CFG->libdir/filelib.php");
+
+ $syscontext = context_system::instance();
+ $component = 'theme_'.$this->name;
+
+ $revision = array_shift($args);
+ if ($revision < 0) {
+ $lifetime = 0;
+ } else {
+ $lifetime = 60*60*24*60;
+ // By default, theme files must be cache-able by both browsers and proxies.
+ if (!array_key_exists('cacheability', $options)) {
+ $options['cacheability'] = 'public';
+ }
+ }
+
+ $fs = get_file_storage();
+ $relativepath = implode('/', $args);
+
+ $fullpath = "/{$syscontext->id}/{$component}/{$filearea}/0/{$relativepath}";
+ $fullpath = rtrim($fullpath, '/');
+ if ($file = $fs->get_file_by_hash(sha1($fullpath))) {
+ send_stored_file($file, $lifetime, 0, $forcedownload, $options);
+ return true;
+ } else {
+ send_file_not_found();
+ }
+ }
+
+ /**
+ * Resolves the real image location.
+ *
+ * $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG
+ * and we need a way in which to turn it off.
+ * By default SVG won't be used unless asked for. This is done for two reasons:
+ * 1. It ensures that we don't serve svg images unless we really want to. The admin has selected to force them, of the users
+ * browser supports SVG.
+ * 2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded
+ * by the user due to security concerns.
+ *
+ * @param string $image name of image, may contain relative path
+ * @param string $component
+ * @param bool|null $svg Should SVG images also be looked for? If null, falls back to auto-detection of browser support
+ * @return string full file path
+ */
+ public function resolve_image_location($image, $component, $svg = false) {
+ global $CFG;
+
+ if (!is_bool($svg)) {
+ // If $svg isn't a bool then we need to decide for ourselves.
+ $svg = $this->use_svg_icons();
+ }
+
+ if ($component === 'moodle' or $component === 'core' or empty($component)) {
+ if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) {
+ return $imagefile;
+ }
+ foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
+ if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) {
+ return $imagefile;
+ }
+ }
+ if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) {
+ return $imagefile;
+ }
+ if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) {
+ return $imagefile;
+ }
+ return null;
+
+ } else if ($component === 'theme') { //exception
+ if ($image === 'favicon') {
+ return "$this->dir/pix/favicon.ico";
+ }
+ if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) {
+ return $imagefile;
+ }
+ foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
+ if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) {
+ return $imagefile;
+ }
+ }
+ return null;
+
+ } else {
+ if (strpos($component, '_') === false) {
+ $component = "mod_{$component}";
+ }
+ list($type, $plugin) = explode('_', $component, 2);
+
+ // In Moodle 4.0 we introduced a new image format.
+ // Support that image format here.
+ $candidates = [$image];
+
+ if ($type === 'mod') {
+ if ($image === 'icon' || $image === 'monologo') {
+ $candidates = ['monologo', 'icon'];
+ if ($image === 'icon') {
+ debugging(
+ "The 'icon' image for activity modules has been replaced with a new 'monologo'. " .
+ "Please update your calling code to fetch the new icon where possible. " .
+ "Called for component {$component}.",
+ DEBUG_DEVELOPER
+ );
+ }
+ }
+ }
+ foreach ($candidates as $image) {
+ if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) {
+ return $imagefile;
+ }
+
+ // Base first, the immediate parent last.
+ foreach (array_reverse($this->parent_configs) as $parentconfig) {
+ if ($imagefile = $this->image_exists("$parentconfig->dir/pix_plugins/$type/$plugin/$image", $svg)) {
+ return $imagefile;
+ }
+ }
+ if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) {
+ return $imagefile;
+ }
+ $dir = core_component::get_plugin_directory($type, $plugin);
+ if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) {
+ return $imagefile;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Resolves the real font location.
+ *
+ * @param string $font name of font file
+ * @param string $component
+ * @return string full file path
+ */
+ public function resolve_font_location($font, $component) {
+ global $CFG;
+
+ if ($component === 'moodle' or $component === 'core' or empty($component)) {
+ if (file_exists("$this->dir/fonts_core/$font")) {
+ return "$this->dir/fonts_core/$font";
+ }
+ foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
+ if (file_exists("$parent_config->dir/fonts_core/$font")) {
+ return "$parent_config->dir/fonts_core/$font";
+ }
+ }
+ if (file_exists("$CFG->dataroot/fonts/$font")) {
+ return "$CFG->dataroot/fonts/$font";
+ }
+ if (file_exists("$CFG->dirroot/lib/fonts/$font")) {
+ return "$CFG->dirroot/lib/fonts/$font";
+ }
+ return null;
+
+ } else if ($component === 'theme') { // Exception.
+ if (file_exists("$this->dir/fonts/$font")) {
+ return "$this->dir/fonts/$font";
+ }
+ foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
+ if (file_exists("$parent_config->dir/fonts/$font")) {
+ return "$parent_config->dir/fonts/$font";
+ }
+ }
+ return null;
+
+ } else {
+ if (strpos($component, '_') === false) {
+ $component = 'mod_'.$component;
+ }
+ list($type, $plugin) = explode('_', $component, 2);
+
+ if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) {
+ return "$this->dir/fonts_plugins/$type/$plugin/$font";
+ }
+ foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
+ if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) {
+ return "$parent_config->dir/fonts_plugins/$type/$plugin/$font";
+ }
+ }
+ if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) {
+ return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font";
+ }
+ $dir = core_component::get_plugin_directory($type, $plugin);
+ if (file_exists("$dir/fonts/$font")) {
+ return "$dir/fonts/$font";
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Return true if we should look for SVG images as well.
+ *
+ * @return bool
+ */
+ public function use_svg_icons() {
+ if ($this->usesvg === null) {
+ $this->usesvg = core_useragent::supports_svg();
+ }
+
+ return $this->usesvg;
+ }
+
+ /**
+ * Forces the usesvg setting to either true or false, avoiding any decision making.
+ *
+ * This function should only ever be used when absolutely required, and before any generation of image URL's has occurred.
+ * DO NOT ABUSE THIS FUNCTION... not that you'd want to right ;)
+ *
+ * @param bool $setting True to force the use of svg when available, null otherwise.
+ */
+ public function force_svg_use($setting) {
+ $this->usesvg = (bool)$setting;
+ }
+
+ /**
+ * Set to be in RTL mode.
+ *
+ * This will likely be used when post processing the CSS before serving it.
+ *
+ * @param bool $inrtl True when in RTL mode.
+ */
+ public function set_rtl_mode($inrtl = true) {
+ $this->rtlmode = $inrtl;
+ }
+
+ /**
+ * Checks if source maps are supported
+ *
+ * @param bool $themedesigner True if theme designer is enabled.
+ * @return boolean True if source maps are supported.
+ */
+ public function supports_source_maps($themedesigner): bool {
+ if (empty($this->rtlmode) && $themedesigner) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Whether the theme is being served in RTL mode.
+ *
+ * @return bool True when in RTL mode.
+ */
+ public function get_rtl_mode() {
+ return $this->rtlmode;
+ }
+
+ /**
+ * Checks if file with any image extension exists.
+ *
+ * The order to these images was adjusted prior to the release of 2.4
+ * At that point the were the following image counts in Moodle core:
+ *
+ * - png = 667 in pix dirs (1499 total)
+ * - gif = 385 in pix dirs (606 total)
+ * - jpg = 62 in pix dirs (74 total)
+ * - jpeg = 0 in pix dirs (1 total)
+ *
+ * There is work in progress to move towards SVG presently hence that has been prioritiesed.
+ *
+ * @param string $filepath
+ * @param bool $svg If set to true SVG images will also be looked for.
+ * @return string image name with extension
+ */
+ private static function image_exists($filepath, $svg = false) {
+ if ($svg && file_exists("$filepath.svg")) {
+ return "$filepath.svg";
+ } else if (file_exists("$filepath.png")) {
+ return "$filepath.png";
+ } else if (file_exists("$filepath.gif")) {
+ return "$filepath.gif";
+ } else if (file_exists("$filepath.jpg")) {
+ return "$filepath.jpg";
+ } else if (file_exists("$filepath.jpeg")) {
+ return "$filepath.jpeg";
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Loads the theme config from config.php file.
+ *
+ * @param string $themename
+ * @param stdClass $settings from config_plugins table
+ * @param boolean $parentscheck true to also check the parents. .
+ * @return ?stdClass The theme configuration
+ */
+ private static function find_theme_config($themename, $settings, $parentscheck = true) {
+ // We have to use the variable name $THEME (upper case) because that
+ // is what is used in theme config.php files.
+
+ if (!$dir = theme_config::find_theme_location($themename)) {
+ return null;
+ }
+
+ $THEME = new stdClass();
+ $THEME->name = $themename;
+ $THEME->dir = $dir;
+ $THEME->settings = $settings;
+
+ global $CFG; // just in case somebody tries to use $CFG in theme config
+ include("$THEME->dir/config.php");
+
+ // verify the theme configuration is OK
+ if (!is_array($THEME->parents)) {
+ // parents option is mandatory now
+ return null;
+ } else {
+ // We use $parentscheck to only check the direct parents (avoid infinite loop).
+ if ($parentscheck) {
+ // Find all parent theme configs.
+ foreach ($THEME->parents as $parent) {
+ $parentconfig = theme_config::find_theme_config($parent, $settings, false);
+ if (empty($parentconfig)) {
+ return null;
+ }
+ }
+ }
+ }
+
+ return $THEME;
+ }
+
+ /**
+ * Finds the theme location and verifies the theme has all needed files
+ * and is not obsoleted.
+ *
+ * @param string $themename
+ * @return string full dir path or null if not found
+ */
+ private static function find_theme_location($themename) {
+ global $CFG;
+
+ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
+ $dir = "$CFG->dirroot/theme/$themename";
+
+ } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
+ $dir = "$CFG->themedir/$themename";
+
+ } else {
+ return null;
+ }
+
+ if (file_exists("$dir/styles.php")) {
+ //legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings page
+ return null;
+ }
+
+ return $dir;
+ }
+
+ /**
+ * Get the renderer for a part of Moodle for this theme.
+ *
+ * @param moodle_page $page the page we are rendering
+ * @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
+ * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
+ * @param string $target one of rendering target constants
+ * @return renderer_base the requested renderer.
+ */
+ public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
+ if (is_null($this->rf)) {
+ $classname = $this->rendererfactory;
+ $this->rf = new $classname($this);
+ }
+
+ return $this->rf->get_renderer($page, $component, $subtype, $target);
+ }
+
+ /**
+ * Get the information from {@link $layouts} for this type of page.
+ *
+ * @param string $pagelayout the the page layout name.
+ * @return array the appropriate part of {@link $layouts}.
+ */
+ protected function layout_info_for_page($pagelayout) {
+ if (array_key_exists($pagelayout, $this->layouts)) {
+ return $this->layouts[$pagelayout];
+ } else {
+ debugging('Invalid page layout specified: ' . $pagelayout);
+ return $this->layouts['standard'];
+ }
+ }
+
+ /**
+ * Given the settings of this theme, and the page pagelayout, return the
+ * full path of the page layout file to use.
+ *
+ * Used by {@link core_renderer::header()}.
+ *
+ * @param string $pagelayout the the page layout name.
+ * @return string Full path to the lyout file to use
+ */
+ public function layout_file($pagelayout) {
+ global $CFG;
+
+ $layoutinfo = $this->layout_info_for_page($pagelayout);
+ $layoutfile = $layoutinfo['file'];
+
+ if (array_key_exists('theme', $layoutinfo)) {
+ $themes = array($layoutinfo['theme']);
+ } else {
+ $themes = array_merge(array($this->name),$this->parents);
+ }
+
+ foreach ($themes as $theme) {
+ if ($dir = $this->find_theme_location($theme)) {
+ $path = "$dir/layout/$layoutfile";
+
+ // Check the template exists, return general base theme template if not.
+ if (is_readable($path)) {
+ return $path;
+ }
+ }
+ }
+
+ throw new coding_exception('Can not find layout file for: ' . $pagelayout . ' (' . $layoutfile . ')');
+ }
+
+ /**
+ * Returns auxiliary page layout options specified in layout configuration array.
+ *
+ * @param string $pagelayout
+ * @return array
+ */
+ public function pagelayout_options($pagelayout) {
+ $info = $this->layout_info_for_page($pagelayout);
+ if (!empty($info['options'])) {
+ return $info['options'];
+ }
+ return array();
+ }
+
+ /**
+ * Inform a block_manager about the block regions this theme wants on this
+ * page layout.
+ *
+ * @param string $pagelayout the general type of the page.
+ * @param block_manager $blockmanager the block_manger to set up.
+ */
+ public function setup_blocks($pagelayout, $blockmanager) {
+ $layoutinfo = $this->layout_info_for_page($pagelayout);
+ if (!empty($layoutinfo['regions'])) {
+ $blockmanager->add_regions($layoutinfo['regions'], false);
+ $blockmanager->set_default_region($layoutinfo['defaultregion']);
+ }
+ }
+
+ /**
+ * Gets the visible name for the requested block region.
+ *
+ * @param string $region The region name to get
+ * @param string $theme The theme the region belongs to (may come from the parent theme)
+ * @return string
+ */
+ protected function get_region_name($region, $theme) {
+
+ $stringman = get_string_manager();
+
+ // Check if the name is defined in the theme.
+ if ($stringman->string_exists('region-' . $region, 'theme_' . $theme)) {
+ return get_string('region-' . $region, 'theme_' . $theme);
+ }
+
+ // Check the theme parents.
+ foreach ($this->parents as $parentthemename) {
+ if ($stringman->string_exists('region-' . $region, 'theme_' . $parentthemename)) {
+ return get_string('region-' . $region, 'theme_' . $parentthemename);
+ }
+ }
+
+ // Last resort, try the boost theme for names.
+ return get_string('region-' . $region, 'theme_boost');
+ }
+
+ /**
+ * Get the list of all block regions known to this theme in all templates.
+ *
+ * @return array internal region name => human readable name.
+ */
+ public function get_all_block_regions() {
+ $regions = array();
+ foreach ($this->layouts as $layoutinfo) {
+ foreach ($layoutinfo['regions'] as $region) {
+ $regions[$region] = $this->get_region_name($region, $this->name);
+ }
+ }
+ return $regions;
+ }
+
+ /**
+ * Returns the human readable name of the theme
+ *
+ * @return string
+ */
+ public function get_theme_name() {
+ return get_string('pluginname', 'theme_'.$this->name);
+ }
+
+ /**
+ * Returns the block render method.
+ *
+ * It is set by the theme via:
+ * $THEME->blockrendermethod = '...';
+ *
+ * It can be one of two values, blocks or blocks_for_region.
+ * It should be set to the method being used by the theme layouts.
+ *
+ * @return string
+ */
+ public function get_block_render_method() {
+ if ($this->blockrendermethod) {
+ // Return the specified block render method.
+ return $this->blockrendermethod;
+ }
+ // Its not explicitly set, check the parent theme configs.
+ foreach ($this->parent_configs as $config) {
+ if (isset($config->blockrendermethod)) {
+ return $config->blockrendermethod;
+ }
+ }
+ // Default it to blocks.
+ return 'blocks';
+ }
+
+ /**
+ * Get the callable for CSS tree post processing.
+ *
+ * @return string|null
+ */
+ public function get_css_tree_post_processor() {
+ $configs = [$this] + $this->parent_configs;
+ foreach ($configs as $config) {
+ if (!empty($config->csstreepostprocessor) && is_callable($config->csstreepostprocessor)) {
+ return $config->csstreepostprocessor;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/lib/classes/output/url_select.php b/lib/classes/output/url_select.php
new file mode 100644
index 00000000000..9051e83d0a4
--- /dev/null
+++ b/lib/classes/output/url_select.php
@@ -0,0 +1,282 @@
+.
+
+/**
+ * Simple URL selection widget description.
+ *
+ * @copyright 2009 Petr Skoda
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class url_select implements renderable, templatable {
+ /**
+ * @var array $urls associative array value=>label ex.: array(1=>'One, 2=>Two)
+ * it is also possible to specify optgroup as complex label array ex.:
+ * array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
+ * array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
+ */
+ var $urls;
+
+ /**
+ * @var string Selected option
+ */
+ var $selected;
+
+ /**
+ * @var array Nothing selected
+ */
+ var $nothing;
+
+ /**
+ * @var array Extra select field attributes
+ */
+ var $attributes = array();
+
+ /**
+ * @var string Button label
+ */
+ var $label = '';
+
+ /**
+ * @var array Button label's attributes
+ */
+ var $labelattributes = array();
+
+ /**
+ * @var string Wrapping div class
+ */
+ var $class = 'urlselect';
+
+ /**
+ * @var bool True if button disabled, false if normal
+ */
+ var $disabled = false;
+
+ /**
+ * @var string Button tooltip
+ */
+ var $tooltip = null;
+
+ /**
+ * @var string Form id
+ */
+ var $formid = null;
+
+ /**
+ * @var help_icon The help icon for this element.
+ */
+ var $helpicon = null;
+
+ /**
+ * @var string If set, makes button visible with given name for button
+ */
+ var $showbutton = null;
+
+ /**
+ * Constructor
+ * @param array $urls list of options
+ * @param string $selected selected element
+ * @param array $nothing
+ * @param string $formid
+ * @param string $showbutton Set to text of button if it should be visible
+ * or null if it should be hidden (hidden version always has text 'go')
+ */
+ public function __construct(array $urls, $selected = '', $nothing = array('' => 'choosedots'), $formid = null, $showbutton = null) {
+ $this->urls = $urls;
+ $this->selected = $selected;
+ $this->nothing = $nothing;
+ $this->formid = $formid;
+ $this->showbutton = $showbutton;
+ }
+
+ /**
+ * Adds help icon.
+ *
+ * @deprecated since Moodle 2.0
+ */
+ public function set_old_help_icon($helppage, $title, $component = 'moodle') {
+ throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
+ }
+
+ /**
+ * Adds help icon.
+ *
+ * @param string $identifier The keyword that defines a help page
+ * @param string $component
+ */
+ public function set_help_icon($identifier, $component = 'moodle') {
+ $this->helpicon = new help_icon($identifier, $component);
+ }
+
+ /**
+ * Sets select's label
+ *
+ * @param string $label
+ * @param array $attributes (optional)
+ */
+ public function set_label($label, $attributes = array()) {
+ $this->label = $label;
+ $this->labelattributes = $attributes;
+ }
+
+ /**
+ * Clean a URL.
+ *
+ * @param string $value The URL.
+ * @return string The cleaned URL.
+ */
+ protected function clean_url($value) {
+ global $CFG;
+
+ if (empty($value)) {
+ // Nothing.
+
+ } else if (strpos($value, $CFG->wwwroot . '/') === 0) {
+ $value = str_replace($CFG->wwwroot, '', $value);
+
+ } else if (strpos($value, '/') !== 0) {
+ debugging("Invalid url_select urls parameter: url '$value' is not local relative url!", DEBUG_DEVELOPER);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Flatten the options for Mustache.
+ *
+ * This also cleans the URLs.
+ *
+ * @param array $options The options.
+ * @param array $nothing The nothing option.
+ * @return array
+ */
+ protected function flatten_options($options, $nothing) {
+ $flattened = [];
+
+ foreach ($options as $value => $option) {
+ if (is_array($option)) {
+ foreach ($option as $groupname => $optoptions) {
+ if (!isset($flattened[$groupname])) {
+ $flattened[$groupname] = [
+ 'name' => $groupname,
+ 'isgroup' => true,
+ 'options' => []
+ ];
+ }
+ foreach ($optoptions as $optvalue => $optoption) {
+ $cleanedvalue = $this->clean_url($optvalue);
+ $flattened[$groupname]['options'][$cleanedvalue] = [
+ 'name' => $optoption,
+ 'value' => $cleanedvalue,
+ 'selected' => $this->selected == $optvalue,
+ ];
+ }
+ }
+
+ } else {
+ $cleanedvalue = $this->clean_url($value);
+ $flattened[$cleanedvalue] = [
+ 'name' => $option,
+ 'value' => $cleanedvalue,
+ 'selected' => $this->selected == $value,
+ ];
+ }
+ }
+
+ if (!empty($nothing)) {
+ $value = key($nothing);
+ $name = reset($nothing);
+ $flattened = [
+ $value => ['name' => $name, 'value' => $value, 'selected' => $this->selected == $value]
+ ] + $flattened;
+ }
+
+ // Make non-associative array.
+ foreach ($flattened as $key => $value) {
+ if (!empty($value['options'])) {
+ $flattened[$key]['options'] = array_values($value['options']);
+ }
+ }
+ $flattened = array_values($flattened);
+
+ return $flattened;
+ }
+
+ /**
+ * Export for template.
+ *
+ * @param renderer_base $output Renderer.
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ $attributes = $this->attributes;
+
+ $data = new stdClass();
+ $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('url_select_f');
+ $data->classes = $this->class;
+ $data->label = $this->label;
+ $data->disabled = $this->disabled;
+ $data->title = $this->tooltip;
+ $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('url_select');
+ $data->sesskey = sesskey();
+ $data->action = (new moodle_url('/course/jumpto.php'))->out(false);
+
+ // Remove attributes passed as property directly.
+ unset($attributes['class']);
+ unset($attributes['id']);
+ unset($attributes['name']);
+ unset($attributes['title']);
+ unset($attributes['disabled']);
+
+ $data->showbutton = $this->showbutton;
+
+ // Select options.
+ $nothing = false;
+ if (is_string($this->nothing) && $this->nothing !== '') {
+ $nothing = ['' => $this->nothing];
+ } else if (is_array($this->nothing)) {
+ $nothingvalue = reset($this->nothing);
+ if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
+ $nothing = [key($this->nothing) => get_string('choosedots')];
+ } else {
+ $nothing = $this->nothing;
+ }
+ }
+ $data->options = $this->flatten_options($this->urls, $nothing);
+
+ // Label attributes.
+ $data->labelattributes = [];
+ // Unset label attributes that are already in the template.
+ unset($this->labelattributes['for']);
+ // Map the label attributes.
+ foreach ($this->labelattributes as $key => $value) {
+ $data->labelattributes[] = ['name' => $key, 'value' => $value];
+ }
+
+ // Help icon.
+ $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
+
+ // Finally all the remaining attributes.
+ $data->attributes = [];
+ foreach ($attributes as $key => $value) {
+ $data->attributes[] = ['name' => $key, 'value' => $value];
+ }
+
+ return $data;
+ }
+}
diff --git a/lib/classes/output/user_picture.php b/lib/classes/output/user_picture.php
new file mode 100644
index 00000000000..d278e4d2c10
--- /dev/null
+++ b/lib/classes/output/user_picture.php
@@ -0,0 +1,302 @@
+.
+
+/**
+ * Data structure representing a user picture.
+ *
+ * @copyright 2009 Nicolas Connault, 2010 Petr Skoda
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Modle 2.0
+ * @package core
+ * @category output
+ */
+class user_picture implements renderable {
+ /**
+ * @var stdClass A user object with at least fields all columns specified
+ * in $fields array constant set.
+ */
+ public $user;
+
+ /**
+ * @var int The course id. Used when constructing the link to the user's
+ * profile, page course id used if not specified.
+ */
+ public $courseid;
+
+ /**
+ * @var bool Add course profile link to image
+ */
+ public $link = true;
+
+ /**
+ * @var int Size in pixels. Special values are (true/1 = 100px) and (false/0 = 35px) for backward compatibility.
+ * Recommended values (supporting user initials too): 16, 35, 64 and 100.
+ */
+ public $size = 35;
+
+ /**
+ * @var bool Add non-blank alt-text to the image.
+ * Default true, set to false when image alt just duplicates text in screenreaders.
+ */
+ public $alttext = true;
+
+ /**
+ * @var bool Whether or not to open the link in a popup window.
+ */
+ public $popup = false;
+
+ /**
+ * @var string Image class attribute
+ */
+ public $class = 'userpicture';
+
+ /**
+ * @var bool Whether to be visible to screen readers.
+ */
+ public $visibletoscreenreaders = true;
+
+ /**
+ * @var bool Whether to include the fullname in the user picture link.
+ */
+ public $includefullname = false;
+
+ /**
+ * @var mixed Include user authentication token. True indicates to generate a token for current user, and integer value
+ * indicates to generate a token for the user whose id is the value indicated.
+ */
+ public $includetoken = false;
+
+ /**
+ * User picture constructor.
+ *
+ * @param stdClass $user user record with at least id, picture, imagealt, firstname and lastname set.
+ * It is recommended to add also contextid of the user for performance reasons.
+ */
+ public function __construct(stdClass $user) {
+ global $DB;
+
+ if (empty($user->id)) {
+ throw new coding_exception('User id is required when printing user avatar image.');
+ }
+
+ // only touch the DB if we are missing data and complain loudly...
+ $needrec = false;
+ foreach (\core_user\fields::get_picture_fields() as $field) {
+ if (!property_exists($user, $field)) {
+ $needrec = true;
+ debugging('Missing '.$field.' property in $user object, this is a performance problem that needs to be fixed by a developer. '
+ .'Please use the \core_user\fields API to get the full list of required fields.', DEBUG_DEVELOPER);
+ break;
+ }
+ }
+
+ if ($needrec) {
+ $this->user = $DB->get_record('user', array('id' => $user->id),
+ implode(',', \core_user\fields::get_picture_fields()), MUST_EXIST);
+ } else {
+ $this->user = clone($user);
+ }
+ }
+
+ /**
+ * Returns a list of required user fields, useful when fetching required user info from db.
+ *
+ * In some cases we have to fetch the user data together with some other information,
+ * the idalias is useful there because the id would otherwise override the main
+ * id of the result record. Please note it has to be converted back to id before rendering.
+ *
+ * @param string $tableprefix name of database table prefix in query
+ * @param array $extrafields extra fields to be included in result (do not include TEXT columns because it would break SELECT DISTINCT in MSSQL and ORACLE)
+ * @param string $idalias alias of id field
+ * @param string $fieldprefix prefix to add to all columns in their aliases, does not apply to 'id'
+ * @return string
+ * @deprecated since Moodle 3.11 MDL-45242
+ * @see \core_user\fields
+ */
+ public static function fields($tableprefix = '', array $extrafields = NULL, $idalias = 'id', $fieldprefix = '') {
+ debugging('user_picture::fields() is deprecated. Please use the \core_user\fields API instead.', DEBUG_DEVELOPER);
+ $userfields = \core_user\fields::for_userpic();
+ if ($extrafields) {
+ $userfields->including(...$extrafields);
+ }
+ $selects = $userfields->get_sql($tableprefix, false, $fieldprefix, $idalias, false)->selects;
+ if ($tableprefix === '') {
+ // If no table alias is specified, don't add {user}. in front of fields.
+ $selects = str_replace('{user}.', '', $selects);
+ }
+ // Maintain legacy behaviour where the field list was done with 'implode' and no spaces.
+ $selects = str_replace(', ', ',', $selects);
+ return $selects;
+ }
+
+ /**
+ * Extract the aliased user fields from a given record
+ *
+ * Given a record that was previously obtained using {@link self::fields()} with aliases,
+ * this method extracts user related unaliased fields.
+ *
+ * @param stdClass $record containing user picture fields
+ * @param array $extrafields extra fields included in the $record
+ * @param string $idalias alias of the id field
+ * @param string $fieldprefix prefix added to all columns in their aliases, does not apply to 'id'
+ * @return stdClass object with unaliased user fields
+ */
+ public static function unalias(stdClass $record, array $extrafields = null, $idalias = 'id', $fieldprefix = '') {
+
+ if (empty($idalias)) {
+ $idalias = 'id';
+ }
+
+ $return = new stdClass();
+
+ foreach (\core_user\fields::get_picture_fields() as $field) {
+ if ($field === 'id') {
+ if (property_exists($record, $idalias)) {
+ $return->id = $record->{$idalias};
+ }
+ } else {
+ if (property_exists($record, $fieldprefix.$field)) {
+ $return->{$field} = $record->{$fieldprefix.$field};
+ }
+ }
+ }
+ // add extra fields if not already there
+ if ($extrafields) {
+ foreach ($extrafields as $e) {
+ if ($e === 'id' or property_exists($return, $e)) {
+ continue;
+ }
+ $return->{$e} = $record->{$fieldprefix.$e};
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Works out the URL for the users picture.
+ *
+ * This method is recommended as it avoids costly redirects of user pictures
+ * if requests are made for non-existent files etc.
+ *
+ * @param moodle_page $page
+ * @param renderer_base $renderer
+ * @return moodle_url
+ */
+ public function get_url(moodle_page $page, renderer_base $renderer = null) {
+ global $CFG;
+
+ if (is_null($renderer)) {
+ $renderer = $page->get_renderer('core');
+ }
+
+ // Sort out the filename and size. Size is only required for the gravatar
+ // implementation presently.
+ if (empty($this->size)) {
+ $filename = 'f2';
+ $size = 35;
+ } else if ($this->size === true or $this->size == 1) {
+ $filename = 'f1';
+ $size = 100;
+ } else if ($this->size > 100) {
+ $filename = 'f3';
+ $size = (int)$this->size;
+ } else if ($this->size >= 50) {
+ $filename = 'f1';
+ $size = (int)$this->size;
+ } else {
+ $filename = 'f2';
+ $size = (int)$this->size;
+ }
+
+ $defaulturl = $renderer->image_url('u/'.$filename); // default image
+
+ if ((!empty($CFG->forcelogin) and !isloggedin()) ||
+ (!empty($CFG->forceloginforprofileimage) && (!isloggedin() || isguestuser()))) {
+ // Protect images if login required and not logged in;
+ // also if login is required for profile images and is not logged in or guest
+ // do not use require_login() because it is expensive and not suitable here anyway.
+ return $defaulturl;
+ }
+
+ // First try to detect deleted users - but do not read from database for performance reasons!
+ if (!empty($this->user->deleted) or strpos($this->user->email, '@') === false) {
+ // All deleted users should have email replaced by md5 hash,
+ // all active users are expected to have valid email.
+ return $defaulturl;
+ }
+
+ // Did the user upload a picture?
+ if ($this->user->picture > 0) {
+ if (!empty($this->user->contextid)) {
+ $contextid = $this->user->contextid;
+ } else {
+ $context = context_user::instance($this->user->id, IGNORE_MISSING);
+ if (!$context) {
+ // This must be an incorrectly deleted user, all other users have context.
+ return $defaulturl;
+ }
+ $contextid = $context->id;
+ }
+
+ $path = '/';
+ if (clean_param($page->theme->name, PARAM_THEME) == $page->theme->name) {
+ // We append the theme name to the file path if we have it so that
+ // in the circumstance that the profile picture is not available
+ // when the user actually requests it they still get the profile
+ // picture for the correct theme.
+ $path .= $page->theme->name.'/';
+ }
+ // Set the image URL to the URL for the uploaded file and return.
+ $url = moodle_url::make_pluginfile_url(
+ $contextid, 'user', 'icon', null, $path, $filename, false, $this->includetoken);
+ $url->param('rev', $this->user->picture);
+ return $url;
+ }
+
+ if ($this->user->picture == 0 and !empty($CFG->enablegravatar)) {
+ // Normalise the size variable to acceptable bounds
+ if ($size < 1 || $size > 512) {
+ $size = 35;
+ }
+ // Hash the users email address
+ $md5 = md5(strtolower(trim($this->user->email)));
+ // Build a gravatar URL with what we know.
+
+ // Find the best default image URL we can (MDL-35669)
+ if (empty($CFG->gravatardefaulturl)) {
+ $absoluteimagepath = $page->theme->resolve_image_location('u/'.$filename, 'core');
+ if (strpos($absoluteimagepath, $CFG->dirroot) === 0) {
+ $gravatardefault = $CFG->wwwroot . substr($absoluteimagepath, strlen($CFG->dirroot));
+ } else {
+ $gravatardefault = $CFG->wwwroot . '/pix/u/' . $filename . '.png';
+ }
+ } else {
+ $gravatardefault = $CFG->gravatardefaulturl;
+ }
+
+ // If the currently requested page is https then we'll return an
+ // https gravatar page.
+ if (is_https()) {
+ return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
+ } else {
+ return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
+ }
+ }
+
+ return $defaulturl;
+ }
+}
diff --git a/lib/classes/output/xhtml_container_stack.php b/lib/classes/output/xhtml_container_stack.php
new file mode 100644
index 00000000000..193b90ab36b
--- /dev/null
+++ b/lib/classes/output/xhtml_container_stack.php
@@ -0,0 +1,153 @@
+.
+
+/**
+ * This class keeps track of which HTML tags are currently open.
+ *
+ * This makes it much easier to always generate well formed XHTML output, even
+ * if execution terminates abruptly. Any time you output some opening HTML
+ * without the matching closing HTML, you should push the necessary close tags
+ * onto the stack.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class xhtml_container_stack {
+ /**
+ * @var array Stores the list of open containers.
+ */
+ protected $opencontainers = array();
+
+ /**
+ * @var array In developer debug mode, stores a stack trace of all opens and
+ * closes, so we can output helpful error messages when there is a mismatch.
+ */
+ protected $log = array();
+
+ /**
+ * @var boolean Store whether we are developer debug mode. We need this in
+ * several places including in the destructor where we may not have access to $CFG.
+ */
+ protected $isdebugging;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ global $CFG;
+ $this->isdebugging = $CFG->debugdeveloper;
+ }
+
+ /**
+ * Push the close HTML for a recently opened container onto the stack.
+ *
+ * @param string $type The type of container. This is checked when {@link pop()}
+ * is called and must match, otherwise a developer debug warning is output.
+ * @param string $closehtml The HTML required to close the container.
+ */
+ public function push($type, $closehtml) {
+ $container = new stdClass;
+ $container->type = $type;
+ $container->closehtml = $closehtml;
+ if ($this->isdebugging) {
+ $this->log('Open', $type);
+ }
+ array_push($this->opencontainers, $container);
+ }
+
+ /**
+ * Pop the HTML for the next closing container from the stack. The $type
+ * must match the type passed when the container was opened, otherwise a
+ * warning will be output.
+ *
+ * @param string $type The type of container.
+ * @return ?string the HTML required to close the container.
+ */
+ public function pop($type) {
+ if (empty($this->opencontainers)) {
+ debugging('There are no more open containers. This suggests there is a nesting problem.
' .
+ $this->output_log(), DEBUG_DEVELOPER);
+ return;
+ }
+
+ $container = array_pop($this->opencontainers);
+ if ($container->type != $type) {
+ debugging('The type of container to be closed (' . $container->type .
+ ') does not match the type of the next open container (' . $type .
+ '). This suggests there is a nesting problem.
' .
+ $this->output_log(), DEBUG_DEVELOPER);
+ }
+ if ($this->isdebugging) {
+ $this->log('Close', $type);
+ }
+ return $container->closehtml;
+ }
+
+ /**
+ * Close all but the last open container. This is useful in places like error
+ * handling, where you want to close all the open containers (apart from )
+ * before outputting the error message.
+ *
+ * @param bool $shouldbenone assert that the stack should be empty now - causes a
+ * developer debug warning if it isn't.
+ * @return string the HTML required to close any open containers inside .
+ */
+ public function pop_all_but_last($shouldbenone = false) {
+ if ($shouldbenone && count($this->opencontainers) != 1) {
+ debugging('Some HTML tags were opened in the body of the page but not closed.
' .
+ $this->output_log(), DEBUG_DEVELOPER);
+ }
+ $output = '';
+ while (count($this->opencontainers) > 1) {
+ $container = array_pop($this->opencontainers);
+ $output .= $container->closehtml;
+ }
+ return $output;
+ }
+
+ /**
+ * You can call this function if you want to throw away an instance of this
+ * class without properly emptying the stack (for example, in a unit test).
+ * Calling this method stops the destruct method from outputting a developer
+ * debug warning. After calling this method, the instance can no longer be used.
+ */
+ public function discard() {
+ $this->opencontainers = null;
+ }
+
+ /**
+ * Adds an entry to the log.
+ *
+ * @param string $action The name of the action
+ * @param string $type The type of action
+ */
+ protected function log($action, $type) {
+ $this->log[] = '' . $action . ' ' . $type . ' at:' .
+ format_backtrace(debug_backtrace()) . '';
+ }
+
+ /**
+ * Outputs the log's contents as a HTML list.
+ *
+ * @return string HTML list of the log
+ */
+ protected function output_log() {
+ return '' . implode("\n", $this->log) . '
';
+ }
+}
diff --git a/lib/db/legacyclasses.php b/lib/db/legacyclasses.php
index 75076d95c32..696ba07dfa0 100644
--- a/lib/db/legacyclasses.php
+++ b/lib/db/legacyclasses.php
@@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die();
// The old class name is the key, the path to the file containing the class is the vlaue.
// The array must be called $legacyclasses.
$legacyclasses = [
+ // Exception API.
\bootstrap_renderer::class => 'output/bootstrap_renderer.php',
\coding_exception::class => 'exception/coding_exception.php',
\file_serving_exception::class => 'exception/file_serving_exception.php',
@@ -42,4 +43,86 @@ $legacyclasses = [
\require_login_session_timeout_exception::class => 'exception/require_login_session_timeout_exception.php',
\required_capability_exception::class => 'exception/required_capability_exception.php',
\webservice_parameter_exception::class => 'exception/webservice_parameter_exception.php',
+
+ // Output API.
+ \theme_config::class => 'output/theme_config.php',
+ \xhtml_container_stack::class => 'output/xhtml_container_stack.php',
+
+ \renderable::class => 'output/renderable.php',
+ \templatable::class => 'output/templatable.php',
+
+ // Output API: Renderer Factories.
+ \renderer_factory::class => 'output/renderer_factory/renderer_factory_interface.php',
+ \renderer_factory_base::class => 'output/renderer_factory/renderer_factory_base.php',
+ \standard_renderer_factory::class => 'output/renderer_factory/standard_renderer_factory.php',
+ \theme_overridden_renderer_factory::class => 'output/renderer_factory/theme_overridden_renderer_factory.php',
+
+ // Output API: Renderers.
+ \renderer_base::class => 'output/renderer_base.php',
+ \plugin_renderer_base::class => 'output/plugin_renderer_base.php',
+ \core_renderer::class => 'output/core_renderer.php',
+ \core_renderer_cli::class => 'output/core_renderer_cli.php',
+ \core_renderer_ajax::class => 'output/core_renderer_ajax.php',
+ \core_renderer_maintenance::class => 'output/core_renderer_maintenance.php',
+ \page_requirements_manager::class => 'output/requirements/page_requirements_manager.php',
+ \YUI_config::class => 'output/requirements/yui.php',
+ \fragment_requirements_manager::class => 'output/requirements/fragment_requirements_manager.php',
+
+ // Output API: components.
+ \file_picker::class => 'output/file_picker.php',
+ \user_picture::class => 'output/user_picture.php',
+ \help_icon::class => 'output/help_icon.php',
+ \pix_icon_font::class => 'output/pix_icon_font.php',
+ \pix_icon_fontawesome::class => 'output/pix_icon_fontawesome.php',
+ \pix_icon::class => 'output/pix_icon.php',
+ \image_icon::class => 'output/image_icon.php',
+ \pix_emoticon::class => 'output/pix_emoticon.php',
+ \single_button::class => 'output/single_button.php',
+ \single_select::class => 'output/single_select.php',
+ \url_select::class => 'output/url_select.php',
+ \action_link::class => 'output/action_link.php',
+ \html_writer::class => 'output/html_writer.php',
+ \js_writer::class => 'output/js_writer.php',
+ \paging_bar::class => 'output/paging_bar.php',
+ \initials_bar::class => 'output/initials_bar.php',
+ \custom_menu_item::class => 'output/custom_menu_item.php',
+ \custom_menu::class => 'output/custom_menu.php',
+ \tabobject::class => 'output/tabobject.php',
+ \context_header::class => 'output/context_header.php',
+ \tabtree::class => 'output/tabtree.php',
+ \action_menu::class => 'output/action_menu.php',
+ \action_menu_filler::class => 'output/action_menu/filler.php',
+ \action_menu_link::class => 'output/action_menu/link.php',
+ \action_menu_link_primary::class => 'output/action_menu/link_primary.php',
+ \action_menu_link_secondary::class => 'output/action_menu/link_secondary.php',
+ \preferences_groups::class => 'output/preferences_groups.php',
+ \preferences_group::class => 'output/preferences_group.php',
+ \progress_bar::class => 'output/progress_bar.php',
+ \component_action::class => 'output/actions/component_action.php',
+ \confirm_action::class => 'output/actions/confirm_action.php',
+ \popup_action::class => 'output/actions/popup_action.php',
+
+ // Block Subsystem.
+ \block_contents::class => [
+ 'core_block',
+ 'output/block_contents.php',
+ ],
+ \block_move_target::class => [
+ 'core_block',
+ 'output/block_move_target.php',
+ ],
+
+ // Table Subsystem.
+ \html_table::class => [
+ 'core_table',
+ 'output/html_table.php',
+ ],
+ \html_table_row::class => [
+ 'core_table',
+ 'output/html_table_row.php',
+ ],
+ \html_table_cell::class => [
+ 'core_table',
+ 'output/html_table_cell.php',
+ ],
];
diff --git a/lib/form/button.php b/lib/form/button.php
index fdbb4ffe1ea..b5a44aab17d 100644
--- a/lib/form/button.php
+++ b/lib/form/button.php
@@ -26,7 +26,6 @@
*/
require_once("HTML/QuickForm/button.php");
-require_once(__DIR__ . '/../outputcomponents.php');
require_once('templatable_form_element.php');
/**
diff --git a/lib/form/templatable_form_element.php b/lib/form/templatable_form_element.php
index 07fb98e2d7f..e94e5baf5bc 100644
--- a/lib/form/templatable_form_element.php
+++ b/lib/form/templatable_form_element.php
@@ -21,18 +21,6 @@
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
-// Some form elements are used before $CFG is created - do not rely on it here.
-require_once(__DIR__ . '/../outputcomponents.php');
-
-/**
- * templatable_form_element trait.
- *
- * @package core_form
- * @copyright 2016 Damyon Wiese
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
trait templatable_form_element {
/**
diff --git a/lib/outputactions.php b/lib/outputactions.php
index 04bf469af06..0fdca282e68 100644
--- a/lib/outputactions.php
+++ b/lib/outputactions.php
@@ -14,207 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-/**
- * Classes representing JS event handlers, used by output components.
- *
- * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
- * for an overview.
- *
- * @package core
- * @category output
- * @copyright 2009 Nicolas Connault
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Helper class used by other components that involve an action on the page (URL or JS).
- *
- * @copyright 2009 Nicolas Connault
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class component_action implements templatable {
-
- /**
- * @var string $event The DOM event that will trigger this action when caught
- */
- public $event;
-
- /**
- * @var string A function name to call when the button is clicked
- * The JS function you create must have two arguments:
- * 1. The event object
- * 2. An object/array of arguments ($jsfunctionargs)
- */
- public $jsfunction = false;
-
- /**
- * @var array An array of arguments to pass to the JS function
- */
- public $jsfunctionargs = array();
-
- /**
- * Constructor
- * @param string $event DOM event
- * @param string $jsfunction An optional JS function. Required if jsfunctionargs is given
- * @param array $jsfunctionargs An array of arguments to pass to the jsfunction
- */
- public function __construct($event, $jsfunction, $jsfunctionargs=array()) {
- $this->event = $event;
-
- $this->jsfunction = $jsfunction;
- $this->jsfunctionargs = $jsfunctionargs;
-
- if (!empty($this->jsfunctionargs)) {
- if (empty($this->jsfunction)) {
- throw new coding_exception('The component_action object needs a jsfunction value to pass the jsfunctionargs to.');
- }
- }
- }
-
- /**
- * Export for template.
- *
- * @param renderer_base $output The renderer.
- * @return stdClass
- */
- public function export_for_template(renderer_base $output) {
- $args = !empty($this->jsfunctionargs) ? json_encode($this->jsfunctionargs) : false;
- return (object) [
- 'event' => $this->event,
- 'jsfunction' => $this->jsfunction,
- 'jsfunctionargs' => $args,
- ];
- }
-}
-
-
-/**
- * Confirm action
- *
- * @copyright 2009 Nicolas Connault
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class confirm_action extends component_action {
- /**
- * Constructs the confirm action object
- *
- * @param string $message The message to display to the user when they are shown
- * the confirm dialogue.
- * @param string $callback Deprecated since 2.7
- * @param string $continuelabel The string to use for he continue button
- * @param string $cancellabel The string to use for the cancel button
- */
- public function __construct($message, $callback = null, $continuelabel = null, $cancellabel = null) {
- if ($callback !== null) {
- debugging('The callback argument to new confirm_action() has been deprecated.' .
- ' If you need to use a callback, please write Javascript to use moodle-core-notification-confirmation ' .
- 'and attach to the provided events.',
- DEBUG_DEVELOPER);
- }
- parent::__construct('click', 'M.util.show_confirm_dialog', array(
- 'message' => $message,
- 'continuelabel' => $continuelabel, 'cancellabel' => $cancellabel));
- }
-}
-
-
-/**
- * Component action for a popup window.
- *
- * @copyright 2009 Nicolas Connault
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class popup_action extends component_action {
-
- /**
- * @var string The JS function to call for the popup
- */
- public $jsfunction = 'openpopup';
-
- /**
- * @var array An array of parameters that will be passed to the openpopup JS function
- */
- public $params = array(
- 'height' => 400,
- 'width' => 500,
- 'top' => 0,
- 'left' => 0,
- 'menubar' => false,
- 'location' => false,
- 'scrollbars' => true,
- 'resizable' => true,
- 'toolbar' => true,
- 'status' => true,
- 'directories' => false,
- 'fullscreen' => false,
- 'dependent' => true);
-
- /**
- * Constructor
- *
- * @param string $event DOM event
- * @param moodle_url|string $url A moodle_url object, required if no jsfunction is given
- * @param string $name The JS function to call for the popup (default 'popup')
- * @param array $params An array of popup parameters
- */
- public function __construct($event, $url, $name='popup', $params=array()) {
- global $CFG;
-
- $url = new moodle_url($url);
-
- if ($name) {
- $_name = $name;
- if (($_name = preg_replace("/\s/", '_', $_name)) != $name) {
- throw new coding_exception('The $name of a popup window shouldn\'t contain spaces - string modified. '. $name .' changed to '. $_name);
- $name = $_name;
- }
- } else {
- $name = 'popup';
- }
-
- foreach ($this->params as $var => $val) {
- if (array_key_exists($var, $params)) {
- $this->params[$var] = $params[$var];
- }
- }
-
- $attributes = array('url' => $url->out(false), 'name' => $name, 'options' => $this->get_js_options($params));
- if (!empty($params['fullscreen'])) {
- $attributes['fullscreen'] = 1;
- }
- parent::__construct($event, $this->jsfunction, $attributes);
- }
-
- /**
- * Returns a string of concatenated option->value pairs used by JS to call the popup window,
- * based on this object's variables
- *
- * @return string String of option->value pairs for JS popup function.
- */
- public function get_js_options() {
- $jsoptions = '';
-
- foreach ($this->params as $var => $val) {
- if (is_string($val) || is_int($val)) {
- $jsoptions .= "$var=$val,";
- } elseif (is_bool($val)) {
- $jsoptions .= ($val) ? "$var," : "$var=0,";
- }
- }
-
- $jsoptions = substr($jsoptions, 0, strlen($jsoptions) - 1);
-
- return $jsoptions;
- }
-}
+// This file is deprecated, but it should never have been manually included by anything outside of lib/outputlib.php.
+// Throwing an exception here should be fine because removing the manual inclusion should have no impact.
+throw new \core\exception\coding_exception(
+ 'This file should not be manually included by any component.',
+);
diff --git a/lib/outputcomponents.php b/lib/outputcomponents.php
index 99d22f8e21c..0fdca282e68 100644
--- a/lib/outputcomponents.php
+++ b/lib/outputcomponents.php
@@ -14,5353 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-/**
- * Classes representing HTML elements, used by $OUTPUT methods
- *
- * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
- * for an overview.
- *
- * @package core
- * @category output
- * @copyright 2009 Tim Hunt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-use core\output\local\action_menu\subpanel;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Interface marking other classes as suitable for renderer_base::render()
- *
- * @copyright 2010 Petr Skoda (skodak) info@skodak.org
- * @package core
- * @category output
- */
-interface renderable {
- // intentionally empty
-}
-
-/**
- * Interface marking other classes having the ability to export their data for use by templates.
- *
- * @copyright 2015 Damyon Wiese
- * @package core
- * @category output
- * @since 2.9
- */
-interface templatable {
-
- /**
- * Function to export the renderer data in a format that is suitable for a
- * mustache template. This means:
- * 1. No complex types - only stdClass, array, int, string, float, bool
- * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks).
- *
- * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
- * @return stdClass|array
- */
- public function export_for_template(renderer_base $output);
-}
-
-/**
- * Data structure representing a file picker.
- *
- * @copyright 2010 Dongsheng Cai
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class file_picker implements renderable {
-
- /**
- * @var stdClass An object containing options for the file picker
- */
- public $options;
-
- /**
- * Constructs a file picker object.
- *
- * The following are possible options for the filepicker:
- * - accepted_types (*)
- * - return_types (FILE_INTERNAL)
- * - env (filepicker)
- * - client_id (uniqid)
- * - itemid (0)
- * - maxbytes (-1)
- * - maxfiles (1)
- * - buttonname (false)
- *
- * @param stdClass $options An object containing options for the file picker.
- */
- public function __construct(stdClass $options) {
- global $CFG, $USER, $PAGE;
- require_once($CFG->dirroot. '/repository/lib.php');
- $defaults = array(
- 'accepted_types'=>'*',
- 'return_types'=>FILE_INTERNAL,
- 'env' => 'filepicker',
- 'client_id' => uniqid(),
- 'itemid' => 0,
- 'maxbytes'=>-1,
- 'maxfiles'=>1,
- 'buttonname'=>false
- );
- foreach ($defaults as $key=>$value) {
- if (empty($options->$key)) {
- $options->$key = $value;
- }
- }
-
- $options->currentfile = '';
- if (!empty($options->itemid)) {
- $fs = get_file_storage();
- $usercontext = context_user::instance($USER->id);
- if (empty($options->filename)) {
- if ($files = $fs->get_area_files($usercontext->id, 'user', 'draft', $options->itemid, 'id DESC', false)) {
- $file = reset($files);
- }
- } else {
- $file = $fs->get_file($usercontext->id, 'user', 'draft', $options->itemid, $options->filepath, $options->filename);
- }
- if (!empty($file)) {
- $options->currentfile = html_writer::link(moodle_url::make_draftfile_url($file->get_itemid(), $file->get_filepath(), $file->get_filename()), $file->get_filename());
- }
- }
-
- // initilise options, getting files in root path
- $this->options = initialise_filepicker($options);
-
- // copying other options
- foreach ($options as $name=>$value) {
- if (!isset($this->options->$name)) {
- $this->options->$name = $value;
- }
- }
- }
-}
-
-/**
- * Data structure representing a user picture.
- *
- * @copyright 2009 Nicolas Connault, 2010 Petr Skoda
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Modle 2.0
- * @package core
- * @category output
- */
-class user_picture implements renderable {
- /**
- * @var stdClass A user object with at least fields all columns specified
- * in $fields array constant set.
- */
- public $user;
-
- /**
- * @var int The course id. Used when constructing the link to the user's
- * profile, page course id used if not specified.
- */
- public $courseid;
-
- /**
- * @var bool Add course profile link to image
- */
- public $link = true;
-
- /**
- * @var int Size in pixels. Special values are (true/1 = 100px) and (false/0 = 35px) for backward compatibility.
- * Recommended values (supporting user initials too): 16, 35, 64 and 100.
- */
- public $size = 35;
-
- /**
- * @var bool Add non-blank alt-text to the image.
- * Default true, set to false when image alt just duplicates text in screenreaders.
- */
- public $alttext = true;
-
- /**
- * @var bool Whether or not to open the link in a popup window.
- */
- public $popup = false;
-
- /**
- * @var string Image class attribute
- */
- public $class = 'userpicture';
-
- /**
- * @var bool Whether to be visible to screen readers.
- */
- public $visibletoscreenreaders = true;
-
- /**
- * @var bool Whether to include the fullname in the user picture link.
- */
- public $includefullname = false;
-
- /**
- * @var mixed Include user authentication token. True indicates to generate a token for current user, and integer value
- * indicates to generate a token for the user whose id is the value indicated.
- */
- public $includetoken = false;
-
- /**
- * User picture constructor.
- *
- * @param stdClass $user user record with at least id, picture, imagealt, firstname and lastname set.
- * It is recommended to add also contextid of the user for performance reasons.
- */
- public function __construct(stdClass $user) {
- global $DB;
-
- if (empty($user->id)) {
- throw new coding_exception('User id is required when printing user avatar image.');
- }
-
- // only touch the DB if we are missing data and complain loudly...
- $needrec = false;
- foreach (\core_user\fields::get_picture_fields() as $field) {
- if (!property_exists($user, $field)) {
- $needrec = true;
- debugging('Missing '.$field.' property in $user object, this is a performance problem that needs to be fixed by a developer. '
- .'Please use the \core_user\fields API to get the full list of required fields.', DEBUG_DEVELOPER);
- break;
- }
- }
-
- if ($needrec) {
- $this->user = $DB->get_record('user', array('id' => $user->id),
- implode(',', \core_user\fields::get_picture_fields()), MUST_EXIST);
- } else {
- $this->user = clone($user);
- }
- }
-
- /**
- * Returns a list of required user fields, useful when fetching required user info from db.
- *
- * In some cases we have to fetch the user data together with some other information,
- * the idalias is useful there because the id would otherwise override the main
- * id of the result record. Please note it has to be converted back to id before rendering.
- *
- * @param string $tableprefix name of database table prefix in query
- * @param array $extrafields extra fields to be included in result (do not include TEXT columns because it would break SELECT DISTINCT in MSSQL and ORACLE)
- * @param string $idalias alias of id field
- * @param string $fieldprefix prefix to add to all columns in their aliases, does not apply to 'id'
- * @return string
- * @deprecated since Moodle 3.11 MDL-45242
- * @see \core_user\fields
- */
- public static function fields($tableprefix = '', array $extrafields = NULL, $idalias = 'id', $fieldprefix = '') {
- debugging('user_picture::fields() is deprecated. Please use the \core_user\fields API instead.', DEBUG_DEVELOPER);
- $userfields = \core_user\fields::for_userpic();
- if ($extrafields) {
- $userfields->including(...$extrafields);
- }
- $selects = $userfields->get_sql($tableprefix, false, $fieldprefix, $idalias, false)->selects;
- if ($tableprefix === '') {
- // If no table alias is specified, don't add {user}. in front of fields.
- $selects = str_replace('{user}.', '', $selects);
- }
- // Maintain legacy behaviour where the field list was done with 'implode' and no spaces.
- $selects = str_replace(', ', ',', $selects);
- return $selects;
- }
-
- /**
- * Extract the aliased user fields from a given record
- *
- * Given a record that was previously obtained using {@link self::fields()} with aliases,
- * this method extracts user related unaliased fields.
- *
- * @param stdClass $record containing user picture fields
- * @param array $extrafields extra fields included in the $record
- * @param string $idalias alias of the id field
- * @param string $fieldprefix prefix added to all columns in their aliases, does not apply to 'id'
- * @return stdClass object with unaliased user fields
- */
- public static function unalias(stdClass $record, array $extrafields = null, $idalias = 'id', $fieldprefix = '') {
-
- if (empty($idalias)) {
- $idalias = 'id';
- }
-
- $return = new stdClass();
-
- foreach (\core_user\fields::get_picture_fields() as $field) {
- if ($field === 'id') {
- if (property_exists($record, $idalias)) {
- $return->id = $record->{$idalias};
- }
- } else {
- if (property_exists($record, $fieldprefix.$field)) {
- $return->{$field} = $record->{$fieldprefix.$field};
- }
- }
- }
- // add extra fields if not already there
- if ($extrafields) {
- foreach ($extrafields as $e) {
- if ($e === 'id' or property_exists($return, $e)) {
- continue;
- }
- $return->{$e} = $record->{$fieldprefix.$e};
- }
- }
-
- return $return;
- }
-
- /**
- * Works out the URL for the users picture.
- *
- * This method is recommended as it avoids costly redirects of user pictures
- * if requests are made for non-existent files etc.
- *
- * @param moodle_page $page
- * @param renderer_base $renderer
- * @return moodle_url
- */
- public function get_url(moodle_page $page, renderer_base $renderer = null) {
- global $CFG;
-
- if (is_null($renderer)) {
- $renderer = $page->get_renderer('core');
- }
-
- // Sort out the filename and size. Size is only required for the gravatar
- // implementation presently.
- if (empty($this->size)) {
- $filename = 'f2';
- $size = 35;
- } else if ($this->size === true or $this->size == 1) {
- $filename = 'f1';
- $size = 100;
- } else if ($this->size > 100) {
- $filename = 'f3';
- $size = (int)$this->size;
- } else if ($this->size >= 50) {
- $filename = 'f1';
- $size = (int)$this->size;
- } else {
- $filename = 'f2';
- $size = (int)$this->size;
- }
-
- $defaulturl = $renderer->image_url('u/'.$filename); // default image
-
- if ((!empty($CFG->forcelogin) and !isloggedin()) ||
- (!empty($CFG->forceloginforprofileimage) && (!isloggedin() || isguestuser()))) {
- // Protect images if login required and not logged in;
- // also if login is required for profile images and is not logged in or guest
- // do not use require_login() because it is expensive and not suitable here anyway.
- return $defaulturl;
- }
-
- // First try to detect deleted users - but do not read from database for performance reasons!
- if (!empty($this->user->deleted) or strpos($this->user->email, '@') === false) {
- // All deleted users should have email replaced by md5 hash,
- // all active users are expected to have valid email.
- return $defaulturl;
- }
-
- // Did the user upload a picture?
- if ($this->user->picture > 0) {
- if (!empty($this->user->contextid)) {
- $contextid = $this->user->contextid;
- } else {
- $context = context_user::instance($this->user->id, IGNORE_MISSING);
- if (!$context) {
- // This must be an incorrectly deleted user, all other users have context.
- return $defaulturl;
- }
- $contextid = $context->id;
- }
-
- $path = '/';
- if (clean_param($page->theme->name, PARAM_THEME) == $page->theme->name) {
- // We append the theme name to the file path if we have it so that
- // in the circumstance that the profile picture is not available
- // when the user actually requests it they still get the profile
- // picture for the correct theme.
- $path .= $page->theme->name.'/';
- }
- // Set the image URL to the URL for the uploaded file and return.
- $url = moodle_url::make_pluginfile_url(
- $contextid, 'user', 'icon', null, $path, $filename, false, $this->includetoken);
- $url->param('rev', $this->user->picture);
- return $url;
- }
-
- if ($this->user->picture == 0 and !empty($CFG->enablegravatar)) {
- // Normalise the size variable to acceptable bounds
- if ($size < 1 || $size > 512) {
- $size = 35;
- }
- // Hash the users email address
- $md5 = md5(strtolower(trim($this->user->email)));
- // Build a gravatar URL with what we know.
-
- // Find the best default image URL we can (MDL-35669)
- if (empty($CFG->gravatardefaulturl)) {
- $absoluteimagepath = $page->theme->resolve_image_location('u/'.$filename, 'core');
- if (strpos($absoluteimagepath, $CFG->dirroot) === 0) {
- $gravatardefault = $CFG->wwwroot . substr($absoluteimagepath, strlen($CFG->dirroot));
- } else {
- $gravatardefault = $CFG->wwwroot . '/pix/u/' . $filename . '.png';
- }
- } else {
- $gravatardefault = $CFG->gravatardefaulturl;
- }
-
- // If the currently requested page is https then we'll return an
- // https gravatar page.
- if (is_https()) {
- return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
- } else {
- return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
- }
- }
-
- return $defaulturl;
- }
-}
-
-/**
- * Data structure representing a help icon.
- *
- * @copyright 2010 Petr Skoda (info@skodak.org)
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class help_icon implements renderable, templatable {
-
- /**
- * @var string lang pack identifier (without the "_help" suffix),
- * both get_string($identifier, $component) and get_string($identifier.'_help', $component)
- * must exist.
- */
- public $identifier;
-
- /**
- * @var string Component name, the same as in get_string()
- */
- public $component;
-
- /**
- * @var string Extra descriptive text next to the icon
- */
- public $linktext = null;
-
- /**
- * @var mixed An object, string or number that can be used within translation strings
- */
- public $a = null;
-
- /**
- * Constructor
- *
- * @param string $identifier string for help page title,
- * string with _help suffix is used for the actual help text.
- * string with _link suffix is used to create a link to further info (if it exists)
- * @param string $component
- * @param string|object|array|int $a An object, string or number that can be used
- * within translation strings
- */
- public function __construct($identifier, $component, $a = null) {
- $this->identifier = $identifier;
- $this->component = $component;
- $this->a = $a;
- }
-
- /**
- * Verifies that both help strings exists, shows debug warnings if not
- */
- public function diag_strings() {
- $sm = get_string_manager();
- if (!$sm->string_exists($this->identifier, $this->component)) {
- debugging("Help title string does not exist: [$this->identifier, $this->component]");
- }
- if (!$sm->string_exists($this->identifier.'_help', $this->component)) {
- debugging("Help contents string does not exist: [{$this->identifier}_help, $this->component]");
- }
- }
-
- /**
- * Export this data so it can be used as the context for a mustache template.
- *
- * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
- * @return stdClass
- */
- public function export_for_template(renderer_base $output) {
- global $CFG;
-
- $title = get_string($this->identifier, $this->component, $this->a);
-
- if (empty($this->linktext)) {
- $alt = get_string('helpprefix2', '', trim($title, ". \t"));
- } else {
- $alt = get_string('helpwiththis');
- }
-
- $data = get_formatted_help_string($this->identifier, $this->component, false, $this->a);
-
- $data->alt = $alt;
- $data->icon = (new pix_icon('help', $alt, 'core', ['class' => 'iconhelp']))->export_for_template($output);
- $data->linktext = $this->linktext;
- $data->title = get_string('helpprefix2', '', trim($title, ". \t"));
-
- $options = [
- 'component' => $this->component,
- 'identifier' => $this->identifier,
- 'lang' => current_language()
- ];
-
- // Debugging feature lets you display string identifier and component.
- if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
- $options['strings'] = 1;
- }
-
- $data->url = (new moodle_url('/help.php', $options))->out(false);
- $data->ltr = !right_to_left();
- return $data;
- }
-}
-
-
-/**
- * Data structure representing an icon font.
- *
- * @copyright 2016 Damyon Wiese
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @package core
- * @category output
- */
-class pix_icon_font implements templatable {
-
- /**
- * @var pix_icon $pixicon The original icon.
- */
- private $pixicon = null;
-
- /**
- * @var string $key The mapped key.
- */
- private $key;
-
- /**
- * @var bool $mapped The icon could not be mapped.
- */
- private $mapped;
-
- /**
- * Constructor
- *
- * @param pix_icon $pixicon The original icon
- */
- public function __construct(pix_icon $pixicon) {
- global $PAGE;
-
- $this->pixicon = $pixicon;
- $this->mapped = false;
- $iconsystem = \core\output\icon_system::instance();
-
- $this->key = $iconsystem->remap_icon_name($pixicon->pix, $pixicon->component);
- if (!empty($this->key)) {
- $this->mapped = true;
- }
- }
-
- /**
- * Return true if this pix_icon was successfully mapped to an icon font.
- *
- * @return bool
- */
- public function is_mapped() {
- return $this->mapped;
- }
-
- /**
- * Export this data so it can be used as the context for a mustache template.
- *
- * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
- * @return array
- */
- public function export_for_template(renderer_base $output) {
-
- $pixdata = $this->pixicon->export_for_template($output);
-
- $title = isset($this->pixicon->attributes['title']) ? $this->pixicon->attributes['title'] : '';
- $alt = isset($this->pixicon->attributes['alt']) ? $this->pixicon->attributes['alt'] : '';
- if (empty($title)) {
- $title = $alt;
- }
- $data = array(
- 'extraclasses' => $pixdata['extraclasses'],
- 'title' => $title,
- 'alt' => $alt,
- 'key' => $this->key
- );
-
- return $data;
- }
-}
-
-/**
- * Data structure representing an icon subtype.
- *
- * @copyright 2016 Damyon Wiese
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @package core
- * @category output
- */
-class pix_icon_fontawesome extends pix_icon_font {
-
-}
-
-/**
- * Data structure representing an icon.
- *
- * @copyright 2010 Petr Skoda
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class pix_icon implements renderable, templatable {
-
- /**
- * @var string The icon name
- */
- var $pix;
-
- /**
- * @var string The component the icon belongs to.
- */
- var $component;
-
- /**
- * @var array An array of attributes to use on the icon
- */
- var $attributes = array();
-
- /**
- * Constructor
- *
- * @param string $pix short icon name
- * @param string $alt The alt text to use for the icon
- * @param string $component component name
- * @param array $attributes html attributes
- */
- public function __construct($pix, $alt, $component='moodle', array $attributes = null) {
- global $PAGE;
-
- $this->pix = $pix;
- $this->component = $component;
- $this->attributes = (array)$attributes;
-
- if (empty($this->attributes['class'])) {
- $this->attributes['class'] = '';
- }
-
- // Set an additional class for big icons so that they can be styled properly.
- if (substr($pix, 0, 2) === 'b/') {
- $this->attributes['class'] .= ' iconsize-big';
- }
-
- // If the alt is empty, don't place it in the attributes, otherwise it will override parent alt text.
- if (!is_null($alt)) {
- $this->attributes['alt'] = $alt;
-
- // If there is no title, set it to the attribute.
- if (!isset($this->attributes['title'])) {
- $this->attributes['title'] = $this->attributes['alt'];
- }
- } else {
- unset($this->attributes['alt']);
- }
-
- if (empty($this->attributes['title'])) {
- // Remove the title attribute if empty, we probably want to use the parent node's title
- // and some browsers might overwrite it with an empty title.
- unset($this->attributes['title']);
- }
-
- // Hide icons from screen readers that have no alt.
- if (empty($this->attributes['alt'])) {
- $this->attributes['aria-hidden'] = 'true';
- }
- }
-
- /**
- * Export this data so it can be used as the context for a mustache template.
- *
- * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
- * @return array
- */
- public function export_for_template(renderer_base $output) {
- $attributes = $this->attributes;
- $extraclasses = '';
-
- foreach ($attributes as $key => $item) {
- if ($key == 'class') {
- $extraclasses = $item;
- unset($attributes[$key]);
- break;
- }
- }
-
- $attributes['src'] = $output->image_url($this->pix, $this->component)->out(false);
- $templatecontext = array();
- foreach ($attributes as $name => $value) {
- $templatecontext[] = array('name' => $name, 'value' => $value);
- }
- $title = isset($attributes['title']) ? $attributes['title'] : '';
- if (empty($title)) {
- $title = isset($attributes['alt']) ? $attributes['alt'] : '';
- }
- $data = array(
- 'attributes' => $templatecontext,
- 'extraclasses' => $extraclasses
- );
-
- return $data;
- }
-
- /**
- * Much simpler version of export that will produce the data required to render this pix with the
- * pix helper in a mustache tag.
- *
- * @return array
- */
- public function export_for_pix() {
- $title = isset($this->attributes['title']) ? $this->attributes['title'] : '';
- if (empty($title)) {
- $title = isset($this->attributes['alt']) ? $this->attributes['alt'] : '';
- }
- return [
- 'key' => $this->pix,
- 'component' => $this->component,
- 'title' => (string) $title,
- ];
- }
-}
-
-/**
- * Data structure representing an activity icon.
- *
- * The difference is that activity icons will always render with the standard icon system (no font icons).
- *
- * @copyright 2017 Damyon Wiese
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @package core
- */
-class image_icon extends pix_icon {
-}
-
-/**
- * Data structure representing an emoticon image
- *
- * @copyright 2010 David Mudrak
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class pix_emoticon extends pix_icon implements renderable {
-
- /**
- * Constructor
- * @param string $pix short icon name
- * @param string $alt alternative text
- * @param string $component emoticon image provider
- * @param array $attributes explicit HTML attributes
- */
- public function __construct($pix, $alt, $component = 'moodle', array $attributes = array()) {
- if (empty($attributes['class'])) {
- $attributes['class'] = 'emoticon';
- }
- parent::__construct($pix, $alt, $component, $attributes);
- }
-}
-
-/**
- * Data structure representing a simple form with only one button.
- *
- * @copyright 2009 Petr Skoda
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class single_button implements renderable {
-
- /**
- * Possible button types. From boostrap.
- */
- const BUTTON_TYPES = [
- self::BUTTON_PRIMARY,
- self::BUTTON_SECONDARY,
- self::BUTTON_SUCCESS,
- self::BUTTON_DANGER,
- self::BUTTON_WARNING,
- self::BUTTON_INFO
- ];
-
- /**
- * Possible button types - Primary.
- */
- const BUTTON_PRIMARY = 'primary';
- /**
- * Possible button types - Secondary.
- */
- const BUTTON_SECONDARY = 'secondary';
- /**
- * Possible button types - Danger.
- */
- const BUTTON_DANGER = 'danger';
- /**
- * Possible button types - Success.
- */
- const BUTTON_SUCCESS = 'success';
- /**
- * Possible button types - Warning.
- */
- const BUTTON_WARNING = 'warning';
- /**
- * Possible button types - Info.
- */
- const BUTTON_INFO = 'info';
-
- /**
- * @var moodle_url Target url
- */
- public $url;
-
- /**
- * @var string Button label
- */
- public $label;
-
- /**
- * @var string Form submit method post or get
- */
- public $method = 'post';
-
- /**
- * @var string Wrapping div class
- */
- public $class = 'singlebutton';
-
- /**
- * @var string Type of button (from defined types). Used for styling.
- */
- protected $type;
-
- /**
- * @var bool True if button is primary button. Used for styling.
- * @deprecated since Moodle 4.2
- */
- private $primary = false;
-
- /**
- * @var bool True if button disabled, false if normal
- */
- public $disabled = false;
-
- /**
- * @var string Button tooltip
- */
- public $tooltip = null;
-
- /**
- * @var string Form id
- */
- public $formid;
-
- /**
- * @var array List of attached actions
- */
- public $actions = array();
-
- /**
- * @var array $params URL Params
- */
- public $params;
-
- /**
- * @var string Action id
- */
- public $actionid;
-
- /**
- * @var array
- */
- protected $attributes = [];
-
- /**
- * Constructor
- *
- * @param moodle_url $url
- * @param string $label button text
- * @param string $method get or post submit method
- * @param string $type whether this is a primary button or another type, used for styling
- * @param array $attributes Attributes for the HTML button tag
- */
- public function __construct(moodle_url $url, $label, $method = 'post', $type = self::BUTTON_SECONDARY,
- $attributes = []) {
- if (is_bool($type)) {
- debugging('The boolean $primary is deprecated and replaced by $type,
- use single_button::BUTTON_PRIMARY or self::BUTTON_SECONDARY instead');
- $type = $type ? self::BUTTON_PRIMARY : self::BUTTON_SECONDARY;
- }
- $this->url = clone($url);
- $this->label = $label;
- $this->method = $method;
- $this->type = $type;
- $this->attributes = $attributes;
- }
-
- /**
- * Shortcut for adding a JS confirm dialog when the button is clicked.
- * The message must be a yes/no question.
- *
- * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
- */
- public function add_confirm_action($confirmmessage) {
- $this->add_action(new confirm_action($confirmmessage));
- }
-
- /**
- * Add action to the button.
- * @param component_action $action
- */
- public function add_action(component_action $action) {
- $this->actions[] = $action;
- }
-
- /**
- * Sets an attribute for the HTML button tag.
- *
- * @param string $name The attribute name
- * @param mixed $value The value
- * @return null
- */
- public function set_attribute($name, $value) {
- $this->attributes[$name] = $value;
- }
-
- /**
- * Magic setter method.
- *
- * This method manages access to some properties and will display deprecation message when accessing 'primary' property.
- *
- * @param string $name
- * @param mixed $value
- */
- public function __set($name, $value) {
- switch ($name) {
- case 'primary':
- debugging('The primary field is deprecated, use the type field instead');
- // Here just in case we modified the primary field from outside {@see \mod_quiz_renderer::summary_page_controls}.
- $this->type = $value ? self::BUTTON_PRIMARY : self::BUTTON_SECONDARY;
- break;
- case 'type':
- $this->type = in_array($value, self::BUTTON_TYPES) ? $value : self::BUTTON_SECONDARY;
- break;
- default:
- $this->$name = $value;
- }
- }
-
- /**
- * Magic method getter.
- *
- * This method manages access to some properties and will display deprecation message when accessing 'primary' property.
- *
- * @param string $name
- * @return mixed
- */
- public function __get($name) {
- switch ($name) {
- case 'primary':
- debugging('The primary field is deprecated, use type field instead');
- return $this->type == self::BUTTON_PRIMARY;
- case 'type':
- return $this->type;
- default:
- return $this->$name;
- }
- }
-
- /**
- * Export data.
- *
- * @param renderer_base $output Renderer.
- * @return stdClass
- */
- public function export_for_template(renderer_base $output) {
- $url = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
-
- $data = new stdClass();
- $data->id = html_writer::random_id('single_button');
- $data->formid = $this->formid;
- $data->method = $this->method;
- $data->url = $url === '' ? '#' : $url;
- $data->label = $this->label;
- $data->classes = $this->class;
- $data->disabled = $this->disabled;
- $data->tooltip = $this->tooltip;
- $data->type = $this->type;
- $data->attributes = [];
- foreach ($this->attributes as $key => $value) {
- $data->attributes[] = ['name' => $key, 'value' => $value];
- }
-
- // Form parameters.
- $actionurl = new moodle_url($this->url);
- if ($this->method === 'post') {
- $actionurl->param('sesskey', sesskey());
- }
- $data->params = $actionurl->export_params_for_template();
-
- // Button actions.
- $actions = $this->actions;
- $data->actions = array_map(function($action) use ($output) {
- return $action->export_for_template($output);
- }, $actions);
- $data->hasactions = !empty($data->actions);
-
- return $data;
- }
-}
-
-
-/**
- * Simple form with just one select field that gets submitted automatically.
- *
- * If JS not enabled small go button is printed too.
- *
- * @copyright 2009 Petr Skoda
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class single_select implements renderable, templatable {
-
- /**
- * @var moodle_url Target url - includes hidden fields
- */
- var $url;
-
- /**
- * @var string Name of the select element.
- */
- var $name;
-
- /**
- * @var array $options associative array value=>label ex.: array(1=>'One, 2=>Two)
- * it is also possible to specify optgroup as complex label array ex.:
- * array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
- * array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
- */
- var $options;
-
- /**
- * @var string Selected option
- */
- var $selected;
-
- /**
- * @var array Nothing selected
- */
- var $nothing;
-
- /**
- * @var array Extra select field attributes
- */
- var $attributes = array();
-
- /**
- * @var string Button label
- */
- var $label = '';
-
- /**
- * @var array Button label's attributes
- */
- var $labelattributes = array();
-
- /**
- * @var string Form submit method post or get
- */
- var $method = 'get';
-
- /**
- * @var string Wrapping div class
- */
- var $class = 'singleselect';
-
- /**
- * @var bool True if button disabled, false if normal
- */
- var $disabled = false;
-
- /**
- * @var string Button tooltip
- */
- var $tooltip = null;
-
- /**
- * @var string Form id
- */
- var $formid = null;
-
- /**
- * @var help_icon The help icon for this element.
- */
- var $helpicon = null;
-
- /** @var component_action[] component action. */
- public $actions = [];
-
- /**
- * Constructor
- * @param moodle_url $url form action target, includes hidden fields
- * @param string $name name of selection field - the changing parameter in url
- * @param array $options list of options
- * @param string $selected selected element
- * @param ?array $nothing
- * @param string $formid
- */
- public function __construct(moodle_url $url, $name, array $options, $selected = '', $nothing = array('' => 'choosedots'), $formid = null) {
- $this->url = $url;
- $this->name = $name;
- $this->options = $options;
- $this->selected = $selected;
- $this->nothing = $nothing;
- $this->formid = $formid;
- }
-
- /**
- * Shortcut for adding a JS confirm dialog when the button is clicked.
- * The message must be a yes/no question.
- *
- * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
- */
- public function add_confirm_action($confirmmessage) {
- $this->add_action(new component_action('submit', 'M.util.show_confirm_dialog', array('message' => $confirmmessage)));
- }
-
- /**
- * Add action to the button.
- *
- * @param component_action $action
- */
- public function add_action(component_action $action) {
- $this->actions[] = $action;
- }
-
- /**
- * Adds help icon.
- *
- * @deprecated since Moodle 2.0
- */
- public function set_old_help_icon($helppage, $title, $component = 'moodle') {
- throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
- }
-
- /**
- * Adds help icon.
- *
- * @param string $identifier The keyword that defines a help page
- * @param string $component
- */
- public function set_help_icon($identifier, $component = 'moodle') {
- $this->helpicon = new help_icon($identifier, $component);
- }
-
- /**
- * Sets select's label
- *
- * @param string $label
- * @param array $attributes (optional)
- */
- public function set_label($label, $attributes = array()) {
- $this->label = $label;
- $this->labelattributes = $attributes;
-
- }
-
- /**
- * Export data.
- *
- * @param renderer_base $output Renderer.
- * @return stdClass
- */
- public function export_for_template(renderer_base $output) {
- $attributes = $this->attributes;
-
- $data = new stdClass();
- $data->name = $this->name;
- $data->method = $this->method;
- $data->action = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
- $data->classes = $this->class;
- $data->label = $this->label;
- $data->disabled = $this->disabled;
- $data->title = $this->tooltip;
- $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('single_select_f');
- $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('single_select');
-
- // Select element attributes.
- // Unset attributes that are already predefined in the template.
- unset($attributes['id']);
- unset($attributes['class']);
- unset($attributes['name']);
- unset($attributes['title']);
- unset($attributes['disabled']);
-
- // Map the attributes.
- $data->attributes = array_map(function($key) use ($attributes) {
- return ['name' => $key, 'value' => $attributes[$key]];
- }, array_keys($attributes));
-
- // Form parameters.
- $actionurl = new moodle_url($this->url);
- if ($this->method === 'post') {
- $actionurl->param('sesskey', sesskey());
- }
- $data->params = $actionurl->export_params_for_template();
-
- // Select options.
- $hasnothing = false;
- if (is_string($this->nothing) && $this->nothing !== '') {
- $nothing = ['' => $this->nothing];
- $hasnothing = true;
- $nothingkey = '';
- } else if (is_array($this->nothing)) {
- $nothingvalue = reset($this->nothing);
- if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
- $nothing = [key($this->nothing) => get_string('choosedots')];
- } else {
- $nothing = $this->nothing;
- }
- $hasnothing = true;
- $nothingkey = key($this->nothing);
- }
- if ($hasnothing) {
- $options = $nothing + $this->options;
- } else {
- $options = $this->options;
- }
-
- foreach ($options as $value => $name) {
- if (is_array($options[$value])) {
- foreach ($options[$value] as $optgroupname => $optgroupvalues) {
- $sublist = [];
- foreach ($optgroupvalues as $optvalue => $optname) {
- $option = [
- 'value' => $optvalue,
- 'name' => $optname,
- 'selected' => strval($this->selected) === strval($optvalue),
- ];
-
- if ($hasnothing && $nothingkey === $optvalue) {
- $option['ignore'] = 'data-ignore';
- }
-
- $sublist[] = $option;
- }
- $data->options[] = [
- 'name' => $optgroupname,
- 'optgroup' => true,
- 'options' => $sublist
- ];
- }
- } else {
- $option = [
- 'value' => $value,
- 'name' => $options[$value],
- 'selected' => strval($this->selected) === strval($value),
- 'optgroup' => false
- ];
-
- if ($hasnothing && $nothingkey === $value) {
- $option['ignore'] = 'data-ignore';
- }
-
- $data->options[] = $option;
- }
- }
-
- // Label attributes.
- $data->labelattributes = [];
- // Unset label attributes that are already in the template.
- unset($this->labelattributes['for']);
- // Map the label attributes.
- foreach ($this->labelattributes as $key => $value) {
- $data->labelattributes[] = ['name' => $key, 'value' => $value];
- }
-
- // Help icon.
- $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
-
- return $data;
- }
-}
-
-/**
- * Simple URL selection widget description.
- *
- * @copyright 2009 Petr Skoda
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class url_select implements renderable, templatable {
- /**
- * @var array $urls associative array value=>label ex.: array(1=>'One, 2=>Two)
- * it is also possible to specify optgroup as complex label array ex.:
- * array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
- * array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
- */
- var $urls;
-
- /**
- * @var string Selected option
- */
- var $selected;
-
- /**
- * @var array Nothing selected
- */
- var $nothing;
-
- /**
- * @var array Extra select field attributes
- */
- var $attributes = array();
-
- /**
- * @var string Button label
- */
- var $label = '';
-
- /**
- * @var array Button label's attributes
- */
- var $labelattributes = array();
-
- /**
- * @var string Wrapping div class
- */
- var $class = 'urlselect';
-
- /**
- * @var bool True if button disabled, false if normal
- */
- var $disabled = false;
-
- /**
- * @var string Button tooltip
- */
- var $tooltip = null;
-
- /**
- * @var string Form id
- */
- var $formid = null;
-
- /**
- * @var help_icon The help icon for this element.
- */
- var $helpicon = null;
-
- /**
- * @var string If set, makes button visible with given name for button
- */
- var $showbutton = null;
-
- /**
- * Constructor
- * @param array $urls list of options
- * @param string $selected selected element
- * @param array $nothing
- * @param string $formid
- * @param string $showbutton Set to text of button if it should be visible
- * or null if it should be hidden (hidden version always has text 'go')
- */
- public function __construct(array $urls, $selected = '', $nothing = array('' => 'choosedots'), $formid = null, $showbutton = null) {
- $this->urls = $urls;
- $this->selected = $selected;
- $this->nothing = $nothing;
- $this->formid = $formid;
- $this->showbutton = $showbutton;
- }
-
- /**
- * Adds help icon.
- *
- * @deprecated since Moodle 2.0
- */
- public function set_old_help_icon($helppage, $title, $component = 'moodle') {
- throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
- }
-
- /**
- * Adds help icon.
- *
- * @param string $identifier The keyword that defines a help page
- * @param string $component
- */
- public function set_help_icon($identifier, $component = 'moodle') {
- $this->helpicon = new help_icon($identifier, $component);
- }
-
- /**
- * Sets select's label
- *
- * @param string $label
- * @param array $attributes (optional)
- */
- public function set_label($label, $attributes = array()) {
- $this->label = $label;
- $this->labelattributes = $attributes;
- }
-
- /**
- * Clean a URL.
- *
- * @param string $value The URL.
- * @return string The cleaned URL.
- */
- protected function clean_url($value) {
- global $CFG;
-
- if (empty($value)) {
- // Nothing.
-
- } else if (strpos($value, $CFG->wwwroot . '/') === 0) {
- $value = str_replace($CFG->wwwroot, '', $value);
-
- } else if (strpos($value, '/') !== 0) {
- debugging("Invalid url_select urls parameter: url '$value' is not local relative url!", DEBUG_DEVELOPER);
- }
-
- return $value;
- }
-
- /**
- * Flatten the options for Mustache.
- *
- * This also cleans the URLs.
- *
- * @param array $options The options.
- * @param array $nothing The nothing option.
- * @return array
- */
- protected function flatten_options($options, $nothing) {
- $flattened = [];
-
- foreach ($options as $value => $option) {
- if (is_array($option)) {
- foreach ($option as $groupname => $optoptions) {
- if (!isset($flattened[$groupname])) {
- $flattened[$groupname] = [
- 'name' => $groupname,
- 'isgroup' => true,
- 'options' => []
- ];
- }
- foreach ($optoptions as $optvalue => $optoption) {
- $cleanedvalue = $this->clean_url($optvalue);
- $flattened[$groupname]['options'][$cleanedvalue] = [
- 'name' => $optoption,
- 'value' => $cleanedvalue,
- 'selected' => $this->selected == $optvalue,
- ];
- }
- }
-
- } else {
- $cleanedvalue = $this->clean_url($value);
- $flattened[$cleanedvalue] = [
- 'name' => $option,
- 'value' => $cleanedvalue,
- 'selected' => $this->selected == $value,
- ];
- }
- }
-
- if (!empty($nothing)) {
- $value = key($nothing);
- $name = reset($nothing);
- $flattened = [
- $value => ['name' => $name, 'value' => $value, 'selected' => $this->selected == $value]
- ] + $flattened;
- }
-
- // Make non-associative array.
- foreach ($flattened as $key => $value) {
- if (!empty($value['options'])) {
- $flattened[$key]['options'] = array_values($value['options']);
- }
- }
- $flattened = array_values($flattened);
-
- return $flattened;
- }
-
- /**
- * Export for template.
- *
- * @param renderer_base $output Renderer.
- * @return stdClass
- */
- public function export_for_template(renderer_base $output) {
- $attributes = $this->attributes;
-
- $data = new stdClass();
- $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('url_select_f');
- $data->classes = $this->class;
- $data->label = $this->label;
- $data->disabled = $this->disabled;
- $data->title = $this->tooltip;
- $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('url_select');
- $data->sesskey = sesskey();
- $data->action = (new moodle_url('/course/jumpto.php'))->out(false);
-
- // Remove attributes passed as property directly.
- unset($attributes['class']);
- unset($attributes['id']);
- unset($attributes['name']);
- unset($attributes['title']);
- unset($attributes['disabled']);
-
- $data->showbutton = $this->showbutton;
-
- // Select options.
- $nothing = false;
- if (is_string($this->nothing) && $this->nothing !== '') {
- $nothing = ['' => $this->nothing];
- } else if (is_array($this->nothing)) {
- $nothingvalue = reset($this->nothing);
- if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
- $nothing = [key($this->nothing) => get_string('choosedots')];
- } else {
- $nothing = $this->nothing;
- }
- }
- $data->options = $this->flatten_options($this->urls, $nothing);
-
- // Label attributes.
- $data->labelattributes = [];
- // Unset label attributes that are already in the template.
- unset($this->labelattributes['for']);
- // Map the label attributes.
- foreach ($this->labelattributes as $key => $value) {
- $data->labelattributes[] = ['name' => $key, 'value' => $value];
- }
-
- // Help icon.
- $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
-
- // Finally all the remaining attributes.
- $data->attributes = [];
- foreach ($attributes as $key => $value) {
- $data->attributes[] = ['name' => $key, 'value' => $value];
- }
-
- return $data;
- }
-}
-
-/**
- * Data structure describing html link with special action attached.
- *
- * @copyright 2010 Petr Skoda
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class action_link implements renderable {
-
- /**
- * @var moodle_url Href url
- */
- public $url;
-
- /**
- * @var string Link text HTML fragment
- */
- public $text;
-
- /**
- * @var array HTML attributes
- */
- public $attributes;
-
- /**
- * @var array List of actions attached to link
- */
- public $actions;
-
- /**
- * @var pix_icon Optional pix icon to render with the link
- */
- public $icon;
-
- /**
- * Constructor
- * @param moodle_url $url
- * @param string $text HTML fragment
- * @param component_action $action
- * @param array $attributes associative array of html link attributes + disabled
- * @param pix_icon $icon optional pix_icon to render with the link text
- */
- public function __construct(moodle_url $url,
- $text,
- component_action $action=null,
- array $attributes=null,
- pix_icon $icon=null) {
- $this->url = clone($url);
- $this->text = $text;
- if (empty($attributes['id'])) {
- $attributes['id'] = html_writer::random_id('action_link');
- }
- $this->attributes = (array)$attributes;
- if ($action) {
- $this->add_action($action);
- }
- $this->icon = $icon;
- }
-
- /**
- * Add action to the link.
- *
- * @param component_action $action
- */
- public function add_action(component_action $action) {
- $this->actions[] = $action;
- }
-
- /**
- * Adds a CSS class to this action link object
- * @param string $class
- */
- public function add_class($class) {
- if (empty($this->attributes['class'])) {
- $this->attributes['class'] = $class;
- } else {
- $this->attributes['class'] .= ' ' . $class;
- }
- }
-
- /**
- * Returns true if the specified class has been added to this link.
- * @param string $class
- * @return bool
- */
- public function has_class($class) {
- return strpos(' ' . $this->attributes['class'] . ' ', ' ' . $class . ' ') !== false;
- }
-
- /**
- * Return the rendered HTML for the icon. Useful for rendering action links in a template.
- * @return string
- */
- public function get_icon_html() {
- global $OUTPUT;
- if (!$this->icon) {
- return '';
- }
- return $OUTPUT->render($this->icon);
- }
-
- /**
- * Export for template.
- *
- * @param renderer_base $output The renderer.
- * @return stdClass
- */
- public function export_for_template(renderer_base $output) {
- $data = new stdClass();
- $attributes = $this->attributes;
-
- $data->id = $attributes['id'];
- unset($attributes['id']);
-
- $data->disabled = !empty($attributes['disabled']);
- unset($attributes['disabled']);
-
- $data->text = $this->text instanceof renderable ? $output->render($this->text) : (string) $this->text;
- $data->url = $this->url ? $this->url->out(false) : '';
- $data->icon = $this->icon ? $this->icon->export_for_pix() : null;
- $data->classes = isset($attributes['class']) ? $attributes['class'] : '';
- unset($attributes['class']);
-
- $data->attributes = array_map(function($key, $value) {
- return [
- 'name' => $key,
- 'value' => $value
- ];
- }, array_keys($attributes), $attributes);
-
- $data->actions = array_map(function($action) use ($output) {
- return $action->export_for_template($output);
- }, !empty($this->actions) ? $this->actions : []);
- $data->hasactions = !empty($this->actions);
-
- return $data;
- }
-}
-
-/**
- * Simple html output class
- *
- * @copyright 2009 Tim Hunt, 2010 Petr Skoda
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.0
- * @package core
- * @category output
- */
-class html_writer {
-
- /**
- * Outputs a tag with attributes and contents
- *
- * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
- * @param string $contents What goes between the opening and closing tags
- * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
- * @return string HTML fragment
- */
- public static function tag($tagname, $contents, array $attributes = null) {
- return self::start_tag($tagname, $attributes) . $contents . self::end_tag($tagname);
- }
-
- /**
- * Outputs an opening tag with attributes
- *
- * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
- * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
- * @return string HTML fragment
- */
- public static function start_tag($tagname, array $attributes = null) {
- return '<' . $tagname . self::attributes($attributes) . '>';
- }
-
- /**
- * Outputs a closing tag
- *
- * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
- * @return string HTML fragment
- */
- public static function end_tag($tagname) {
- return '' . $tagname . '>';
- }
-
- /**
- * Outputs an empty tag with attributes
- *
- * @param string $tagname The name of tag ('input', 'img', 'br' etc.)
- * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
- * @return string HTML fragment
- */
- public static function empty_tag($tagname, array $attributes = null) {
- return '<' . $tagname . self::attributes($attributes) . ' />';
- }
-
- /**
- * Outputs a tag, but only if the contents are not empty
- *
- * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
- * @param string $contents What goes between the opening and closing tags
- * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
- * @return string HTML fragment
- */
- public static function nonempty_tag($tagname, $contents, array $attributes = null) {
- if ($contents === '' || is_null($contents)) {
- return '';
- }
- return self::tag($tagname, $contents, $attributes);
- }
-
- /**
- * Outputs a HTML attribute and value
- *
- * @param string $name The name of the attribute ('src', 'href', 'class' etc.)
- * @param string $value The value of the attribute. The value will be escaped with {@link s()}
- * @return string HTML fragment
- */
- public static function attribute($name, $value) {
- if ($value instanceof moodle_url) {
- return ' ' . $name . '="' . $value->out() . '"';
- }
-
- // special case, we do not want these in output
- if ($value === null) {
- return '';
- }
-
- // no sloppy trimming here!
- return ' ' . $name . '="' . s($value) . '"';
- }
-
- /**
- * Outputs a list of HTML attributes and values
- *
- * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
- * The values will be escaped with {@link s()}
- * @return string HTML fragment
- */
- public static function attributes(array $attributes = null) {
- $attributes = (array)$attributes;
- $output = '';
- foreach ($attributes as $name => $value) {
- $output .= self::attribute($name, $value);
- }
- return $output;
- }
-
- /**
- * Generates a simple image tag with attributes.
- *
- * @param string $src The source of image
- * @param string $alt The alternate text for image
- * @param array $attributes The tag attributes (array('height' => $max_height, 'class' => 'class1') etc.)
- * @return string HTML fragment
- */
- public static function img($src, $alt, array $attributes = null) {
- $attributes = (array)$attributes;
- $attributes['src'] = $src;
- // In case a null alt text is provided, set it to an empty string.
- $attributes['alt'] = $alt ?? '';
- if (array_key_exists('role', $attributes) && core_text::strtolower($attributes['role']) === 'presentation') {
- // A presentation role is not necessary for the img tag.
- // If a non-empty alt text is provided, the presentation role will conflict with the alt text.
- // An empty alt text denotes a decorative image. The presence of a presentation role is redundant.
- unset($attributes['role']);
- debugging('The presentation role is not necessary for an img tag.', DEBUG_DEVELOPER);
- }
-
- return self::empty_tag('img', $attributes);
- }
-
- /**
- * Generates random html element id.
- *
- * @staticvar int $counter
- * @staticvar string $uniq
- * @param string $base A string fragment that will be included in the random ID.
- * @return string A unique ID
- */
- public static function random_id($base='random') {
- static $counter = 0;
- static $uniq;
-
- if (!isset($uniq)) {
- $uniq = uniqid();
- }
-
- $counter++;
- return $base.$uniq.$counter;
- }
-
- /**
- * Generates a simple html link
- *
- * @param string|moodle_url $url The URL
- * @param string $text The text
- * @param array $attributes HTML attributes
- * @return string HTML fragment
- */
- public static function link($url, $text, array $attributes = null) {
- $attributes = (array)$attributes;
- $attributes['href'] = $url;
- return self::tag('a', $text, $attributes);
- }
-
- /**
- * Generates a simple checkbox with optional label
- *
- * @param string $name The name of the checkbox
- * @param string $value The value of the checkbox
- * @param bool $checked Whether the checkbox is checked
- * @param string $label The label for the checkbox
- * @param array $attributes Any attributes to apply to the checkbox
- * @param array $labelattributes Any attributes to apply to the label, if present
- * @return string html fragment
- */
- public static function checkbox($name, $value, $checked = true, $label = '',
- array $attributes = null, array $labelattributes = null) {
- $attributes = (array) $attributes;
- $output = '';
-
- if ($label !== '' and !is_null($label)) {
- if (empty($attributes['id'])) {
- $attributes['id'] = self::random_id('checkbox_');
- }
- }
- $attributes['type'] = 'checkbox';
- $attributes['value'] = $value;
- $attributes['name'] = $name;
- $attributes['checked'] = $checked ? 'checked' : null;
-
- $output .= self::empty_tag('input', $attributes);
-
- if ($label !== '' and !is_null($label)) {
- $labelattributes = (array) $labelattributes;
- $labelattributes['for'] = $attributes['id'];
- $output .= self::tag('label', $label, $labelattributes);
- }
-
- return $output;
- }
-
- /**
- * Generates a simple select yes/no form field
- *
- * @param string $name name of select element
- * @param bool $selected
- * @param array $attributes - html select element attributes
- * @return string HTML fragment
- */
- public static function select_yes_no($name, $selected=true, array $attributes = null) {
- $options = array('1'=>get_string('yes'), '0'=>get_string('no'));
- return self::select($options, $name, $selected, null, $attributes);
- }
-
- /**
- * Generates a simple select form field
- *
- * Note this function does HTML escaping on the optgroup labels, but not on the choice labels.
- *
- * @param array $options associative array value=>label ex.:
- * array(1=>'One, 2=>Two)
- * it is also possible to specify optgroup as complex label array ex.:
- * array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
- * array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
- * @param string $name name of select element
- * @param string|array $selected value or array of values depending on multiple attribute
- * @param array|bool|null $nothing add nothing selected option, or false of not added
- * @param array $attributes html select element attributes
- * @return string HTML fragment
- */
- public static function select(array $options, $name, $selected = '', $nothing = array('' => 'choosedots'), array $attributes = null) {
- $attributes = (array)$attributes;
- if (is_array($nothing)) {
- foreach ($nothing as $k=>$v) {
- if ($v === 'choose' or $v === 'choosedots') {
- $nothing[$k] = get_string('choosedots');
- }
- }
- $options = $nothing + $options; // keep keys, do not override
-
- } else if (is_string($nothing) and $nothing !== '') {
- // BC
- $options = array(''=>$nothing) + $options;
- }
-
- // we may accept more values if multiple attribute specified
- $selected = (array)$selected;
- foreach ($selected as $k=>$v) {
- $selected[$k] = (string)$v;
- }
-
- if (!isset($attributes['id'])) {
- $id = 'menu'.$name;
- // name may contaion [], which would make an invalid id. e.g. numeric question type editing form, assignment quickgrading
- $id = str_replace('[', '', $id);
- $id = str_replace(']', '', $id);
- $attributes['id'] = $id;
- }
-
- if (!isset($attributes['class'])) {
- $class = 'menu'.$name;
- // name may contaion [], which would make an invalid class. e.g. numeric question type editing form, assignment quickgrading
- $class = str_replace('[', '', $class);
- $class = str_replace(']', '', $class);
- $attributes['class'] = $class;
- }
- $attributes['class'] = 'select custom-select ' . $attributes['class']; // Add 'select' selector always.
-
- $attributes['name'] = $name;
-
- if (!empty($attributes['disabled'])) {
- $attributes['disabled'] = 'disabled';
- } else {
- unset($attributes['disabled']);
- }
-
- $output = '';
- foreach ($options as $value=>$label) {
- if (is_array($label)) {
- // ignore key, it just has to be unique
- $output .= self::select_optgroup(key($label), current($label), $selected);
- } else {
- $output .= self::select_option($label, $value, $selected);
- }
- }
- return self::tag('select', $output, $attributes);
- }
-
- /**
- * Returns HTML to display a select box option.
- *
- * @param string $label The label to display as the option.
- * @param string|int $value The value the option represents
- * @param array $selected An array of selected options
- * @return string HTML fragment
- */
- private static function select_option($label, $value, array $selected) {
- $attributes = array();
- $value = (string)$value;
- if (in_array($value, $selected, true)) {
- $attributes['selected'] = 'selected';
- }
- $attributes['value'] = $value;
- return self::tag('option', $label, $attributes);
- }
-
- /**
- * Returns HTML to display a select box option group.
- *
- * @param string $groupname The label to use for the group
- * @param array $options The options in the group
- * @param array $selected An array of selected values.
- * @return string HTML fragment.
- */
- private static function select_optgroup($groupname, $options, array $selected) {
- if (empty($options)) {
- return '';
- }
- $attributes = array('label'=>$groupname);
- $output = '';
- foreach ($options as $value=>$label) {
- $output .= self::select_option($label, $value, $selected);
- }
- return self::tag('optgroup', $output, $attributes);
- }
-
- /**
- * This is a shortcut for making an hour selector menu.
- *
- * @param string $type The type of selector (years, months, days, hours, minutes)
- * @param string $name fieldname
- * @param int $currenttime A default timestamp in GMT
- * @param int $step minute spacing
- * @param array $attributes - html select element attributes
- * @param float|int|string $timezone the timezone to use to calculate the time
- * {@link https://moodledev.io/docs/apis/subsystems/time#timezone}
- * @return string HTML fragment
- */
- public static function select_time($type, $name, $currenttime = 0, $step = 5, array $attributes = null, $timezone = 99) {
- global $OUTPUT;
-
- if (!$currenttime) {
- $currenttime = time();
- }
- $calendartype = \core_calendar\type_factory::get_calendar_instance();
- $currentdate = $calendartype->timestamp_to_date_array($currenttime, $timezone);
-
- $userdatetype = $type;
- $timeunits = array();
-
- switch ($type) {
- case 'years':
- $timeunits = $calendartype->get_years();
- $userdatetype = 'year';
- break;
- case 'months':
- $timeunits = $calendartype->get_months();
- $userdatetype = 'month';
- $currentdate['month'] = (int)$currentdate['mon'];
- break;
- case 'days':
- $timeunits = $calendartype->get_days();
- $userdatetype = 'mday';
- break;
- case 'hours':
- for ($i=0; $i<=23; $i++) {
- $timeunits[$i] = sprintf("%02d",$i);
- }
- break;
- case 'minutes':
- if ($step != 1) {
- $currentdate['minutes'] = ceil($currentdate['minutes']/$step)*$step;
- }
-
- for ($i=0; $i<=59; $i+=$step) {
- $timeunits[$i] = sprintf("%02d",$i);
- }
- break;
- default:
- throw new coding_exception("Time type $type is not supported by html_writer::select_time().");
- }
-
- $attributes = (array) $attributes;
- $data = (object) [
- 'name' => $name,
- 'id' => !empty($attributes['id']) ? $attributes['id'] : self::random_id('ts_'),
- 'label' => get_string(substr($type, 0, -1), 'form'),
- 'options' => array_map(function($value) use ($timeunits, $currentdate, $userdatetype) {
- return [
- 'name' => $timeunits[$value],
- 'value' => $value,
- 'selected' => $currentdate[$userdatetype] == $value
- ];
- }, array_keys($timeunits)),
- ];
-
- unset($attributes['id']);
- unset($attributes['name']);
- $data->attributes = array_map(function($name) use ($attributes) {
- return [
- 'name' => $name,
- 'value' => $attributes[$name]
- ];
- }, array_keys($attributes));
-
- return $OUTPUT->render_from_template('core/select_time', $data);
- }
-
- /**
- * Shortcut for quick making of lists
- *
- * Note: 'list' is a reserved keyword ;-)
- *
- * @param array $items
- * @param array $attributes
- * @param string $tag ul or ol
- * @return string
- */
- public static function alist(array $items, array $attributes = null, $tag = 'ul') {
- $output = html_writer::start_tag($tag, $attributes)."\n";
- foreach ($items as $item) {
- $output .= html_writer::tag('li', $item)."\n";
- }
- $output .= html_writer::end_tag($tag);
- return $output;
- }
-
- /**
- * Returns hidden input fields created from url parameters.
- *
- * @param moodle_url $url
- * @param array $exclude list of excluded parameters
- * @return string HTML fragment
- */
- public static function input_hidden_params(moodle_url $url, array $exclude = null) {
- $exclude = (array)$exclude;
- $params = $url->params();
- foreach ($exclude as $key) {
- unset($params[$key]);
- }
-
- $output = '';
- foreach ($params as $key => $value) {
- $attributes = array('type'=>'hidden', 'name'=>$key, 'value'=>$value);
- $output .= self::empty_tag('input', $attributes)."\n";
- }
- return $output;
- }
-
- /**
- * Generate a script tag containing the the specified code.
- *
- * @param string $jscode the JavaScript code
- * @param moodle_url|string $url optional url of the external script, $code ignored if specified
- * @return string HTML, the code wrapped in ';
- } else {
- $code = '';
- foreach ($baserollups as $rollup) {
- $code .= '';
- }
- return $code;
- }
-
- }
-
- /**
- * Returns html tags needed for inclusion of theme CSS.
- *
- * @return string
- */
- protected function get_css_code() {
- // First of all the theme CSS, then any custom CSS
- // Please note custom CSS is strongly discouraged,
- // because it can not be overridden by themes!
- // It is suitable only for things like mod/data which accepts CSS from teachers.
- $attributes = array('rel'=>'stylesheet', 'type'=>'text/css');
-
- // Add the YUI code first. We want this to be overridden by any Moodle CSS.
- $code = $this->get_yui3lib_headcss();
-
- // This line of code may look funny but it is currently required in order
- // to avoid MASSIVE display issues in Internet Explorer.
- // As of IE8 + YUI3.1.1 the reference stylesheet (firstthemesheet) gets
- // ignored whenever another resource is added until such time as a redraw
- // is forced, usually by moving the mouse over the affected element.
- $code .= html_writer::tag('script', '/** Required in order to fix style inclusion problems in IE with YUI **/', array('id'=>'firstthemesheet', 'type'=>'text/css'));
-
- $urls = $this->cssthemeurls + $this->cssurls;
- foreach ($urls as $url) {
- $attributes['href'] = $url;
- $code .= html_writer::empty_tag('link', $attributes) . "\n";
- // This id is needed in first sheet only so that theme may override YUI sheets loaded on the fly.
- unset($attributes['id']);
- }
-
- return $code;
- }
-
- /**
- * Adds extra modules specified after printing of page header.
- *
- * @return string
- */
- protected function get_extra_modules_code() {
- if (empty($this->extramodules)) {
- return '';
- }
- return html_writer::script(js_writer::function_call('M.yui.add_module', array($this->extramodules)));
- }
-
- /**
- * Generate any HTML that needs to go inside the tag.
- *
- * Normally, this method is called automatically by the code that prints the
- * tag. You should not normally need to call it in your own code.
- *
- * @param moodle_page $page
- * @param core_renderer $renderer
- * @return string the HTML code to to inside the tag.
- */
- public function get_head_code(moodle_page $page, core_renderer $renderer) {
- global $CFG;
-
- // Note: the $page and $output are not stored here because it would
- // create circular references in memory which prevents garbage collection.
- $this->init_requirements_data($page, $renderer);
-
- $output = '';
-
- // Add all standard CSS for this page.
- $output .= $this->get_css_code();
-
- // Set up the M namespace.
- $js = "var M = {}; M.yui = {};\n";
-
- // Capture the time now ASAP during page load. This minimises the lag when
- // we try to relate times on the server to times in the browser.
- // An example of where this is used is the quiz countdown timer.
- $js .= "M.pageloadstarttime = new Date();\n";
-
- // Add a subset of Moodle configuration to the M namespace.
- $js .= js_writer::set_variable('M.cfg', $this->M_cfg, false);
-
- // Set up global YUI3 loader object - this should contain all code needed by plugins.
- // Note: in JavaScript just use "YUI().use('overlay', function(Y) { .... });",
- // this needs to be done before including any other script.
- $js .= $this->YUI_config->get_config_functions();
- $js .= js_writer::set_variable('YUI_config', $this->YUI_config, false) . "\n";
- $js .= "M.yui.loader = {modules: {}};\n"; // Backwards compatibility only, not used any more.
- $js = $this->YUI_config->update_header_js($js);
-
- $output .= html_writer::script($js);
-
- // Add variables.
- if ($this->jsinitvariables['head']) {
- $js = '';
- foreach ($this->jsinitvariables['head'] as $data) {
- list($var, $value) = $data;
- $js .= js_writer::set_variable($var, $value, true);
- }
- $output .= html_writer::script($js);
- }
-
- // Mark head sending done, it is not possible to anything there.
- $this->headdone = true;
-
- return $output;
- }
-
- /**
- * Generate any HTML that needs to go at the start of the tag.
- *
- * Normally, this method is called automatically by the code that prints the
- * tag. You should not normally need to call it in your own code.
- *
- * @param renderer_base $renderer
- * @return string the HTML code to go at the start of the tag.
- */
- public function get_top_of_body_code(renderer_base $renderer) {
- global $CFG;
-
- // First the skip links.
- $output = $renderer->render_skip_links($this->skiplinks);
-
- // Include the Polyfills.
- $output .= html_writer::script('', $this->js_fix_url('/lib/polyfills/polyfill.js'));
-
- // YUI3 JS needs to be loaded early in the body. It should be cached well by the browser.
- $output .= $this->get_yui3lib_headcode();
-
- // Add hacked jQuery support, it is not intended for standard Moodle distribution!
- $output .= $this->get_jquery_headcode();
-
- // Link our main JS file, all core stuff should be there.
- $output .= html_writer::script('', $this->js_fix_url('/lib/javascript-static.js'));
-
- // All the other linked things from HEAD - there should be as few as possible.
- if ($this->jsincludes['head']) {
- foreach ($this->jsincludes['head'] as $url) {
- $output .= html_writer::script('', $url);
- }
- }
-
- // Then the clever trick for hiding of things not needed when JS works.
- $output .= html_writer::script("document.body.className += ' jsenabled';") . "\n";
- $this->topofbodydone = true;
- return $output;
- }
-
- /**
- * Generate any HTML that needs to go at the end of the page.
- *
- * Normally, this method is called automatically by the code that prints the
- * page footer. You should not normally need to call it in your own code.
- *
- * @return string the HTML code to to at the end of the page.
- */
- public function get_end_code() {
- global $CFG;
- $output = '';
-
- // Set the log level for the JS logging.
- $logconfig = new stdClass();
- $logconfig->level = 'warn';
- if ($CFG->debugdeveloper) {
- $logconfig->level = 'trace';
- }
- $this->js_call_amd('core/log', 'setConfig', array($logconfig));
- // Add any global JS that needs to run on all pages.
- $this->js_call_amd('core/page_global', 'init');
- $this->js_call_amd('core/utility');
-
- // Call amd init functions.
- $output .= $this->get_amd_footercode();
-
- // Add other requested modules.
- $output .= $this->get_extra_modules_code();
-
- $this->js_init_code('M.util.js_complete("init");', true);
-
- // All the other linked scripts - there should be as few as possible.
- if ($this->jsincludes['footer']) {
- foreach ($this->jsincludes['footer'] as $url) {
- $output .= html_writer::script('', $url);
- }
- }
-
- // Add all needed strings.
- // First add core strings required for some dialogues.
- $this->strings_for_js(array(
- 'confirm',
- 'yes',
- 'no',
- 'areyousure',
- 'closebuttontitle',
- 'unknownerror',
- 'error',
- 'file',
- 'url',
- // TODO MDL-70830 shortforms should preload the collapseall/expandall strings properly.
- 'collapseall',
- 'expandall',
- ), 'moodle');
- $this->strings_for_js(array(
- 'debuginfo',
- 'line',
- 'stacktrace',
- ), 'debug');
- $this->string_for_js('labelsep', 'langconfig');
- if (!empty($this->stringsforjs)) {
- $strings = array();
- foreach ($this->stringsforjs as $component=>$v) {
- foreach($v as $indentifier => $langstring) {
- $strings[$component][$indentifier] = $langstring->out();
- }
- }
- $output .= html_writer::script(js_writer::set_variable('M.str', $strings));
- }
-
- // Add variables.
- if ($this->jsinitvariables['footer']) {
- $js = '';
- foreach ($this->jsinitvariables['footer'] as $data) {
- list($var, $value) = $data;
- $js .= js_writer::set_variable($var, $value, true);
- }
- $output .= html_writer::script($js);
- }
-
- $inyuijs = $this->get_javascript_code(false);
- $ondomreadyjs = $this->get_javascript_code(true);
- $jsinit = $this->get_javascript_init_code();
- $handlersjs = $this->get_event_handler_code();
-
- // There is a global Y, make sure it is available in your scope.
- $js = "(function() {{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}})();";
-
- $output .= html_writer::script($js);
-
- return $output;
- }
-
- /**
- * Have we already output the code in the tag?
- *
- * @return bool
- */
- public function is_head_done() {
- return $this->headdone;
- }
-
- /**
- * Have we already output the code at the start of the tag?
- *
- * @return bool
- */
- public function is_top_of_body_done() {
- return $this->topofbodydone;
- }
-
- /**
- * Should we generate a bit of content HTML that is only required once on
- * this page (e.g. the contents of the modchooser), now? Basically, we call
- * {@link has_one_time_item_been_created()}, and if the thing has not already
- * been output, we return true to tell the caller to generate it, and also
- * call {@link set_one_time_item_created()} to record the fact that it is
- * about to be generated.
- *
- * That is, a typical usage pattern (in a renderer method) is:
- *
- * if (!$this->page->requires->should_create_one_time_item_now($thing)) {
- * return '';
- * }
- * // Else generate it.
- *
- *
- * @param string $thing identifier for the bit of content. Should be of the form
- * frankenstyle_things, e.g. core_course_modchooser.
- * @return bool if true, the caller should generate that bit of output now, otherwise don't.
- */
- public function should_create_one_time_item_now($thing) {
- if ($this->has_one_time_item_been_created($thing)) {
- return false;
- }
-
- $this->set_one_time_item_created($thing);
- return true;
- }
-
- /**
- * Has a particular bit of HTML that is only required once on this page
- * (e.g. the contents of the modchooser) already been generated?
- *
- * Normally, you can use the {@link should_create_one_time_item_now()} helper
- * method rather than calling this method directly.
- *
- * @param string $thing identifier for the bit of content. Should be of the form
- * frankenstyle_things, e.g. core_course_modchooser.
- * @return bool whether that bit of output has been created.
- */
- public function has_one_time_item_been_created($thing) {
- return isset($this->onetimeitemsoutput[$thing]);
- }
-
- /**
- * Indicate that a particular bit of HTML that is only required once on this
- * page (e.g. the contents of the modchooser) has been generated (or is about to be)?
- *
- * Normally, you can use the {@link should_create_one_time_item_now()} helper
- * method rather than calling this method directly.
- *
- * @param string $thing identifier for the bit of content. Should be of the form
- * frankenstyle_things, e.g. core_course_modchooser.
- */
- public function set_one_time_item_created($thing) {
- if ($this->has_one_time_item_been_created($thing)) {
- throw new coding_exception($thing . ' is only supposed to be ouput ' .
- 'once per page, but it seems to be being output again.');
- }
- return $this->onetimeitemsoutput[$thing] = true;
- }
-}
-
-/**
- * This class represents the YUI configuration.
- *
- * @copyright 2013 Andrew Nicols
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @since Moodle 2.5
- * @package core
- * @category output
- */
-class YUI_config {
- /**
- * These settings must be public so that when the object is converted to json they are exposed.
- * Note: Some of these are camelCase because YUI uses camelCase variable names.
- *
- * The settings are described and documented in the YUI API at:
- * - http://yuilibrary.com/yui/docs/api/classes/config.html
- * - http://yuilibrary.com/yui/docs/api/classes/Loader.html
- */
- public $debug = false;
- public $base;
- public $comboBase;
- public $combine;
- public $filter = null;
- public $insertBefore = 'firstthemesheet';
- public $groups = array();
- public $modules = array();
- /** @var array The log sources that should be not be logged. */
- public $logInclude = [];
- /** @var array Tog sources that should be logged. */
- public $logExclude = [];
- /** @var string The minimum log level for YUI logging statements. */
- public $logLevel;
-
- /**
- * @var array List of functions used by the YUI Loader group pattern recognition.
- */
- protected $jsconfigfunctions = array();
-
- /**
- * Create a new group within the YUI_config system.
- *
- * @param string $name The name of the group. This must be unique and
- * not previously used.
- * @param array $config The configuration for this group.
- * @return void
- */
- public function add_group($name, $config) {
- if (isset($this->groups[$name])) {
- throw new coding_exception("A YUI configuration group for '{$name}' already exists. To make changes to this group use YUI_config->update_group().");
- }
- $this->groups[$name] = $config;
- }
-
- /**
- * Update an existing group configuration
- *
- * Note, any existing configuration for that group will be wiped out.
- * This includes module configuration.
- *
- * @param string $name The name of the group. This must be unique and
- * not previously used.
- * @param array $config The configuration for this group.
- * @return void
- */
- public function update_group($name, $config) {
- if (!isset($this->groups[$name])) {
- throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.');
- }
- $this->groups[$name] = $config;
- }
-
- /**
- * Set the value of a configuration function used by the YUI Loader's pattern testing.
- *
- * Only the body of the function should be passed, and not the whole function wrapper.
- *
- * The JS function your write will be passed a single argument 'name' containing the
- * name of the module being loaded.
- *
- * @param $function String the body of the JavaScript function. This should be used i
- * @return string the name of the function to use in the group pattern configuration.
- */
- public function set_config_function($function) {
- $configname = 'yui' . (count($this->jsconfigfunctions) + 1) . 'ConfigFn';
- if (isset($this->jsconfigfunctions[$configname])) {
- throw new coding_exception("A YUI config function with this name already exists. Config function names must be unique.");
- }
- $this->jsconfigfunctions[$configname] = $function;
- return '@' . $configname . '@';
- }
-
- /**
- * Allow setting of the config function described in {@see set_config_function} from a file.
- * The contents of this file are then passed to set_config_function.
- *
- * When jsrev is positive, the function is minified and stored in a MUC cache for subsequent uses.
- *
- * @param $file The path to the JavaScript function used for YUI configuration.
- * @return string the name of the function to use in the group pattern configuration.
- */
- public function set_config_source($file) {
- global $CFG;
- $cache = cache::make('core', 'yuimodules');
-
- // Attempt to get the metadata from the cache.
- $keyname = 'configfn_' . $file;
- $fullpath = $CFG->dirroot . '/' . $file;
- if (!isset($CFG->jsrev) || $CFG->jsrev == -1) {
- $cache->delete($keyname);
- $configfn = file_get_contents($fullpath);
- } else {
- $configfn = $cache->get($keyname);
- if ($configfn === false) {
- require_once($CFG->libdir . '/jslib.php');
- $configfn = core_minify::js_files(array($fullpath));
- $cache->set($keyname, $configfn);
- }
- }
- return $this->set_config_function($configfn);
- }
-
- /**
- * Retrieve the list of JavaScript functions for YUI_config groups.
- *
- * @return string The complete set of config functions
- */
- public function get_config_functions() {
- $configfunctions = '';
- foreach ($this->jsconfigfunctions as $functionname => $function) {
- $configfunctions .= "var {$functionname} = function(me) {";
- $configfunctions .= $function;
- $configfunctions .= "};\n";
- }
- return $configfunctions;
- }
-
- /**
- * Update the header JavaScript with any required modification for the YUI Loader.
- *
- * @param $js String The JavaScript to manipulate.
- * @return string the modified JS string.
- */
- public function update_header_js($js) {
- // Update the names of the the configFn variables.
- // The PHP json_encode function cannot handle literal names so we have to wrap
- // them in @ and then replace them with literals of the same function name.
- foreach ($this->jsconfigfunctions as $functionname => $function) {
- $js = str_replace('"@' . $functionname . '@"', $functionname, $js);
- }
- return $js;
- }
-
- /**
- * Add configuration for a specific module.
- *
- * @param string $name The name of the module to add configuration for.
- * @param array $config The configuration for the specified module.
- * @param string $group The name of the group to add configuration for.
- * If not specified, then this module is added to the global
- * configuration.
- * @return void
- */
- public function add_module_config($name, $config, $group = null) {
- if ($group) {
- if (!isset($this->groups[$name])) {
- throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.');
- }
- if (!isset($this->groups[$group]['modules'])) {
- $this->groups[$group]['modules'] = array();
- }
- $modules = &$this->groups[$group]['modules'];
- } else {
- $modules = &$this->modules;
- }
- $modules[$name] = $config;
- }
-
- /**
- * Add the moodle YUI module metadata for the moodle group to the YUI_config instance.
- *
- * If js caching is disabled, metadata will not be served causing YUI to calculate
- * module dependencies as each module is loaded.
- *
- * If metadata does not exist it will be created and stored in a MUC entry.
- *
- * @return void
- */
- public function add_moodle_metadata() {
- global $CFG;
- if (!isset($this->groups['moodle'])) {
- throw new coding_exception('The Moodle YUI module does not exist. You must define the moodle module config using YUI_config->add_module_config first.');
- }
-
- if (!isset($this->groups['moodle']['modules'])) {
- $this->groups['moodle']['modules'] = array();
- }
-
- $cache = cache::make('core', 'yuimodules');
- if (!isset($CFG->jsrev) || $CFG->jsrev == -1) {
- $metadata = array();
- $metadata = $this->get_moodle_metadata();
- $cache->delete('metadata');
- } else {
- // Attempt to get the metadata from the cache.
- if (!$metadata = $cache->get('metadata')) {
- $metadata = $this->get_moodle_metadata();
- $cache->set('metadata', $metadata);
- }
- }
-
- // Merge with any metadata added specific to this page which was added manually.
- $this->groups['moodle']['modules'] = array_merge($this->groups['moodle']['modules'],
- $metadata);
- }
-
- /**
- * Determine the module metadata for all moodle YUI modules.
- *
- * This works through all modules capable of serving YUI modules, and attempts to get
- * metadata for each of those modules.
- *
- * @return array of module metadata
- */
- private function get_moodle_metadata() {
- $moodlemodules = array();
- // Core isn't a plugin type or subsystem - handle it seperately.
- if ($module = $this->get_moodle_path_metadata(core_component::get_component_directory('core'))) {
- $moodlemodules = array_merge($moodlemodules, $module);
- }
-
- // Handle other core subsystems.
- $subsystems = core_component::get_core_subsystems();
- foreach ($subsystems as $subsystem => $path) {
- if (is_null($path)) {
- continue;
- }
- if ($module = $this->get_moodle_path_metadata($path)) {
- $moodlemodules = array_merge($moodlemodules, $module);
- }
- }
-
- // And finally the plugins.
- $plugintypes = core_component::get_plugin_types();
- foreach ($plugintypes as $plugintype => $pathroot) {
- $pluginlist = core_component::get_plugin_list($plugintype);
- foreach ($pluginlist as $plugin => $path) {
- if ($module = $this->get_moodle_path_metadata($path)) {
- $moodlemodules = array_merge($moodlemodules, $module);
- }
- }
- }
-
- return $moodlemodules;
- }
-
- /**
- * Helper function process and return the YUI metadata for all of the modules under the specified path.
- *
- * @param string $path the UNC path to the YUI src directory.
- * @return array the complete array for frankenstyle directory.
- */
- private function get_moodle_path_metadata($path) {
- // Add module metadata is stored in frankenstyle_modname/yui/src/yui_modname/meta/yui_modname.json.
- $baseyui = $path . '/yui/src';
- $modules = array();
- if (is_dir($baseyui)) {
- $items = new DirectoryIterator($baseyui);
- foreach ($items as $item) {
- if ($item->isDot() or !$item->isDir()) {
- continue;
- }
- $metafile = realpath($baseyui . '/' . $item . '/meta/' . $item . '.json');
- if (!is_readable($metafile)) {
- continue;
- }
- $metadata = file_get_contents($metafile);
- $modules = array_merge($modules, (array) json_decode($metadata));
- }
- }
- return $modules;
- }
-
- /**
- * Define YUI modules which we have been required to patch between releases.
- *
- * We must do this because we aggressively cache content on the browser, and we must also override use of the
- * external CDN which will serve the true authoritative copy of the code without our patches.
- *
- * @param string $combobase The local combobase
- * @param string $yuiversion The current YUI version
- * @param int $patchlevel The patch level we're working to for YUI
- * @param array $patchedmodules An array containing the names of the patched modules
- * @return void
- */
- public function define_patched_core_modules($combobase, $yuiversion, $patchlevel, $patchedmodules) {
- // The version we use is suffixed with a patchlevel so that we can get additional revisions between YUI releases.
- $subversion = $yuiversion . '_' . $patchlevel;
-
- if ($this->comboBase == $combobase) {
- // If we are using the local combobase in the loader, we can add a group and still make use of the combo
- // loader. We just need to specify a different root which includes a slightly different YUI version number
- // to include our patchlevel.
- $patterns = array();
- $modules = array();
- foreach ($patchedmodules as $modulename) {
- // We must define the pattern and module here so that the loader uses our group configuration instead of
- // the standard module definition. We may lose some metadata provided by upstream but this will be
- // loaded when the module is loaded anyway.
- $patterns[$modulename] = array(
- 'group' => 'yui-patched',
- );
- $modules[$modulename] = array();
- }
-
- // Actually add the patch group here.
- $this->add_group('yui-patched', array(
- 'combine' => true,
- 'root' => $subversion . '/',
- 'patterns' => $patterns,
- 'modules' => $modules,
- ));
-
- } else {
- // The CDN is in use - we need to instead use the local combobase for this module and override the modules
- // definition. We cannot use the local base - we must use the combobase because we cannot invalidate the
- // local base in browser caches.
- $fullpathbase = $combobase . $subversion . '/';
- foreach ($patchedmodules as $modulename) {
- $this->modules[$modulename] = array(
- 'fullpath' => $fullpathbase . $modulename . '/' . $modulename . '-min.js'
- );
- }
- }
- }
-}
-
-/**
- * Invalidate all server and client side template caches.
- */
-function template_reset_all_caches() {
- global $CFG;
-
- $next = time();
- if (isset($CFG->templaterev) and $next <= $CFG->templaterev and $CFG->templaterev - $next < 60 * 60) {
- // This resolves problems when reset is requested repeatedly within 1s,
- // the < 1h condition prevents accidental switching to future dates
- // because we might not recover from it.
- $next = $CFG->templaterev + 1;
- }
-
- set_config('templaterev', $next);
-}
-
-/**
- * Invalidate all server and client side JS caches.
- */
-function js_reset_all_caches() {
- global $CFG;
-
- $next = time();
- if (isset($CFG->jsrev) and $next <= $CFG->jsrev and $CFG->jsrev - $next < 60*60) {
- // This resolves problems when reset is requested repeatedly within 1s,
- // the < 1h condition prevents accidental switching to future dates
- // because we might not recover from it.
- $next = $CFG->jsrev+1;
- }
-
- set_config('jsrev', $next);
-}
+// This file is deprecated, but it should never have been manually included by anything outside of lib/outputlib.php.
+// Throwing an exception here should be fine because removing the manual inclusion should have no impact.
+throw new \core\exception\coding_exception(
+ 'This file should not be manually included by any component.',
+);
diff --git a/lib/pagelib.php b/lib/pagelib.php
index cd649a39d7a..169e0354a15 100644
--- a/lib/pagelib.php
+++ b/lib/pagelib.php
@@ -1034,9 +1034,6 @@ class moodle_page {
* by the get_fragment() web service and not for use elsewhere.
*/
public function start_collecting_javascript_requirements() {
- global $CFG;
- require_once($CFG->libdir.'/outputfragmentrequirementslib.php');
-
// Check that the requirements manager has not already been switched.
if (get_class($this->_requires) == 'fragment_requirements_manager') {
throw new coding_exception('JavaScript collection has already been started.');
diff --git a/lib/table/classes/output/html_table.php b/lib/table/classes/output/html_table.php
new file mode 100644
index 00000000000..86cc3a3b4e2
--- /dev/null
+++ b/lib/table/classes/output/html_table.php
@@ -0,0 +1,207 @@
+.
+
+/**
+ * Holds all the information required to render a by {@link core_renderer::table()}
+ *
+ * Example of usage:
+ * $t = new html_table();
+ * ... // set various properties of the object $t as described below
+ * echo html_writer::table($t);
+ *
+ * @copyright 2009 David Mudrak
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class html_table {
+
+ /**
+ * @var string Value to use for the id attribute of the table
+ */
+ public $id = null;
+
+ /**
+ * @var array Attributes of HTML attributes for the element
+ */
+ public $attributes = array();
+
+ /**
+ * @var array An array of headings. The n-th array item is used as a heading of the n-th column.
+ * For more control over the rendering of the headers, an array of html_table_cell objects
+ * can be passed instead of an array of strings.
+ *
+ * Example of usage:
+ * $t->head = array('Student', 'Grade');
+ */
+ public $head;
+
+ /**
+ * @var array An array that can be used to make a heading span multiple columns.
+ * In this example, {@link html_table:$data} is supposed to have three columns. For the first two columns,
+ * the same heading is used. Therefore, {@link html_table::$head} should consist of two items.
+ *
+ * Example of usage:
+ * $t->headspan = array(2,1);
+ */
+ public $headspan;
+
+ /**
+ * @var array An array of column alignments.
+ * The value is used as CSS 'text-align' property. Therefore, possible
+ * values are 'left', 'right', 'center' and 'justify'. Specify 'right' or 'left' from the perspective
+ * of a left-to-right (LTR) language. For RTL, the values are flipped automatically.
+ *
+ * Examples of usage:
+ * $t->align = array(null, 'right');
+ * or
+ * $t->align[1] = 'right';
+ */
+ public $align;
+
+ /**
+ * @var array The value is used as CSS 'size' property.
+ *
+ * Examples of usage:
+ * $t->size = array('50%', '50%');
+ * or
+ * $t->size[1] = '120px';
+ */
+ public $size;
+
+ /**
+ * @var array An array of wrapping information.
+ * The only possible value is 'nowrap' that sets the
+ * CSS property 'white-space' to the value 'nowrap' in the given column.
+ *
+ * Example of usage:
+ * $t->wrap = array(null, 'nowrap');
+ */
+ public $wrap;
+
+ /**
+ * @var array Array of arrays or html_table_row objects containing the data. Alternatively, if you have
+ * $head specified, the string 'hr' (for horizontal ruler) can be used
+ * instead of an array of cells data resulting in a divider rendered.
+ *
+ * Example of usage with array of arrays:
+ * $row1 = array('Harry Potter', '76 %');
+ * $row2 = array('Hermione Granger', '100 %');
+ * $t->data = array($row1, $row2);
+ *
+ * Example with array of html_table_row objects: (used for more fine-grained control)
+ * $cell1 = new html_table_cell();
+ * $cell1->text = 'Harry Potter';
+ * $cell1->colspan = 2;
+ * $row1 = new html_table_row();
+ * $row1->cells[] = $cell1;
+ * $cell2 = new html_table_cell();
+ * $cell2->text = 'Hermione Granger';
+ * $cell3 = new html_table_cell();
+ * $cell3->text = '100 %';
+ * $row2 = new html_table_row();
+ * $row2->cells = array($cell2, $cell3);
+ * $t->data = array($row1, $row2);
+ */
+ public $data = [];
+
+ /**
+ * @deprecated since Moodle 2.0. Styling should be in the CSS.
+ * @var string Width of the table, percentage of the page preferred.
+ */
+ public $width = null;
+
+ /**
+ * @deprecated since Moodle 2.0. Styling should be in the CSS.
+ * @var string Alignment for the whole table. Can be 'right', 'left' or 'center' (default).
+ */
+ public $tablealign = null;
+
+ /**
+ * @deprecated since Moodle 2.0. Styling should be in the CSS.
+ * @var int Padding on each cell, in pixels
+ */
+ public $cellpadding = null;
+
+ /**
+ * @var int Spacing between cells, in pixels
+ * @deprecated since Moodle 2.0. Styling should be in the CSS.
+ */
+ public $cellspacing = null;
+
+ /**
+ * @var array Array of classes to add to particular rows, space-separated string.
+ * Class 'lastrow' is added automatically for the last row in the table.
+ *
+ * Example of usage:
+ * $t->rowclasses[9] = 'tenth'
+ */
+ public $rowclasses;
+
+ /**
+ * @var array An array of classes to add to every cell in a particular column,
+ * space-separated string. Class 'cell' is added automatically by the renderer.
+ * Classes 'c0' or 'c1' are added automatically for every odd or even column,
+ * respectively. Class 'lastcol' is added automatically for all last cells
+ * in a row.
+ *
+ * Example of usage:
+ * $t->colclasses = array(null, 'grade');
+ */
+ public $colclasses;
+
+ /**
+ * @var string Description of the contents for screen readers.
+ *
+ * The "summary" attribute on the "table" element is not supported in HTML5.
+ * Consider describing the structure of the table in a "caption" element or in a "figure" element containing the table;
+ * or, simplify the structure of the table so that no description is needed.
+ *
+ * @deprecated since Moodle 3.9.
+ */
+ public $summary;
+
+ /**
+ * @var string Caption for the table, typically a title.
+ *
+ * Example of usage:
+ * $t->caption = "TV Guide";
+ */
+ public $caption;
+
+ /**
+ * @var bool Whether to hide the table's caption from sighted users.
+ *
+ * Example of usage:
+ * $t->caption = "TV Guide";
+ * $t->captionhide = true;
+ */
+ public $captionhide = false;
+
+ /** @var bool Whether to make the table to be scrolled horizontally with ease. Make table responsive across all viewports. */
+ public $responsive = true;
+
+ /** @var string class name to add to this html table. */
+ public $class;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ $this->attributes['class'] = '';
+ }
+}
diff --git a/lib/table/classes/output/html_table_cell.php b/lib/table/classes/output/html_table_cell.php
new file mode 100644
index 00000000000..bc4c8ab31fb
--- /dev/null
+++ b/lib/table/classes/output/html_table_cell.php
@@ -0,0 +1,82 @@
+.
+
+/**
+ * Component representing a table cell.
+ *
+ * @copyright 2009 Nicolas Connault
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class html_table_cell {
+
+ /**
+ * @var string Value to use for the id attribute of the cell.
+ */
+ public $id = null;
+
+ /**
+ * @var string The contents of the cell.
+ */
+ public $text;
+
+ /**
+ * @var string Abbreviated version of the contents of the cell.
+ */
+ public $abbr = null;
+
+ /**
+ * @var int Number of columns this cell should span.
+ */
+ public $colspan = null;
+
+ /**
+ * @var int Number of rows this cell should span.
+ */
+ public $rowspan = null;
+
+ /**
+ * @var string Defines a way to associate header cells and data cells in a table.
+ */
+ public $scope = null;
+
+ /**
+ * @var bool Whether or not this cell is a header cell.
+ */
+ public $header = null;
+
+ /**
+ * @var string Value to use for the style attribute of the table cell
+ */
+ public $style = null;
+
+ /**
+ * @var array Attributes of additional HTML attributes for the | element
+ */
+ public $attributes = array();
+
+ /**
+ * Constructs a table cell
+ *
+ * @param string $text
+ */
+ public function __construct($text = null) {
+ $this->text = $text;
+ $this->attributes['class'] = '';
+ }
+}
diff --git a/lib/table/classes/output/html_table_row.php b/lib/table/classes/output/html_table_row.php
new file mode 100644
index 00000000000..8a0a366bd17
--- /dev/null
+++ b/lib/table/classes/output/html_table_row.php
@@ -0,0 +1,63 @@
+.
+
+/**
+ * Component representing a table row.
+ *
+ * @copyright 2009 Nicolas Connault
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.0
+ * @package core
+ * @category output
+ */
+class html_table_row {
+
+ /**
+ * @var string Value to use for the id attribute of the row.
+ */
+ public $id = null;
+
+ /**
+ * @var array Array of html_table_cell objects
+ */
+ public $cells = array();
+
+ /**
+ * @var string Value to use for the style attribute of the table row
+ */
+ public $style = null;
+
+ /**
+ * @var array Attributes of additional HTML attributes for the | element
+ */
+ public $attributes = array();
+
+ /**
+ * Constructor
+ * @param array $cells
+ */
+ public function __construct(array $cells=null) {
+ $this->attributes['class'] = '';
+ $cells = (array)$cells;
+ foreach ($cells as $cell) {
+ if ($cell instanceof html_table_cell) {
+ $this->cells[] = $cell;
+ } else {
+ $this->cells[] = new html_table_cell($cell);
+ }
+ }
+ }
+}
diff --git a/lib/tests/coverage.php b/lib/tests/coverage.php
index 0c8faeeff83..2d6961cdd8c 100644
--- a/lib/tests/coverage.php
+++ b/lib/tests/coverage.php
@@ -82,13 +82,7 @@ return new class extends phpunit_coverage_info {
'myprofilelib.php',
'navigationlib.php',
'oauthlib.php',
- 'outputactions.php',
- 'outputcomponents.php',
- 'outputfactories.php',
- 'outputfragmentrequirementslib.php',
'outputlib.php',
- 'outputrenderers.php',
- 'outputrequirementslib.php',
'pagelib.php',
'pdflib.php',
'phpminimumversionlib.php',
diff --git a/lib/tests/fixtures/test_renderer_factory.php b/lib/tests/fixtures/test_renderer_factory.php
index 4d32622c496..fcfb0d06c80 100644
--- a/lib/tests/fixtures/test_renderer_factory.php
+++ b/lib/tests/fixtures/test_renderer_factory.php
@@ -14,20 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-/**
- * Test factory for lib/outputfactories.php.
- *
- * @package core
- * @category phpunit
- * @copyright 2014 Damyon Wiese
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/outputfactories.php');
-
/**
* This is renderer factory testing of the classname autoloading.
*
diff --git a/lib/tests/html_writer_test.php b/lib/tests/html_writer_test.php
index f40b823ee7b..7823a0def9d 100644
--- a/lib/tests/html_writer_test.php
+++ b/lib/tests/html_writer_test.php
@@ -14,20 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-/**
- * Unit tests for the html_writer class.
- *
- * @package core
- * @category phpunit
- * @copyright 2010 Tim Hunt
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/outputcomponents.php');
-
/**
* Unit tests for the html_writer class.
*
@@ -36,8 +22,7 @@ require_once($CFG->libdir . '/outputcomponents.php');
* @covers \html_writer
* @coversDefaultClass \html_writer
*/
-class html_writer_test extends basic_testcase {
-
+final class html_writer_test extends basic_testcase {
/**
* @covers ::start_tag
*/
diff --git a/lib/tests/outputcomponents_test.php b/lib/tests/outputcomponents_test.php
index 7ba5b6c7944..444351e9f79 100644
--- a/lib/tests/outputcomponents_test.php
+++ b/lib/tests/outputcomponents_test.php
@@ -27,11 +27,6 @@ use theme_config;
use url_select;
use user_picture;
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/outputcomponents.php');
-
/**
* Unit tests for lib/outputcomponents.php.
*
@@ -40,8 +35,7 @@ require_once($CFG->libdir . '/outputcomponents.php');
* @copyright 2011 David Mudrak
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class outputcomponents_test extends \advanced_testcase {
-
+final class outputcomponents_test extends \advanced_testcase {
/**
* Tests user_picture::fields.
*
diff --git a/lib/tests/outputfactories_test.php b/lib/tests/outputfactories_test.php
index b0e17ba0333..f235de37a8c 100644
--- a/lib/tests/outputfactories_test.php
+++ b/lib/tests/outputfactories_test.php
@@ -18,12 +18,6 @@ namespace core;
use test_output_factory;
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/outputfactories.php');
-require_once($CFG->libdir . '/tests/fixtures/test_renderer_factory.php');
-
/**
* Unit tests for lib/outputfactories.php.
*
@@ -32,7 +26,15 @@ require_once($CFG->libdir . '/tests/fixtures/test_renderer_factory.php');
* @copyright 2014 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class outputfactories_test extends \advanced_testcase {
+final class outputfactories_test extends \advanced_testcase {
+ #[\Override]
+ public static function setUpBeforeClass(): void {
+ global $CFG;
+
+ require_once($CFG->libdir . '/tests/fixtures/test_renderer_factory.php');
+
+ parent::setUpBeforeClass();
+ }
public function test_nonautoloaded_classnames(): void {
global $PAGE;
diff --git a/lib/tests/outputrequirementslib_test.php b/lib/tests/outputrequirementslib_test.php
index 2209b567066..32535a6a17f 100644
--- a/lib/tests/outputrequirementslib_test.php
+++ b/lib/tests/outputrequirementslib_test.php
@@ -16,12 +16,6 @@
namespace core;
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/outputrequirementslib.php');
-
-
/**
* Unit tests for lib/outputrequirementslibphp.
*
@@ -30,7 +24,7 @@ require_once($CFG->libdir . '/outputrequirementslib.php');
* @copyright 2012 Petr Škoda
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class outputrequirementslib_test extends \advanced_testcase {
+final class outputrequirementslib_test extends \advanced_testcase {
public function test_string_for_js(): void {
$this->resetAfterTest();
diff --git a/mod/wiki/parser/utils.php b/mod/wiki/parser/utils.php
index f489ffd4360..cb65a369bff 100644
--- a/mod/wiki/parser/utils.php
+++ b/mod/wiki/parser/utils.php
@@ -9,8 +9,6 @@
* @package mod_wiki
*/
-require_once($CFG->dirroot . "/lib/outputcomponents.php");
-
class parser_utils {
public static function h($tag, $text = null, $options = array(), $escape_text = false) {
@@ -95,4 +93,3 @@ class parser_utils {
return $url;
}
}
-