From b6ad85946dfc44df457cd1c1bdcf6d6ef3874589 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 1 Mar 2012 21:38:33 +0100 Subject: [PATCH 01/36] MDL-20438 Refactoring pluginlib before adding anything new These changes are motivated by feedback from Tim Hunt (17 October 2011 in the developers chat room). The combination of using an interface, but then putting all the useful information in public fields, was not good design of this library. So I am merging the plugin_information interface and the plugintype_base class into a new abstract class plugininfo_base. This new class defines the basic API of all information holding subclasses. This does not change the code, just puts the documentation closer to the methods they are actually implemented. --- admin/renderer.php | 4 +- lib/pluginlib.php | 418 ++++++++++++++------------------------------- 2 files changed, 128 insertions(+), 294 deletions(-) diff --git a/admin/renderer.php b/admin/renderer.php index cecd725de9f..fa3b23717b3 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -594,12 +594,12 @@ class core_admin_renderer extends plugin_renderer_base { /** * Formats the information that needs to go in the 'Requires' column. - * @param plugin_information $plugin the plugin we are rendering the row for. + * @param plugininfo_base $plugin the plugin we are rendering the row for. * @param plugin_manager $pluginman provides data on all the plugins. * @param string $version * @return string HTML code */ - protected function required_column(plugin_information $plugin, plugin_manager $pluginman, $version) { + protected function required_column(plugininfo_base $plugin, plugin_manager $pluginman, $version) { $requires = array(); if (!empty($plugin->versionrequires)) { diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 06fa73e10dd..f52842bb2e7 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -1,4 +1,5 @@ pluginsinfo[$plugintype] = $plugins; @@ -219,7 +220,7 @@ class plugin_manager { /** * @param string $component frankenstyle component name. - * @return plugin_information|null the corresponding plugin information. + * @return plugininfo_base|null the corresponding plugin information. */ public function get_plugin_info($component) { list($type, $name) = normalize_component($component); @@ -232,7 +233,7 @@ class plugin_manager { } /** - * Get a list of any other pluings that require this one. + * Get a list of any other plugins that require this one. * @param string $component frankenstyle component name. * @return array of frankensyle component names that require this one. */ @@ -510,163 +511,13 @@ class plugin_manager { } } + /** - * Interface for making information about a plugin available. - * - * Note that most of the useful information is made available in pubic fields, - * which cannot be documented in this interface. See the field definitions on - * {@link plugintype_base} to find out what information is available. + * Base class providing access to the information about a plugin * * @property-read string component the component name, type_name */ -interface plugin_information { - - /** - * Gathers and returns the information about all plugins of the given type - * - * Passing the parameter $typeclass allows us to reach the same effect as with the - * late binding in PHP 5.3. Once PHP 5.3 is required, we can refactor this to use - * {@example $plugin = new static();} instead of {@example $plugin = new $typeclass()} - * - * @param string $type the name of the plugintype, eg. mod, auth or workshopform - * @param string $typerootdir full path to the location of the plugin dir - * @param string $typeclass the name of the actually called class - * @return array of plugintype classes, indexed by the plugin name - */ - public static function get_plugins($type, $typerootdir, $typeclass); - - /** - * Sets $displayname property to a localized name of the plugin - * - * @return void - */ - public function init_display_name(); - - /** - * Sets $versiondisk property to a numerical value representing the - * version of the plugin's source code. - * - * If the value is null after calling this method, either the plugin - * does not use versioning (typically does not have any database - * data) or is missing from disk. - * - * @return void - */ - public function load_disk_version(); - - /** - * Sets $versiondb property to a numerical value representing the - * currently installed version of the plugin. - * - * If the value is null after calling this method, either the plugin - * does not use versioning (typically does not have any database - * data) or has not been installed yet. - * - * @return void - */ - public function load_db_version(); - - /** - * Sets $versionrequires property to a numerical value representing - * the version of Moodle core that this plugin requires. - * - * @return void - */ - public function load_required_main_version(); - - /** - * Sets $source property to one of plugin_manager::PLUGIN_SOURCE_xxx - * constants. - * - * If the property's value is null after calling this method, then - * the type of the plugin has not been recognized and you should throw - * an exception. - * - * @return void - */ - public function init_is_standard(); - - /** - * Returns true if the plugin is shipped with the official distribution - * of the current Moodle version, false otherwise. - * - * @return bool - */ - public function is_standard(); - - /** - * Returns the status of the plugin - * - * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants - */ - public function get_status(); - - /** - * Get the list of other plugins that this plugin requires ot be installed. - * @return array with keys the frankenstyle plugin name, and values either - * a version string (like '2011101700') or the constant ANY_VERSION. - */ - public function get_other_required_plugins(); - - /** - * Returns the information about plugin availability - * - * True means that the plugin is enabled. False means that the plugin is - * disabled. Null means that the information is not available, or the - * plugin does not support configurable availability or the availability - * can not be changed. - * - * @return null|bool - */ - public function is_enabled(); - - /** - * Returns the URL of the plugin settings screen - * - * Null value means that the plugin either does not have the settings screen - * or its location is not available via this library. - * - * @return null|moodle_url - */ - public function get_settings_url(); - - /** - * Returns the URL of the screen where this plugin can be uninstalled - * - * Visiting that URL must be safe, that is a manual confirmation is needed - * for actual uninstallation of the plugin. Null value means that the - * plugin either does not support uninstallation, or does not require any - * database cleanup or the location of the screen is not available via this - * library. - * - * @return null|moodle_url - */ - public function get_uninstall_url(); - - /** - * Returns relative directory of the plugin with heading '/' - * - * @example /mod/workshop - * @return string - */ - public function get_dir(); - - /** - * Return the full path name of a file within the plugin. - * No check is made to see if the file exists. - * @param string $relativepath e.g. 'version.php'. - * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'. - */ - public function full_path($relativepath); -} - -/** - * Defines public properties that all plugintype classes must have - * and provides default implementation of required methods. - * - * @property-read string component the component name, type_name - */ -abstract class plugintype_base { +abstract class plugininfo_base { /** @var string the plugintype name, eg. mod, auth or workshopform */ public $type; @@ -686,16 +537,24 @@ abstract class plugintype_base { public $versiondb; /** @var int|float|string required version of Moodle core */ public $versionrequires; - /** @var array other plugins that this one depends on. - * Lazy-loaded by {@link get_other_required_plugins()} */ - public $dependencies = null; + /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */ + public $dependencies; /** @var int number of instances of the plugin - not supported yet */ public $instances; /** @var int order of the plugin among other plugins of the same type - not supported yet */ public $sortorder; /** - * @see plugin_information::get_plugins() + * Gathers and returns the information about all plugins of the given type + * + * Passing the parameter $typeclass allows us to reach the same effect as with the + * late binding in PHP 5.3. Once PHP 5.3 is required, we can refactor this to use + * {@example $plugin = new static();} instead of {@example $plugin = new $typeclass()} + * + * @param string $type the name of the plugintype, eg. mod, auth or workshopform + * @param string $typerootdir full path to the location of the plugin dir + * @param string $typeclass the name of the actually called class + * @return array of plugintype classes, indexed by the plugin name */ public static function get_plugins($type, $typerootdir, $typeclass) { @@ -721,7 +580,7 @@ abstract class plugintype_base { } /** - * @see plugin_information::init_display_name() + * Sets {@link $displayname} property to a localized name of the plugin */ public function init_display_name() { if (!get_string_manager()->string_exists('pluginname', $this->component)) { @@ -733,6 +592,7 @@ abstract class plugintype_base { /** * Magic method getter, redirects to read only values. + * * @param string $name * @return mixed */ @@ -747,7 +607,12 @@ abstract class plugintype_base { } /** - * @see plugin_information::full_path() + * Return the full path name of a file within the plugin. + * + * No check is made to see if the file exists. + * + * @param string $relativepath e.g. 'version.php'. + * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'. */ public function full_path($relativepath) { if (empty($this->rootdir)) { @@ -758,7 +623,8 @@ abstract class plugintype_base { /** * Load the data from version.php. - * @return object the data object defined in version.php. + * + * @return stdClass the object called $plugin defined in version.php */ protected function load_version_php() { $versionfile = $this->full_path('version.php'); @@ -771,7 +637,12 @@ abstract class plugintype_base { } /** - * @see plugin_information::load_disk_version() + * Sets {@link $versiondisk} property to a numerical value representing the + * version of the plugin's source code. + * + * If the value is null after calling this method, either the plugin + * does not use versioning (typically does not have any database + * data) or is missing from disk. */ public function load_disk_version() { $plugin = $this->load_version_php(); @@ -781,7 +652,8 @@ abstract class plugintype_base { } /** - * @see plugin_information::load_required_main_version() + * Sets {@link $versionrequires} property to a numerical value representing + * the version of Moodle core that this plugin requires. */ public function load_required_main_version() { $plugin = $this->load_version_php(); @@ -804,7 +676,10 @@ abstract class plugintype_base { } /** - * @see plugin_information::get_other_required_plugins() + * Get the list of other plugins that this plugin requires to be installed. + * + * @return array with keys the frankenstyle plugin name, and values either + * a version string (like '2011101700') or the constant ANY_VERSION. */ public function get_other_required_plugins() { if (is_null($this->dependencies)) { @@ -814,17 +689,26 @@ abstract class plugintype_base { } /** - * @see plugin_information::load_db_version() + * Sets {@link $versiondb} property to a numerical value representing the + * currently installed version of the plugin. + * + * If the value is null after calling this method, either the plugin + * does not use versioning (typically does not have any database + * data) or has not been installed yet. */ public function load_db_version() { - if ($ver = self::get_version_from_config_plugins($this->component)) { $this->versiondb = $ver; } } /** - * @see plugin_information::init_is_standard() + * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx + * constants. + * + * If the property's value is null after calling this method, then + * the type of the plugin has not been recognized and you should throw + * an exception. */ public function init_is_standard() { @@ -844,14 +728,19 @@ abstract class plugintype_base { } /** - * @see plugin_information::is_standard() + * Returns true if the plugin is shipped with the official distribution + * of the current Moodle version, false otherwise. + * + * @return bool */ public function is_standard() { return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD; } /** - * @see plugin_information::get_status() + * Returns the status of the plugin + * + * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants */ public function get_status() { @@ -884,28 +773,50 @@ abstract class plugintype_base { } /** - * @see plugin_information::is_enabled() + * Returns the information about plugin availability + * + * True means that the plugin is enabled. False means that the plugin is + * disabled. Null means that the information is not available, or the + * plugin does not support configurable availability or the availability + * can not be changed. + * + * @return null|bool */ public function is_enabled() { return null; } /** - * @see plugin_information::get_settings_url() + * Returns the URL of the plugin settings screen + * + * Null value means that the plugin either does not have the settings screen + * or its location is not available via this library. + * + * @return null|moodle_url */ public function get_settings_url() { return null; } /** - * @see plugin_information::get_uninstall_url() + * Returns the URL of the screen where this plugin can be uninstalled + * + * Visiting that URL must be safe, that is a manual confirmation is needed + * for actual uninstallation of the plugin. Null value means that the + * plugin either does not support uninstallation, or does not require any + * database cleanup or the location of the screen is not available via this + * library. + * + * @return null|moodle_url */ public function get_uninstall_url() { return null; } /** - * @see plugin_information::get_dir() + * Returns relative directory of the plugin with heading '/' + * + * @return string */ public function get_dir() { global $CFG; @@ -941,21 +852,19 @@ abstract class plugintype_base { } } + /** * General class for all plugin types that do not have their own class */ -class plugintype_general extends plugintype_base implements plugin_information { - +class plugininfo_general extends plugininfo_base { } + /** * Class for page side blocks */ -class plugintype_block extends plugintype_base implements plugin_information { +class plugininfo_block extends plugininfo_base { - /** - * @see plugin_information::get_plugins() - */ public static function get_plugins($type, $typerootdir, $typeclass) { // get the information about blocks at the disk @@ -982,9 +891,6 @@ class plugintype_block extends plugintype_base implements plugin_information { return $blocks; } - /** - * @see plugin_information::init_display_name() - */ public function init_display_name() { if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) { @@ -998,9 +904,6 @@ class plugintype_block extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::load_db_version() - */ public function load_db_version() { global $DB; @@ -1010,9 +913,6 @@ class plugintype_block extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { $blocksinfo = self::get_blocks_info(); @@ -1027,9 +927,6 @@ class plugintype_block extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::get_settings_url() - */ public function get_settings_url() { if (($block = block_instance($this->name)) === false) { @@ -1048,9 +945,6 @@ class plugintype_block extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::get_uninstall_url() - */ public function get_uninstall_url() { $blocksinfo = self::get_blocks_info(); @@ -1080,14 +974,12 @@ class plugintype_block extends plugintype_base implements plugin_information { } } + /** * Class for text filters */ -class plugintype_filter extends plugintype_base implements plugin_information { +class plugininfo_filter extends plugininfo_base { - /** - * @see plugin_information::get_plugins() - */ public static function get_plugins($type, $typerootdir, $typeclass) { global $CFG, $DB; @@ -1155,15 +1047,12 @@ class plugintype_filter extends plugintype_base implements plugin_information { return $filters; } - /** - * @see plugin_information::init_display_name() - */ public function init_display_name() { // do nothing, the name is set in self::get_plugins() } /** - * @see plugintype_base::load_version_php(). + * @see load_version_php() */ protected function load_version_php() { if (strpos($this->name, 'mod_') === 0) { @@ -1174,9 +1063,6 @@ class plugintype_filter extends plugintype_base implements plugin_information { return parent::load_version_php(); } - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { $globalstates = self::get_global_states(); @@ -1196,9 +1082,6 @@ class plugintype_filter extends plugintype_base implements plugin_information { return null; } - /** - * @see plugin_information::get_settings_url() - */ public function get_settings_url() { $globalstates = self::get_global_states(); @@ -1210,9 +1093,6 @@ class plugintype_filter extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::get_uninstall_url() - */ public function get_uninstall_url() { if (strpos($this->name, 'mod_') === 0) { @@ -1279,14 +1159,12 @@ class plugintype_filter extends plugintype_base implements plugin_information { } } + /** * Class for activity modules */ -class plugintype_mod extends plugintype_base implements plugin_information { +class plugininfo_mod extends plugininfo_base { - /** - * @see plugin_information::get_plugins() - */ public static function get_plugins($type, $typerootdir, $typeclass) { // get the information about plugins at the disk @@ -1313,9 +1191,6 @@ class plugintype_mod extends plugintype_base implements plugin_information { return $modules; } - /** - * @see plugin_information::init_display_name() - */ public function init_display_name() { if (get_string_manager()->string_exists('pluginname', $this->component)) { $this->displayname = get_string('pluginname', $this->component); @@ -1338,9 +1213,6 @@ class plugintype_mod extends plugintype_base implements plugin_information { return $module; } - /** - * @see plugin_information::load_db_version() - */ public function load_db_version() { global $DB; @@ -1350,9 +1222,6 @@ class plugintype_mod extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { $modulesinfo = self::get_modules_info(); @@ -1367,9 +1236,6 @@ class plugintype_mod extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::get_settings_url() - */ public function get_settings_url() { if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) { @@ -1379,9 +1245,6 @@ class plugintype_mod extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::get_uninstall_url() - */ public function get_uninstall_url() { if ($this->name !== 'forum') { @@ -1418,10 +1281,8 @@ class plugintype_mod extends plugintype_base implements plugin_information { /** * Class for question behaviours. */ -class plugintype_qbehaviour extends plugintype_base implements plugin_information { - /** - * @see plugin_information::get_uninstall_url() - */ +class plugininfo_qbehaviour extends plugininfo_base { + public function get_uninstall_url() { return new moodle_url('/admin/qbehaviours.php', array('delete' => $this->name, 'sesskey' => sesskey())); @@ -1432,10 +1293,8 @@ class plugintype_qbehaviour extends plugintype_base implements plugin_informatio /** * Class for question types */ -class plugintype_qtype extends plugintype_base implements plugin_information { - /** - * @see plugin_information::get_uninstall_url() - */ +class plugininfo_qtype extends plugininfo_base { + public function get_uninstall_url() { return new moodle_url('/admin/qtypes.php', array('delete' => $this->name, 'sesskey' => sesskey())); @@ -1446,11 +1305,8 @@ class plugintype_qtype extends plugintype_base implements plugin_information { /** * Class for authentication plugins */ -class plugintype_auth extends plugintype_base implements plugin_information { +class plugininfo_auth extends plugininfo_base { - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { global $CFG; /** @var null|array list of enabled authentication plugins */ @@ -1468,9 +1324,6 @@ class plugintype_auth extends plugintype_base implements plugin_information { return isset($enabled[$this->name]); } - /** - * @see plugin_information::get_settings_url() - */ public function get_settings_url() { if (file_exists($this->full_path('settings.php'))) { return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name)); @@ -1480,24 +1333,22 @@ class plugintype_auth extends plugintype_base implements plugin_information { } } + /** * Class for enrolment plugins */ -class plugintype_enrol extends plugintype_base implements plugin_information { +class plugininfo_enrol extends plugininfo_base { - /** - * We do not actually need whole enrolment classes here so we do not call - * {@link enrol_get_plugins()}. Note that this may produce slightly different - * results, for example if the enrolment plugin does not contain lib.php - * but it is listed in $CFG->enrol_plugins_enabled - * - * @see plugin_information::is_enabled() - */ public function is_enabled() { global $CFG; /** @var null|array list of enabled enrolment plugins */ static $enabled = null; + // We do not actually need whole enrolment classes here so we do not call + // {@link enrol_get_plugins()}. Note that this may produce slightly different + // results, for example if the enrolment plugin does not contain lib.php + // but it is listed in $CFG->enrol_plugins_enabled + if (is_null($enabled)) { $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled)); } @@ -1505,9 +1356,6 @@ class plugintype_enrol extends plugintype_base implements plugin_information { return isset($enabled[$this->name]); } - /** - * @see plugin_information::get_settings_url() - */ public function get_settings_url() { if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) { @@ -1517,22 +1365,17 @@ class plugintype_enrol extends plugintype_base implements plugin_information { } } - /** - * @see plugin_information::get_uninstall_url() - */ public function get_uninstall_url() { return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey())); } } + /** * Class for messaging processors */ -class plugintype_message extends plugintype_base implements plugin_information { +class plugininfo_message extends plugininfo_base { - /** - * @see plugin_information::get_settings_url() - */ public function get_settings_url() { if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) { @@ -1543,14 +1386,12 @@ class plugintype_message extends plugintype_base implements plugin_information { } } + /** * Class for repositories */ -class plugintype_repository extends plugintype_base implements plugin_information { +class plugininfo_repository extends plugininfo_base { - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { $enabled = self::get_enabled_repositories(); @@ -1558,9 +1399,6 @@ class plugintype_repository extends plugintype_base implements plugin_informatio return isset($enabled[$this->name]); } - /** - * @see plugin_information::get_settings_url() - */ public function get_settings_url() { if ($this->is_enabled()) { @@ -1588,14 +1426,12 @@ class plugintype_repository extends plugintype_base implements plugin_informatio } } + /** * Class for portfolios */ -class plugintype_portfolio extends plugintype_base implements plugin_information { +class plugininfo_portfolio extends plugininfo_base { - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { $enabled = self::get_enabled_portfolios(); @@ -1631,14 +1467,12 @@ class plugintype_portfolio extends plugintype_base implements plugin_information } } + /** * Class for themes */ -class plugintype_theme extends plugintype_base implements plugin_information { +class plugininfo_theme extends plugininfo_base { - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { global $CFG; @@ -1651,14 +1485,12 @@ class plugintype_theme extends plugintype_base implements plugin_information { } } + /** * Class representing an MNet service */ -class plugintype_mnetservice extends plugintype_base implements plugin_information { +class plugininfo_mnetservice extends plugininfo_base { - /** - * @see plugin_information::is_enabled() - */ public function is_enabled() { global $CFG; @@ -1670,20 +1502,22 @@ class plugintype_mnetservice extends plugintype_base implements plugin_informati } } + /** * Class for admin tool plugins */ -class plugintype_tool extends plugintype_base implements plugin_information { +class plugininfo_tool extends plugininfo_base { public function get_uninstall_url() { return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey())); } } + /** * Class for admin tool plugins */ -class plugintype_report extends plugintype_base implements plugin_information { +class plugininfo_report extends plugininfo_base { public function get_uninstall_url() { return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey())); From d26f3ddde7a530571febce634faacb74c58312a2 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 1 Mar 2012 23:37:56 +0100 Subject: [PATCH 02/36] MDL-20438 New plugins overview panel displaying some stats --- admin/renderer.php | 41 ++++++++++++++++++++++++++++++++++++++ lang/en/plugin.php | 4 ++++ lib/pluginlib.php | 13 ++++++++++++ theme/base/style/admin.css | 4 ++++ 4 files changed, 62 insertions(+) diff --git a/admin/renderer.php b/admin/renderer.php index fa3b23717b3..8477bd060e4 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -236,6 +236,7 @@ class core_admin_renderer extends plugin_renderer_base { $output .= $this->header(); $output .= $this->heading(get_string('pluginsoverview', 'core_admin')); + $output .= $this->plugins_overview_panel($pluginman); $output .= $this->box_start('generalbox'); $output .= $this->plugins_control_panel($pluginman); $output .= $this->box_end(); @@ -646,6 +647,46 @@ class core_admin_renderer extends plugin_renderer_base { return html_writer::tag('ul', implode("\n", $requires)); } + /** + * Prints an overview about the plugins - number of installed, number of extensions etc. + * + * @param plugin_manager $pluginman provides information about the plugins + * @return string as usually + */ + public function plugins_overview_panel(plugin_manager $pluginman) { + $plugininfo = $pluginman->get_plugins(); + + $numtotal = $numdisabled = $numextension = $numupdatable = 0; + + foreach ($plugininfo as $type => $plugins) { + foreach ($plugins as $name => $plugin) { + if ($plugin->get_status() === plugin_manager::PLUGIN_STATUS_MISSING) { + continue; + } + $numtotal++; + if ($plugin->is_enabled() === false) { + $numdisabled++; + } + if (!$plugin->is_standard()) { + $numextension++; + } + if ($plugin->available_update()) { + $numupdatable++; + } + } + } + + $info = array(); + $info[] = html_writer::tag('span', get_string('numtotal', 'core_plugin', $numtotal), array('class' => 'info total')); + $info[] = html_writer::tag('span', get_string('numdisabled', 'core_plugin', $numdisabled), array('class' => 'info disabled')); + $info[] = html_writer::tag('span', get_string('numextension', 'core_plugin', $numextension), array('class' => 'info extension')); + if ($numupdatable > 0) { + $info[] = html_writer::tag('span', get_string('numupdatable', 'core_plugin', $numupdatable), array('class' => 'info updatable')); + } + + return $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '', 'plugins-overview-panel'); + } + /** * Displays all known plugins and links to manage them * diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 6b062117a61..fbb72a1c13e 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -31,6 +31,10 @@ $string['moodleversion'] = 'Moodle {$a}'; $string['nonehighlighted'] = 'No plugins require your attention during this upgrade'; $string['nonehighlightedinfo'] = 'Display the list of all installed plugins anyway'; $string['noneinstalled'] = 'No plugins of this type are installed'; +$string['numtotal'] = '{$a} installed'; +$string['numdisabled'] = '{$a} disabled'; +$string['numextension'] = '{$a} extensions'; +$string['numupdatable'] = '{$a} updates available'; $string['otherplugin'] = '{$a->component}'; $string['otherpluginversion'] = '{$a->component} ({$a->version})'; $string['showall'] = 'Reload and show all plugins'; diff --git a/lib/pluginlib.php b/lib/pluginlib.php index f52842bb2e7..f69c881e8c1 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -786,6 +786,19 @@ abstract class plugininfo_base { return null; } + /** + * If there is an update of this plugin available, returns the data about it. + * + * Returns object with various properties about the available update, if such + * an update is available. Returns false if there is no update available for + * this plugin. Returns null if the update availabitlity is unknown. + * + * @return stdClass|false|null + */ + public function available_update() { + return null; + } + /** * Returns the URL of the plugin settings screen * diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index 64017d4360e..4e0336a6eef 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -206,6 +206,10 @@ #page-admin-plugins #plugins-control-panel .extension .source {background-color:#f3f2aa;} #page-admin-plugins #plugins-control-panel .msg td {text-align:center;} #page-admin-plugins #plugins-control-panel .requiredby {font-size:0.7em;color:#999;} +#page-admin-plugins #plugins-overview-panel {margin:1em auto;text-align:center;} +#page-admin-plugins #plugins-overview-panel .info {padding:5px 10px;} +#page-admin-plugins #plugins-overview-panel .separator {border-left:1px dotted #999;} +#page-admin-plugins #plugins-overview-panel .info.updatable {margin-left:10px;background-color:#d2ebff;font-weight:bold;-moz-border-radius: 10px;-webkit-border-radius: 10px;border-radius: 10px;} /** MNet networking */ #page-admin-mnet-peers .box.deletedhosts {margin-bottom:1em;font-size:80%;} From 4ed266809c2ee2f3e39226f325f96a0626728cdb Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 2 Mar 2012 22:03:58 +0100 Subject: [PATCH 03/36] MDL-20438 Change the order of plugin types at the overview pages --- lib/pluginlib.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index f69c881e8c1..52a42333778 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -104,6 +104,7 @@ class plugin_manager { if ($disablecache or is_null($this->pluginsinfo)) { $this->pluginsinfo = array(); $plugintypes = get_plugin_types(); + $plugintypes = $this->reorder_plugin_types($plugintypes); foreach ($plugintypes as $plugintype => $plugintyperootdir) { if (in_array($plugintype, array('base', 'general'))) { throw new coding_exception('Illegal usage of reserved word for plugin type'); @@ -509,6 +510,37 @@ class plugin_manager { return false; } } + + /** + * Reordes plugin types into a sequence to be displayed + * + * For technical reasons, plugin types returned by {@link get_plugin_types()} are + * in a certain order that does not need to fit the expected order for the display. + * Particularly, activity modules should be displayed first as they represent the + * real heart of Moodle. They should be followed by other plugin types that are + * used to build the courses (as that is what one expects from LMS). After that, + * other supportive plugin types follow. + * + * @param array $types associative array + * @return array same array with altered order of items + */ + protected function reorder_plugin_types(array $types) { + $fix = array( + 'mod' => $types['mod'], + 'block' => $types['block'], + 'qtype' => $types['qtype'], + 'qbehaviour' => $types['qbehaviour'], + 'qformat' => $types['qformat'], + 'filter' => $types['filter'], + 'enrol' => $types['enrol'], + ); + foreach ($types as $type => $path) { + if (!isset($fix[$type])) { + $fix[$type] = $path; + } + } + return $fix; + } } From 2b135b05d8afad1c2ec3920000c75009ec557ad3 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 2 Mar 2012 22:56:35 +0100 Subject: [PATCH 04/36] MDL-20438 Making the plugins overview page space-friendly --- admin/renderer.php | 48 ++++++++++++++++++++------------------ lang/en/plugin.php | 2 ++ theme/base/style/admin.css | 9 ++++--- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/admin/renderer.php b/admin/renderer.php index 8477bd060e4..c74d6413807 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -706,15 +706,14 @@ class core_admin_renderer extends plugin_renderer_base { $table->id = 'plugins-control-panel'; $table->head = array( get_string('displayname', 'core_plugin'), - get_string('systemname', 'core_plugin'), get_string('source', 'core_plugin'), get_string('version', 'core_plugin'), get_string('availability', 'core_plugin'), - get_string('settings', 'core_plugin'), - get_string('uninstall','core_plugin'), + get_string('actions', 'core_plugin'), + get_string('notes','core_plugin'), ); $table->colclasses = array( - 'displayname', 'systemname', 'source', 'version', 'availability', 'settings', 'uninstall', + 'pluginname', 'source', 'version', 'availability', 'actions', 'notes' ); foreach ($plugininfo as $type => $plugins) { @@ -750,10 +749,9 @@ class core_admin_renderer extends plugin_renderer_base { } else { $msg = ''; } - $displayname = $icon . ' ' . $plugin->displayname . ' ' . $msg; - $displayname = new html_table_cell($displayname); - - $systemname = new html_table_cell($plugin->type . '_' . $plugin->name); + $pluginname = html_writer::tag('div', $icon . ' ' . $plugin->displayname . ' ' . $msg, array('class' => 'displayname')). + html_writer::tag('div', $plugin->component, array('class' => 'componentname')); + $pluginname = new html_table_cell($pluginname); if ($plugin->is_standard()) { $row->attributes['class'] .= ' standard'; @@ -778,28 +776,32 @@ class core_admin_renderer extends plugin_renderer_base { $availability = new html_table_cell($icon . ' ' . get_string('plugindisabled', 'core_plugin')); } + $actions = array(); + $settingsurl = $plugin->get_settings_url(); - if (is_null($settingsurl)) { - $settings = new html_table_cell(''); - } else { - $settings = html_writer::link($settingsurl, get_string('settings', 'core_plugin')); - $settings = new html_table_cell($settings); + if (!is_null($settingsurl)) { + $actions[] = html_writer::link($settingsurl, get_string('settings', 'core_plugin'), array('class' => 'settings')); } $uninstallurl = $plugin->get_uninstall_url(); - $requriedby = $pluginman->other_plugins_that_require($plugin->component); - if (is_null($uninstallurl)) { - $uninstall = new html_table_cell(''); - } else if ($requriedby) { - $uninstall = new html_table_cell(get_string('requiredby', 'core_plugin', implode(', ', $requriedby))); - $uninstall->attributes['class'] = 'requiredby'; - } else { - $uninstall = html_writer::link($uninstallurl, get_string('uninstall', 'core_plugin')); - $uninstall = new html_table_cell($uninstall); + if (!is_null($uninstallurl)) { + $actions[] = html_writer::link($uninstallurl, get_string('uninstall', 'core_plugin'), array('class' => 'uninstall')); } + $actions = new html_table_cell(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $actions)); + + $requriedby = $pluginman->other_plugins_that_require($plugin->component); + if ($requriedby) { + $requiredby = html_writer::tag('div', get_string('requiredby', 'core_plugin', implode(', ', $requriedby)), + array('class' => 'requiredby')); + } else { + $requiredby = ''; + } + + $notes = new html_table_cell($requiredby); + $row->cells = array( - $displayname, $systemname, $source, $version, $availability, $settings, $uninstall + $pluginname, $source, $version, $availability, $actions, $notes ); $table->data[] = $row; } diff --git a/lang/en/plugin.php b/lang/en/plugin.php index fbb72a1c13e..ed3e21d979e 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -25,12 +25,14 @@ defined('MOODLE_INTERNAL') || die(); +$string['actions'] = 'Actions'; $string['availability'] = 'Availability'; $string['displayname'] = 'Plugin name'; $string['moodleversion'] = 'Moodle {$a}'; $string['nonehighlighted'] = 'No plugins require your attention during this upgrade'; $string['nonehighlightedinfo'] = 'Display the list of all installed plugins anyway'; $string['noneinstalled'] = 'No plugins of this type are installed'; +$string['notes'] = 'Notes'; $string['numtotal'] = '{$a} installed'; $string['numdisabled'] = '{$a} disabled'; $string['numextension'] = '{$a} extensions'; diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index 4e0336a6eef..5fabf4f243a 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -200,12 +200,15 @@ /** Plugins management */ #page-admin-plugins #plugins-control-panel {margin-left:auto; margin-right:auto;} -#page-admin-plugins #plugins-control-panel .displayname .pluginicon {width:16px;} -#page-admin-plugins #plugins-control-panel .missingfromdisk .displayname {background-color:#ffd3d9;} +#page-admin-plugins #plugins-control-panel .pluginname .pluginicon {width:16px;} +#page-admin-plugins #plugins-control-panel .pluginname .componentname {font-size:0.8em;color:#999;margin-left:26px;} +#page-admin-plugins #plugins-control-panel .missingfromdisk .pluginname {background-color:#ffd3d9;} #page-admin-plugins #plugins-control-panel .disabled .availability {background-color:#eee;} +#page-admin-plugins #plugins-control-panel .actions a {padding:0 10px;} +#page-admin-plugins #plugins-control-panel .actions .separator {border-left:1px dotted #999;} #page-admin-plugins #plugins-control-panel .extension .source {background-color:#f3f2aa;} #page-admin-plugins #plugins-control-panel .msg td {text-align:center;} -#page-admin-plugins #plugins-control-panel .requiredby {font-size:0.7em;color:#999;} +#page-admin-plugins #plugins-control-panel .requiredby {font-size:0.8em;color:#999;} #page-admin-plugins #plugins-overview-panel {margin:1em auto;text-align:center;} #page-admin-plugins #plugins-overview-panel .info {padding:5px 10px;} #page-admin-plugins #plugins-overview-panel .separator {border-left:1px dotted #999;} From 9c26cf70601dd471f8d5fc9ac4734ae58b842ccd Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Sat, 3 Mar 2012 00:41:53 +0100 Subject: [PATCH 05/36] MDL-20438 New moodle_major_version() function We will need to know our major version to fetch the relevant data from remote servers. This patch also fixes yet another place where the version used to be hard-coded. --- lib/componentlib.class.php | 3 +-- lib/moodlelib.php | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/componentlib.class.php b/lib/componentlib.class.php index efddc9dbcba..c10389e5644 100644 --- a/lib/componentlib.class.php +++ b/lib/componentlib.class.php @@ -585,14 +585,13 @@ class lang_installer { /** * Prepare the installer * - * @todo Moodle major version is hardcoded here, should be obtained from version.php or so * @param string|array $langcode a code of the language to install */ public function __construct($langcode = '') { global $CFG; $this->set_queue($langcode); - $this->version = '2.2'; + $this->version = moodle_major_version(true); if (!empty($CFG->langotherroot) and $CFG->langotherroot !== $CFG->dataroot . '/lang') { debugging('The in-built language pack installer does not support alternative location ' . diff --git a/lib/moodlelib.php b/lib/moodlelib.php index fe642a914b6..1f7a154285c 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -8686,6 +8686,42 @@ function moodle_needs_upgrading() { return false; } +/** + * Returns the major version of this site + * + * Moodle version numbers consist of three numbers separated by a dot, for + * example 1.9.11 or 2.0.2. The first two numbers, like 1.9 or 2.0, represent so + * called major version. This function extracts the major version from either + * $CFG->release (default) or eventually from the $release variable defined in + * the main version.php. + * + * @param bool $fromdisk should the version if source code files be used + * @return string|false the major version like '2.3', false if could not be determined + */ +function moodle_major_version($fromdisk = false) { + global $CFG; + + if ($fromdisk) { + $release = null; + require($CFG->dirroot.'/version.php'); + if (empty($release)) { + return false; + } + + } else { + if (empty($CFG->release)) { + return false; + } + $release = $CFG->release; + } + + if (preg_match('/^[0-9]+\.[0-9]+/', $release, $matches)) { + return $matches[0]; + } else { + return false; + } +} + /** * Sets maximum expected time needed for upgrade task. * Please always make sure that upgrade will not run longer! From 00ef3c3ed1fcdac4cf994d550a88729f09ee6384 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 5 Mar 2012 20:24:53 +0100 Subject: [PATCH 06/36] MDL-20438 Introducing plugininfo classes factory and plugin manager unit tests --- lib/pluginlib.php | 52 ++++++++----- lib/simpletest/testpluginlib.php | 127 +++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 lib/simpletest/testpluginlib.php diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 52a42333778..0a0a4d8dc5e 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -544,6 +544,39 @@ class plugin_manager { } +/** + * Factory class producing required subclasses of {@link plugininfo_base} + */ +class plugininfo_default_factory { + + /** + * Makes a new instance of the plugininfo class + * + * @param string $type the plugin type, eg. 'mod' + * @param string $typerootdir full path to the location of all the plugins of this type + * @param string $name the plugin name, eg. 'workshop' + * @param string $namerootdir full path to the location of the plugin + * @param string $typeclass the name of class that holds the info about the plugin + * @return plugininfo_base the instance of $typeclass + */ + public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) { + $plugin = new $typeclass(); + $plugin->type = $type; + $plugin->typerootdir = $typerootdir; + $plugin->name = $name; + $plugin->rootdir = $namerootdir; + + $plugin->init_display_name(); + $plugin->load_disk_version(); + $plugin->load_db_version(); + $plugin->load_required_main_version(); + $plugin->init_is_standard(); + + return $plugin; + } +} + + /** * Base class providing access to the information about a plugin * @@ -579,10 +612,6 @@ abstract class plugininfo_base { /** * Gathers and returns the information about all plugins of the given type * - * Passing the parameter $typeclass allows us to reach the same effect as with the - * late binding in PHP 5.3. Once PHP 5.3 is required, we can refactor this to use - * {@example $plugin = new static();} instead of {@example $plugin = new $typeclass()} - * * @param string $type the name of the plugintype, eg. mod, auth or workshopform * @param string $typerootdir full path to the location of the plugin dir * @param string $typeclass the name of the actually called class @@ -594,19 +623,8 @@ abstract class plugininfo_base { $plugins = get_plugin_list($type); $ondisk = array(); foreach ($plugins as $pluginname => $pluginrootdir) { - $plugin = new $typeclass(); - $plugin->type = $type; - $plugin->typerootdir = $typerootdir; - $plugin->name = $pluginname; - $plugin->rootdir = $pluginrootdir; - - $plugin->init_display_name(); - $plugin->load_disk_version(); - $plugin->load_db_version(); - $plugin->load_required_main_version(); - $plugin->init_is_standard(); - - $ondisk[$pluginname] = $plugin; + $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir, + $pluginname, $pluginrootdir, $typeclass); } return $ondisk; } diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php new file mode 100644 index 00000000000..7c94917ab60 --- /dev/null +++ b/lib/simpletest/testpluginlib.php @@ -0,0 +1,127 @@ +. + +/** + * Unit tests for the lib/pluginlib.php library + * + * @package core + * @category test + * @copyright 2012 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if (empty($CFG->unittestprefix)) { + die('You must define $CFG->unittestprefix to run these unit tests.'); +} + +require_once($CFG->libdir.'/pluginlib.php'); + +/** + * Modified {@link plugininfo_mod} suitable for testing purposes + */ +class testable_plugininfo_mod extends plugininfo_mod { + + public function init_display_name() { + $this->displayname = ucfirst($this->name); + } + + public function load_disk_version() { + $this->versiondisk = 2012030500; + } + + protected function load_version_php() { + return (object)array( + 'version' => 2012030500, + 'requires' => 2012010100, + 'component' => $this->type.'_'.$this->name); + } + + public function load_db_version() { + $this->versiondb = 2012022900; + } +} + + +/** + * Modified {@link plugin_manager} suitable for testing purposes + */ +class testable_plugin_manager extends plugin_manager { + + /** + * Factory method for this class + * + * @return plugin_manager the singleton instance + */ + public static function instance() { + global $CFG; + + if (is_null(self::$singletoninstance)) { + self::$singletoninstance = new self(); + } + return self::$singletoninstance; + } + + /** + * A version of {@link plugin_manager::get_plugins()} that prepares some faked + * testable instances. + * + * @param bool $disablecache ignored in this class + * @return array + */ + public function get_plugins($disablecache=false) { + global $CFG; + + $this->pluginsinfo = array( + 'mod' => array( + 'foo' => plugininfo_default_factory::make('mod', $CFG->dirroot.'/mod', 'foo', + $CFG->dirroot.'/mod/foo', 'testable_plugininfo_mod'), + ) + ); + + return $this->pluginsinfo; + } +} + + +/** + * Test cases for the pluginlib API + * + * These are basic tests to document the basic API of the plugin manager. + */ +class plugin_manager_test extends UnitTestCase { + + public function test_plugin_manager_instance() { + $pluginman = testable_plugin_manager::instance(); + $this->assertTrue($pluginman instanceof testable_plugin_manager); + } + + public function test_get_plugins() { + $pluginman = testable_plugin_manager::instance(); + $plugins = $pluginman->get_plugins(); + $this->assertTrue(isset($plugins['mod']['foo'])); + $this->assertTrue($plugins['mod']['foo'] instanceof testable_plugininfo_mod); + } + + public function test_get_status() { + $pluginman = testable_plugin_manager::instance(); + $plugins = $pluginman->get_plugins(); + $modfoo = $plugins['mod']['foo']; + $this->assertEqual($modfoo->get_status(), plugin_manager::PLUGIN_STATUS_UPGRADE); + } +} From cd0bb55f84b92a328c40bed702e55fd1aca6f7cc Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Wed, 7 Mar 2012 17:42:46 +0100 Subject: [PATCH 07/36] MDL-20438 Introducing new available_update_checker class The purpose of the class is to fetch the information about available updates for the Moodle core and/or Moodle components from a remote site and cache it locally. --- lib/pluginlib.php | 291 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 0a0a4d8dc5e..ea4c7d2403e 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -544,6 +544,297 @@ class plugin_manager { } +/** + * General exception thrown by the {@link available_update_checker} class + */ +class available_update_checker_exception extends moodle_exception { + + /** + * @param string $errorcode exception description identifier + * @param mixed $debuginfo debugging data to display + */ + public function __construct($errorcode, $debuginfo=null) { + parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true)); + } +} + + +/** + * Singleton class that handles checking for available updates + */ +class available_update_checker { + + /** @var available_update_checker holds the singleton instance */ + protected static $singletoninstance; + /** @var null|string numerical version of the local Moodle site */ + protected $localversion = null; + /** @var null|string release signature of the local Moodle site */ + protected $localrelease = null; + /** @var array of (string)frankestyle => (string)version list of plugins installed at the local Moodle site */ + protected $localplugins = array(); + /** @var null|stdClass */ + protected $config = null; + + /** + * Direct initiation not allowed, use the factory method {@link self::instance()} + */ + protected function __construct() { + $this->load_config(); + } + + /** + * Sorry, this is singleton + */ + protected function __clone() { + } + + /** + * Factory method for this class + * + * @return available_update_checker the singleton instance + */ + public static function instance() { + if (is_null(self::$singletoninstance)) { + self::$singletoninstance = new self(); + } + return self::$singletoninstance; + } + + /** + * Sets the local version + * + * If the version is set before the request is done, the version info will + * be sent to the remote site as a part of the request. The returned data can + * be filtered so that they contain just information relevant to the sent + * version. + * + * @param string $version our local Moodle version, usually $CFG->version + */ + public function set_local_version($version) { + $this->localversion = $version; + } + + /** + * Sets the local release info + * + * If the release is set before the request is done, the release info will + * be sent to the remote site as a part of the request. The returned data can + * be filtered so that they contain just information relevant to the sent + * release/build. + * + * @param string $version our local Moodle version, usually $CFG->release + */ + public function set_local_release($release) { + $this->localrelease = $release; + } + + /** + * Sets the list of plugins and their version at the local Moodle site + * + * The keys of the passed array are frankenstyle component names of plugins. The + * values are the on-disk versions of these plugins (allowing the null value where + * the version can't be obtained for any reason). + * If the plugins are set before the request is done, their list will be sent + * to the remote site as a part of the request. The returned data can + * be filtered so that they contain just information about the installed plugins. + * To obtain a list of all available plugins, do not set this list prior to calling + * {@link self::fetch()} + * + * @param array $plugins of (string)component => (string)version + */ + public function set_local_plugins(array $plugins) { + $this->localplugins = $plugins; + } + + /** + * Returns the timestamp of the last execution of {@link fetch()} + * + * @return int|null null if it has never been executed or we don't known + */ + public function get_last_timefetched() { + if (isset($this->config->timelastfetched)) { + return $this->config->timelastfetched; + } else { + return false; + } + } + + /** + * Fetches the available update status from the remote site + * + * @throws available_update_checker_exception + */ + public function fetch() { + $response = $this->make_request(); + $this->validate_response($response); + $this->process_response($response); + } + + /** + * Returns the available update information for the given component + * + * This method returns null if the most recent response does not contain any information + * about it. Note that this does not mean that the information is not provided by the + * remote site. The recent request might be specific + * + * @param string $component frankenstyle + * @return null|stdClass null if the most recent response does not provide any info + */ + public function get_update_info($component) { + + $branch = moodle_major_version(); + + if (!empty($this->config->components->$branch->$component)) { + return $this->config->components->$branch->$component; + } else { + return null; + } + } + + /** + * Executes cURL request to get data from the remote site + * + * @return stdClass request result + * @throws available_update_checker_exception + */ + protected function make_request() { + $curl = new curl(array('proxy' => true)); + $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params()); + $curlinfo = $curl->get_info(); + if ($curlinfo['http_code'] != 200) { + throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']); + } + $response = json_decode($response); + return $response; + } + + /** + * Makes sure the response is valid, has correct API format etc. + * + * @param stdClass $response + * @throws available_update_checker_exception + */ + protected function validate_response(stdClass $response) { + + if (empty($response)) { + throw new available_update_checker_exception('err_response_empty'); + } + + if (empty($response->status) or $response->status !== 'OK') { + throw new available_update_checker_exception('err_response_status', $response->status); + } + + if (empty($response->apiver) or $response->apiver != '1.0') { + throw new available_update_checker_exception('err_response_format_version', $response->apiver); + } + } + + /** + * Stores the fetched response for later usage + * + * This implementation uses the config_plugins table as the permanent storage. + * + * @param stdClass $response the data returned by the updates info provider + */ + protected function process_response(stdClass $response) { + + $components = $this->merge_components_info($this->config->components, $response->components, $response->timegenerated); + + set_config('timelastfetched', time(), 'core_plugin'); + set_config('ticket', $response->ticket, 'core_plugin'); + set_config('components', json_encode($components), 'core_plugin'); + + $this->load_config(true); + } + + /** + * Merges the current and the new info about the available updates + * + * @param stdClass $old + * @param stdClass $new + * @param int $timegenerated the timestamp of when the $new was generated + * @return stdClass merged + */ + protected function merge_components_info(stdClass $old, stdClass $new, $timegenerated=null) { + $merged = clone($old); + if (is_null($timegenerated)) { + $timegenerated = time(); + } + foreach ($new as $branch => $components) { + foreach ($components as $component => $info) { + $info->timegenerated = $timegenerated; + if (isset($info->version)) { + $merged->$branch->$component = $info; + } + } + } + return $merged; + } + + /** + * Loads the core_plugin subsystem config + * + * @param bool $forcereload reload the config even if it was already loaded + */ + protected function load_config($forcereload = false) { + if ($forcereload or is_null($this->config)) { + $this->config = get_config('core_plugin'); + } + + if (empty($this->config->components)) { + $this->config->components = new stdClass(); + } else { + $this->config->components = json_decode($this->config->components); + } + } + + /** + * Returns the URL to send update requests to + * + * During the development or testing, you can set $CFG->alternativeupdateproviderurl + * to a custom URL that will be used. Otherwise the standard URL will be returned. + * + * @return string URL + */ + protected function prepare_request_url() { + global $CFG; + + if (!empty($CFG->alternativeupdateproviderurl)) { + return $CFG->alternativeupdateproviderurl; + } else { + return 'http://download.moodle.org/api/1.0/updates.php'; + } + } + + /** + * Returns the list of HTTP params to be sent to the updates provider URL + * + * @return array of (string)param => (string)value + */ + protected function prepare_request_params() { + global $CFG; + + $params = array(); + $params['format'] = 'json'; + + if (isset($this->config->ticket)) { + $params['ticket'] = $this->config->ticket; + } + + if (isset($this->localversion)) { + $params['version'] = $this->localversion; + } + + if (isset($this->localrelease)) { + $params['release'] = $this->localrelease; + } + + // todo localplugins + return $params; + } +} + + /** * Factory class producing required subclasses of {@link plugininfo_base} */ From dd119e21fa4cfa615010b14e4864da0593c1b5c3 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Wed, 7 Mar 2012 17:47:45 +0100 Subject: [PATCH 08/36] MDL-20438 Let plugin_manager class use the service provided by the new available_update_checker All plugininfo classes now have new property that holds their remote status information. --- lib/pluginlib.php | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index ea4c7d2403e..d8a878defc1 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -83,8 +83,6 @@ class plugin_manager { * @return plugin_manager the singleton instance */ public static function instance() { - global $CFG; - if (is_null(self::$singletoninstance)) { self::$singletoninstance = new self(); } @@ -120,6 +118,14 @@ class plugin_manager { $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass); $this->pluginsinfo[$plugintype] = $plugins; } + + // append the information about available updates provided by {@link available_update_checker()} + $provider = available_update_checker::instance(); + foreach ($this->pluginsinfo as $plugintype => $plugins) { + foreach ($plugins as $plugininfoholder) { + $plugininfoholder->check_available_update($provider); + } + } } return $this->pluginsinfo; @@ -899,6 +905,8 @@ abstract class plugininfo_base { public $instances; /** @var int order of the plugin among other plugins of the same type - not supported yet */ public $sortorder; + /** @var null|stdClass holds the information about the remote available update for this plugin */ + public $availableupdate; /** * Gathers and returns the information about all plugins of the given type @@ -1127,6 +1135,16 @@ abstract class plugininfo_base { return null; } + /** + * Populates the property {@link $availableupdate} with the information provided by + * available update checker + * + * @param available_update_checker $provider the class providing the available update info + */ + public function check_available_update(available_update_checker $provider) { + $this->availableupdate = $provider->get_update_info($this->component); + } + /** * If there is an update of this plugin available, returns the data about it. * @@ -1137,7 +1155,16 @@ abstract class plugininfo_base { * @return stdClass|false|null */ public function available_update() { - return null; + + if (empty($this->availableupdate)) { + return null; + } + + if ($this->availableupdate->version > $this->versiondisk) { + return $this->availableupdate; + } + + return false; } /** From 2b47947ef5b0245d9ae77418c3188819560a50f5 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Wed, 7 Mar 2012 17:50:47 +0100 Subject: [PATCH 09/36] MDL-20438 A set of unittests to illustrate the update checking integration in the plugin_manager --- lib/simpletest/testpluginlib.php | 144 ++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 3 deletions(-) diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index 7c94917ab60..381f9307b8b 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -91,18 +91,87 @@ class testable_plugin_manager extends plugin_manager { 'mod' => array( 'foo' => plugininfo_default_factory::make('mod', $CFG->dirroot.'/mod', 'foo', $CFG->dirroot.'/mod/foo', 'testable_plugininfo_mod'), + 'bar' => plugininfo_default_factory::make('mod', $CFG->dirroot.'/bar', 'bar', + $CFG->dirroot.'/mod/bar', 'testable_plugininfo_mod'), + 'buz' => plugininfo_default_factory::make('mod', $CFG->dirroot.'/buz', 'buz', + $CFG->dirroot.'/mod/buz', 'testable_plugininfo_mod'), ) ); + $checker = testable_available_update_checker::instance(); + $this->pluginsinfo['mod']['foo']->check_available_update($checker); + $this->pluginsinfo['mod']['bar']->check_available_update($checker); + $this->pluginsinfo['mod']['buz']->check_available_update($checker); + return $this->pluginsinfo; } } /** - * Test cases for the pluginlib API - * - * These are basic tests to document the basic API of the plugin manager. + * Modified version of {@link available_update_checker} suitable for testing + */ +class testable_available_update_checker extends available_update_checker { + + /** + * Factory method for this class + * + * @return testable_available_update_checker the singleton instance + */ + public static function instance() { + global $CFG; + + if (is_null(self::$singletoninstance)) { + self::$singletoninstance = new self(); + } + return self::$singletoninstance; + } + + /** + * Do not load config in this testable subclass + */ + protected function load_config() { + } + + /** + * Do not fetch anything in this testable subclass + */ + public function fetch() { + } + + /** + * Here we simulate read access to the fetched remote statuses + */ + public function get_update_info($component) { + if ($component === 'mod_foo') { + // no update available + return (object)array( + 'version' => 2012030500, + ); + + } else if ($component === 'mod_bar') { + // there is an update available + return (object)array( + 'version' => 2012030501, + ); + + } else { + // nothing known to us + return null; + } + } + + /** + * Makes the method public so we can test it + */ + public function merge_components_info(stdClass $old, stdClass $new, $timegenerated=null) { + return parent::merge_components_info($old, $new, $timegenerated); + } +} + + +/** + * Tests of the basic API of the plugin manager */ class plugin_manager_test extends UnitTestCase { @@ -124,4 +193,73 @@ class plugin_manager_test extends UnitTestCase { $modfoo = $plugins['mod']['foo']; $this->assertEqual($modfoo->get_status(), plugin_manager::PLUGIN_STATUS_UPGRADE); } + + public function test_available_update() { + $pluginman = testable_plugin_manager::instance(); + $plugins = $pluginman->get_plugins(); + $this->assertFalse($plugins['mod']['foo']->available_update()); + $this->assertNull($plugins['mod']['buz']->available_update()); + $this->assertIsA($plugins['mod']['bar']->available_update(), 'stdClass'); + $this->assertEqual($plugins['mod']['bar']->available_update()->version, 2012030501); + } +} + + +/** + * Tests of the basic API of the available update checker + */ +class available_update_checker_test extends UnitTestCase { + + public function test_core_available_update() { + $provider = testable_available_update_checker::instance(); + $this->assertTrue($provider instanceof available_update_checker); + } + + public function test_merge_components_info() { + $old = (object)array( + '2.2' => (object)array( + 'core' => (object)array( + 'version' => 2011120501.11, + 'release' => '2.2.1+ (Build: 20120301)', + 'maturity' => MATURITY_STABLE, + ), + 'mod_foo' => (object)array( + 'version' => 2011010100, + ), + 'mod_bar' => (object)array( + 'version' => 2011020200, + ) + ) + ); + $new = (object)array( + '2.2' => (object)array( + 'core' => (object)array( + 'version' => 2011120501.12, + 'release' => '2.2.1+ (Build: 20120302)', + 'maturity' => MATURITY_STABLE, + ), + 'mod_bar' => (object)array( + 'version' => 2011020201, + ), + ), + '2.3' => (object)array( + 'core' => (object)array( + 'version' => 2012030100.00, + 'release' => '2.3dev (Build: 20120301)', + 'maturity' => MATURITY_ALPHA, + ), + 'mod_foo' => (object)array( + 'version' => 2012010200, + ) + ) + ); + $checker = testable_available_update_checker::instance(); + $now = time(); + $merged = $checker->merge_components_info($old, $new, $now); + $this->assertEqual($merged->{2.2}->core->version, 2011120501.12); // from $new + $this->assertEqual($merged->{2.2}->mod_bar->version, 2011020201); // from $new + $this->assertEqual($merged->{2.2}->mod_foo->version, 2011010100); // from $old + $this->assertEqual($merged->{2.3}->core->version, 2012030100.00); // from $new + $this->assertFalse(isset($merged->{2.3}->mod_bar)); + } } From 3204daeae384134c061a769132270dabe94d0040 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Wed, 7 Mar 2012 17:53:20 +0100 Subject: [PATCH 10/36] MDL-20438 The Plugins overview admin page now provides a way to check for updates --- admin/plugins.php | 27 ++++++++++++++- admin/renderer.php | 68 +++++++++++++++++++++++++++----------- lang/en/plugin.php | 13 +++++--- theme/base/style/admin.css | 5 +++ 4 files changed, 88 insertions(+), 25 deletions(-) diff --git a/admin/plugins.php b/admin/plugins.php index 02049307993..c7ed6fc06e4 100644 --- a/admin/plugins.php +++ b/admin/plugins.php @@ -30,5 +30,30 @@ require_once($CFG->libdir . '/pluginlib.php'); require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM)); admin_externalpage_setup('pluginsoverview'); + +$fetchremote = optional_param('fetchremote', false, PARAM_BOOL); + +$pluginman = plugin_manager::instance(); +$checker = available_update_checker::instance(); + +if ($fetchremote) { + $checker->fetch(true); + redirect($PAGE->url); +} + $output = $PAGE->get_renderer('core', 'admin'); -echo $output->plugin_management_page(plugin_manager::instance()); + +echo $output->header(); +echo $output->heading(get_string('pluginsoverview', 'core_admin')); +echo $output->plugins_overview_panel($pluginman); + +echo $output->container_start('checkforupdates'); +echo $output->single_button(new moodle_url($PAGE->url, array('fetchremote' => 1)), get_string('checkforupdates', 'core_plugin')); +if ($timefetched = $checker->get_last_timefetched()) { + echo $output->container(get_string('checkforupdateslast', 'core_plugin', + userdate($timefetched, get_string('strftimedatetime', 'core_langconfig')))); +} +echo $output->container_end(); + +echo $output->box($output->plugins_control_panel($pluginman), 'generalbox'); +echo $output->footer(); diff --git a/admin/renderer.php b/admin/renderer.php index c74d6413807..78d4ebbaade 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -226,25 +226,6 @@ class core_admin_renderer extends plugin_renderer_base { return $output; } - /** - * Display the plugin management page (admin/plugins.php). - * @param plugin_manager $pluginman - * @return string HTML to output. - */ - public function plugin_management_page(plugin_manager $pluginman) { - $output = ''; - - $output .= $this->header(); - $output .= $this->heading(get_string('pluginsoverview', 'core_admin')); - $output .= $this->plugins_overview_panel($pluginman); - $output .= $this->box_start('generalbox'); - $output .= $this->plugins_control_panel($pluginman); - $output .= $this->box_end(); - $output .= $this->footer(); - - return $output; - } - /** * Display the plugin management page (admin/environment.php). * @param array $versions @@ -798,7 +779,13 @@ class core_admin_renderer extends plugin_renderer_base { $requiredby = ''; } - $notes = new html_table_cell($requiredby); + if ($updateinfo = $plugin->available_update()) { + $updateinfo = $this->plugin_available_update_info($updateinfo); + } else { + $updateinfo = ''; + } + + $notes = new html_table_cell($requiredby.$updateinfo); $row->cells = array( $pluginname, $source, $version, $availability, $actions, $notes @@ -810,6 +797,47 @@ class core_admin_renderer extends plugin_renderer_base { return html_writer::table($table); } + /** + * Helper method to render the information about the available update + * + * The passed objects always provides at least the 'version' property containing + * the (higher) version of the plugin available. Other properties may be provided, see + * the specification of the protocol used by {@link available_update_checker}. + * + * @param stdClass $updateinfo information about the available update for the plugin + */ + protected function plugin_available_update_info(stdClass $updateinfo) { + + $box = $this->output->box_start('pluginupdateinfo'); + $box .= html_writer::tag('div', get_string('updateavailable', 'core_plugin', $updateinfo->version), array('class' => 'version')); + + $info = array(); + + if (isset($updateinfo->release)) { + $info[] = html_writer::tag('span', get_string('updateavailable_release', 'core_plugin', $updateinfo->release), + array('class' => 'info release')); + } + + if (isset($updateinfo->maturity)) { + $info[] = html_writer::tag('span', get_string('maturity'.$updateinfo->maturity, 'core_admin'), + array('class' => 'info maturity')); + } + + if (isset($updateinfo->download)) { + $info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download')); + } + + if (isset($updateinfo->url)) { + $info[] = html_writer::link($updateinfo->url, get_string('updateavailable_moreinfo', 'core_plugin'), + array('class' => 'info more')); + } + + $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '', 'infobox'); + $box .= $this->output->box_end(); + + return $box; + } + /** * This function will render one beautiful table with all the environmental * configuration and how it suits Moodle needs. diff --git a/lang/en/plugin.php b/lang/en/plugin.php index ed3e21d979e..4dffab6dee2 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -27,16 +27,18 @@ defined('MOODLE_INTERNAL') || die(); $string['actions'] = 'Actions'; $string['availability'] = 'Availability'; +$string['checkforupdates'] = 'Check for available updates'; +$string['checkforupdateslast'] = 'Last check done on {$a}'; $string['displayname'] = 'Plugin name'; $string['moodleversion'] = 'Moodle {$a}'; $string['nonehighlighted'] = 'No plugins require your attention during this upgrade'; $string['nonehighlightedinfo'] = 'Display the list of all installed plugins anyway'; $string['noneinstalled'] = 'No plugins of this type are installed'; $string['notes'] = 'Notes'; -$string['numtotal'] = '{$a} installed'; -$string['numdisabled'] = '{$a} disabled'; -$string['numextension'] = '{$a} extensions'; -$string['numupdatable'] = '{$a} updates available'; +$string['numtotal'] = 'Installed: {$a}'; +$string['numdisabled'] = 'Disabled: {$a}'; +$string['numextension'] = 'Extensions: {$a}'; +$string['numupdatable'] = 'Updates available: {$a}'; $string['otherplugin'] = '{$a->component}'; $string['otherpluginversion'] = '{$a->component} ({$a->version})'; $string['showall'] = 'Reload and show all plugins'; @@ -116,6 +118,9 @@ $string['type_tool'] = 'Admin tool'; $string['type_tool_plural'] = 'Admin tools'; $string['type_webservice'] = 'Webservice protocol'; $string['type_webservice_plural'] = 'Webservice protocols'; +$string['updateavailable'] = 'There is a new version {$a} available!'; +$string['updateavailable_moreinfo'] = 'More info...'; +$string['updateavailable_release'] = 'Release {$a}'; $string['uninstall'] = 'Uninstall'; $string['version'] = 'Version'; $string['versiondb'] = 'Current version'; diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index 5fabf4f243a..9d564474b23 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -199,6 +199,7 @@ #page-admin-index #plugins-check .requires-failed {background-color:#ffd3d9;} /** Plugins management */ +#page-admin-plugins .checkforupdates {margin:0 auto 1em;text-align:center;} #page-admin-plugins #plugins-control-panel {margin-left:auto; margin-right:auto;} #page-admin-plugins #plugins-control-panel .pluginname .pluginicon {width:16px;} #page-admin-plugins #plugins-control-panel .pluginname .componentname {font-size:0.8em;color:#999;margin-left:26px;} @@ -213,6 +214,10 @@ #page-admin-plugins #plugins-overview-panel .info {padding:5px 10px;} #page-admin-plugins #plugins-overview-panel .separator {border-left:1px dotted #999;} #page-admin-plugins #plugins-overview-panel .info.updatable {margin-left:10px;background-color:#d2ebff;font-weight:bold;-moz-border-radius: 10px;-webkit-border-radius: 10px;border-radius: 10px;} +#page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo {padding:5px 10px;margin:10px;background-color:#d2ebff;-moz-border-radius: 10px;-webkit-border-radius: 10px;border-radius: 10px;} +#page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo span, +#page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo a {padding-right:1em;} +#page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo .separator {border-left:1px dotted #333;} /** MNet networking */ #page-admin-mnet-peers .box.deletedhosts {margin-bottom:1em;font-size:80%;} From 7d8de6d84a2cf93c5203a16527bcc3f8a4bcb994 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 15 Mar 2012 17:55:12 +0100 Subject: [PATCH 11/36] MDL-20438 updated response format support Multiple component updates are supported now. Code cleanup and refactoring. --- admin/renderer.php | 18 +-- lib/pluginlib.php | 230 +++++++++++++++++++------------ lib/simpletest/testpluginlib.php | 153 +++++++++----------- 3 files changed, 214 insertions(+), 187 deletions(-) diff --git a/admin/renderer.php b/admin/renderer.php index 78d4ebbaade..43760c59344 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -651,7 +651,7 @@ class core_admin_renderer extends plugin_renderer_base { if (!$plugin->is_standard()) { $numextension++; } - if ($plugin->available_update()) { + if ($plugin->available_updates()) { $numupdatable++; } } @@ -779,10 +779,11 @@ class core_admin_renderer extends plugin_renderer_base { $requiredby = ''; } - if ($updateinfo = $plugin->available_update()) { - $updateinfo = $this->plugin_available_update_info($updateinfo); - } else { - $updateinfo = ''; + $updateinfo = ''; + if (is_array($plugin->available_updates())) { + foreach ($plugin->available_updates() as $availableupdate) { + $updateinfo .= $this->plugin_available_update_info($availableupdate); + } } $notes = new html_table_cell($requiredby.$updateinfo); @@ -801,12 +802,11 @@ class core_admin_renderer extends plugin_renderer_base { * Helper method to render the information about the available update * * The passed objects always provides at least the 'version' property containing - * the (higher) version of the plugin available. Other properties may be provided, see - * the specification of the protocol used by {@link available_update_checker}. + * the (higher) version of the plugin available. * - * @param stdClass $updateinfo information about the available update for the plugin + * @param available_update_info $updateinfo information about the available update for the plugin */ - protected function plugin_available_update_info(stdClass $updateinfo) { + protected function plugin_available_update_info(available_update_info $updateinfo) { $box = $this->output->box_start('pluginupdateinfo'); $box .= html_writer::tag('div', get_string('updateavailable', 'core_plugin', $updateinfo->version), array('class' => 'version')); diff --git a/lib/pluginlib.php b/lib/pluginlib.php index d8a878defc1..7638392aa61 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -123,7 +123,7 @@ class plugin_manager { $provider = available_update_checker::instance(); foreach ($this->pluginsinfo as $plugintype => $plugins) { foreach ($plugins as $plugininfoholder) { - $plugininfoholder->check_available_update($provider); + $plugininfoholder->check_available_updates($provider); } } } @@ -572,20 +572,15 @@ class available_update_checker { /** @var available_update_checker holds the singleton instance */ protected static $singletoninstance; - /** @var null|string numerical version of the local Moodle site */ - protected $localversion = null; - /** @var null|string release signature of the local Moodle site */ - protected $localrelease = null; - /** @var array of (string)frankestyle => (string)version list of plugins installed at the local Moodle site */ - protected $localplugins = array(); - /** @var null|stdClass */ - protected $config = null; + /** @var null|int the timestamp of when the most recent response was fetched */ + protected $recentfetch = null; + /** @var null|array the recent response from the update notification provider */ + protected $recentresponse = null; /** * Direct initiation not allowed, use the factory method {@link self::instance()} */ protected function __construct() { - $this->load_config(); } /** @@ -658,10 +653,14 @@ class available_update_checker { * @return int|null null if it has never been executed or we don't known */ public function get_last_timefetched() { - if (isset($this->config->timelastfetched)) { - return $this->config->timelastfetched; + + $this->restore_response(); + + if (!empty($this->recentfetch)) { + return $this->recentfetch; + } else { - return false; + return null; } } @@ -671,126 +670,133 @@ class available_update_checker { * @throws available_update_checker_exception */ public function fetch() { - $response = $this->make_request(); + $response = $this->get_response(); $this->validate_response($response); - $this->process_response($response); + $this->store_response($response); } /** * Returns the available update information for the given component * * This method returns null if the most recent response does not contain any information - * about it. Note that this does not mean that the information is not provided by the - * remote site. The recent request might be specific + * about it. The returned structure is an array of available updates for the given + * component. Each update info is an object with at least one property called + * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'. * * @param string $component frankenstyle - * @return null|stdClass null if the most recent response does not provide any info + * @return null|stdClass null or array of objects */ public function get_update_info($component) { - $branch = moodle_major_version(); + $this->restore_response(); - if (!empty($this->config->components->$branch->$component)) { - return $this->config->components->$branch->$component; + if (!empty($this->recentresponse['updates'][$component])) { + $updates = array(); + foreach ($this->recentresponse['updates'][$component] as $info) { + $updates[] = new available_update_info($component, $info); + } + return $updates; } else { return null; } } /** - * Executes cURL request to get data from the remote site + * Makes cURL request to get data from the remote site * - * @return stdClass request result + * @return string raw request result * @throws available_update_checker_exception */ - protected function make_request() { + protected function get_response() { $curl = new curl(array('proxy' => true)); $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params()); $curlinfo = $curl->get_info(); if ($curlinfo['http_code'] != 200) { throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']); } - $response = json_decode($response); return $response; } /** * Makes sure the response is valid, has correct API format etc. * - * @param stdClass $response + * @param string $response raw response as returned by the {@link self::get_response()} * @throws available_update_checker_exception */ - protected function validate_response(stdClass $response) { + protected function validate_response($response) { + + $response = $this->decode_response($response); if (empty($response)) { throw new available_update_checker_exception('err_response_empty'); } - if (empty($response->status) or $response->status !== 'OK') { - throw new available_update_checker_exception('err_response_status', $response->status); + if (empty($response['status']) or $response['status'] !== 'OK') { + throw new available_update_checker_exception('err_response_status', $response['status']); } - if (empty($response->apiver) or $response->apiver != '1.0') { - throw new available_update_checker_exception('err_response_format_version', $response->apiver); + if (empty($response['apiver']) or $response['apiver'] !== '1.0') { + throw new available_update_checker_exception('err_response_format_version', $response['apiver']); + } + + if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) { + throw new available_update_checker_exception('err_response_target_version', $response['target']); } } /** - * Stores the fetched response for later usage + * Decodes the raw string response from the update notifications provider + * + * @param string $response as returned by {@link self::get_response()} + * @return array decoded response structure + */ + protected function decode_response($response) { + return json_decode($response, true); + } + + /** + * Stores the valid fetched response for later usage * * This implementation uses the config_plugins table as the permanent storage. * - * @param stdClass $response the data returned by the updates info provider + * @param string $response raw valid data returned by {@link self::get_response()} */ - protected function process_response(stdClass $response) { + protected function store_response($response) { - $components = $this->merge_components_info($this->config->components, $response->components, $response->timegenerated); + set_config('recentfetch', time(), 'core_plugin'); + set_config('recentresponse', $response, 'core_plugin'); - set_config('timelastfetched', time(), 'core_plugin'); - set_config('ticket', $response->ticket, 'core_plugin'); - set_config('components', json_encode($components), 'core_plugin'); - - $this->load_config(true); + $this->restore_response(true); } /** - * Merges the current and the new info about the available updates + * Loads the most recent raw response record we have fetched * - * @param stdClass $old - * @param stdClass $new - * @param int $timegenerated the timestamp of when the $new was generated - * @return stdClass merged + * This implementation uses the config_plugins table as the permanent storage. + * + * @param bool $forcereload reload even if it was already loaded */ - protected function merge_components_info(stdClass $old, stdClass $new, $timegenerated=null) { - $merged = clone($old); - if (is_null($timegenerated)) { - $timegenerated = time(); + protected function restore_response($forcereload = false) { + + if (!$forcereload and !is_null($this->recentresponse)) { + // we already have it, nothing to do + return; } - foreach ($new as $branch => $components) { - foreach ($components as $component => $info) { - $info->timegenerated = $timegenerated; - if (isset($info->version)) { - $merged->$branch->$component = $info; - } + + $config = get_config('core_plugin'); + + if (!empty($config->recentresponse) and !empty($config->recentfetch)) { + try { + $this->validate_response($config->recentresponse); + $this->recentfetch = $config->recentfetch; + $this->recentresponse = $this->decode_response($config->recentresponse); + } + catch (available_update_checker_exception $e) { + // do not set recentresponse if the validation fails } - } - return $merged; - } - /** - * Loads the core_plugin subsystem config - * - * @param bool $forcereload reload the config even if it was already loaded - */ - protected function load_config($forcereload = false) { - if ($forcereload or is_null($this->config)) { - $this->config = get_config('core_plugin'); - } - - if (empty($this->config->components)) { - $this->config->components = new stdClass(); } else { - $this->config->components = json_decode($this->config->components); + $this->recentresponse = array(); } } @@ -820,11 +826,13 @@ class available_update_checker { protected function prepare_request_params() { global $CFG; + $this->restore_response(); + $params = array(); $params['format'] = 'json'; - if (isset($this->config->ticket)) { - $params['ticket'] = $this->config->ticket; + if (isset($this->recentresponse['ticket'])) { + $params['ticket'] = $this->recentresponse['ticket']; } if (isset($this->localversion)) { @@ -841,6 +849,44 @@ class available_update_checker { } +/** + * Defines the structure of objects returned by {@link available_update_checker::get_update_info()} + */ +class available_update_info { + + /** @var string frankenstyle component name */ + public $component; + /** @var int the available version of the component */ + public $version; + /** @var string|null optional release name */ + public $release = null; + /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */ + public $maturity = null; + /** @var string|null optional URL of a page with more info about the update */ + public $url = null; + /** @var string|null optional URL of a ZIP package that can be downloaded and installed */ + public $download = null; + + /** + * Creates new instance of the class + * + * The $info array must provide at least the 'version' value and optionally all other + * values to populate the object's properties. + * + * @param string $name the frankenstyle component name + * @param array $info associative array with other properties + */ + public function __construct($name, array $info) { + $this->component = $name; + foreach ($info as $k => $v) { + if (property_exists('available_update_info', $k) and $k != 'component') { + $this->$k = $v; + } + } + } +} + + /** * Factory class producing required subclasses of {@link plugininfo_base} */ @@ -905,8 +951,8 @@ abstract class plugininfo_base { public $instances; /** @var int order of the plugin among other plugins of the same type - not supported yet */ public $sortorder; - /** @var null|stdClass holds the information about the remote available update for this plugin */ - public $availableupdate; + /** @var array|null array of {@link available_update_info} for this plugin */ + public $availableupdates; /** * Gathers and returns the information about all plugins of the given type @@ -1136,35 +1182,43 @@ abstract class plugininfo_base { } /** - * Populates the property {@link $availableupdate} with the information provided by + * Populates the property {@link $availableupdates} with the information provided by * available update checker * * @param available_update_checker $provider the class providing the available update info */ - public function check_available_update(available_update_checker $provider) { - $this->availableupdate = $provider->get_update_info($this->component); + public function check_available_updates(available_update_checker $provider) { + $this->availableupdates = $provider->get_update_info($this->component); } /** - * If there is an update of this plugin available, returns the data about it. + * If there are updates for this plugin available, returns them. * - * Returns object with various properties about the available update, if such - * an update is available. Returns false if there is no update available for - * this plugin. Returns null if the update availabitlity is unknown. + * Returns array of {@link available_update_info} objects, if some update + * is available. Returns null if there is no update available or if the update + * availability is unknown. * - * @return stdClass|false|null + * @return array|null */ - public function available_update() { + public function available_updates() { - if (empty($this->availableupdate)) { + if (empty($this->availableupdates) or !is_array($this->availableupdates)) { return null; } - if ($this->availableupdate->version > $this->versiondisk) { - return $this->availableupdate; + $updates = array(); + + foreach ($this->availableupdates as $availableupdate) { + if ($availableupdate->version > $this->versiondisk) { + $updates[] = $availableupdate; + } } - return false; + if (empty($updates)) { + return null; + } + + return $updates; } /** diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index 381f9307b8b..488df5d6d96 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -93,15 +93,12 @@ class testable_plugin_manager extends plugin_manager { $CFG->dirroot.'/mod/foo', 'testable_plugininfo_mod'), 'bar' => plugininfo_default_factory::make('mod', $CFG->dirroot.'/bar', 'bar', $CFG->dirroot.'/mod/bar', 'testable_plugininfo_mod'), - 'buz' => plugininfo_default_factory::make('mod', $CFG->dirroot.'/buz', 'buz', - $CFG->dirroot.'/mod/buz', 'testable_plugininfo_mod'), ) ); $checker = testable_available_update_checker::instance(); - $this->pluginsinfo['mod']['foo']->check_available_update($checker); - $this->pluginsinfo['mod']['bar']->check_available_update($checker); - $this->pluginsinfo['mod']['buz']->check_available_update($checker); + $this->pluginsinfo['mod']['foo']->check_available_updates($checker); + $this->pluginsinfo['mod']['bar']->check_available_updates($checker); return $this->pluginsinfo; } @@ -113,6 +110,9 @@ class testable_plugin_manager extends plugin_manager { */ class testable_available_update_checker extends available_update_checker { + /** @var replaces the default DB table storage for the fetched response */ + protected $responsestorage; + /** * Factory method for this class * @@ -127,45 +127,65 @@ class testable_available_update_checker extends available_update_checker { return self::$singletoninstance; } - /** - * Do not load config in this testable subclass - */ - protected function load_config() { + protected function validate_response() { } - /** - * Do not fetch anything in this testable subclass - */ - public function fetch() { + protected function store_response($response) { + $this->responsestorage = $response; } - /** - * Here we simulate read access to the fetched remote statuses - */ - public function get_update_info($component) { - if ($component === 'mod_foo') { - // no update available - return (object)array( - 'version' => 2012030500, - ); - - } else if ($component === 'mod_bar') { - // there is an update available - return (object)array( - 'version' => 2012030501, - ); - - } else { - // nothing known to us - return null; - } + protected function restore_response($forcereload = false) { + $this->recentfetch = time(); + $this->recentresponse = $this->decode_response($this->get_fake_response()); } - /** - * Makes the method public so we can test it - */ - public function merge_components_info(stdClass $old, stdClass $new, $timegenerated=null) { - return parent::merge_components_info($old, $new, $timegenerated); + private function get_fake_response() { + $fakeresponse = array( + 'status' => 'OK', + 'provider' => 'http://download.moodle.org/api/1.0/updates.php', + 'apiver' => '1.0', + 'timegenerated' => time(), + 'forversion' => '2012010100.00', + 'forbranch' => '2.3', + 'ticket' => sha1('No, I am not going to mention the word "frog" here. Oh crap. I just did.'), + 'updates' => array( + 'core' => array( + array( + 'version' => 2012060103.00, + 'release' => '2.3.3 (Build: 20121201)', + 'maturity' => 200, + 'url' => 'http://download.moodle.org/', + 'download' => 'http://download.moodle.org/download.php/MOODLE_23_STABLE/moodle-2.3.3-latest.zip', + ), + array( + 'version' => 2012120100.00, + 'release' => '2.4 (Build: 20121201)', + 'maturity' => 200, + 'url' => 'http://download.moodle.org/', + 'download' => 'http://download.moodle.org/download.php/MOODLE_24_STABLE/moodle-2.4.0-latest.zip', + ), + ), + 'mod_foo' => array( + array( + 'version' => 2012030501, + 'requires' => 2012010100, + 'maturity' => 200, + 'release' => '1.1', + 'url' => 'http://moodle.org/plugins/blahblahblah/', + 'download' => 'http://moodle.org/plugins/download.php/blahblahblah', + ), + array( + 'version' => 2012030502, + 'requires' => 2012010100, + 'maturity' => 100, + 'release' => '1.2 beta', + 'url' => 'http://moodle.org/plugins/', + ), + ), + ), + ); + + return json_encode($fakeresponse); } } @@ -197,10 +217,11 @@ class plugin_manager_test extends UnitTestCase { public function test_available_update() { $pluginman = testable_plugin_manager::instance(); $plugins = $pluginman->get_plugins(); - $this->assertFalse($plugins['mod']['foo']->available_update()); - $this->assertNull($plugins['mod']['buz']->available_update()); - $this->assertIsA($plugins['mod']['bar']->available_update(), 'stdClass'); - $this->assertEqual($plugins['mod']['bar']->available_update()->version, 2012030501); + $this->assertNull($plugins['mod']['bar']->available_updates()); + $this->assertIsA($plugins['mod']['foo']->available_updates(), 'array'); + foreach ($plugins['mod']['foo']->available_updates() as $availableupdate) { + $this->assertIsA($availableupdate, 'available_update_info'); + } } } @@ -214,52 +235,4 @@ class available_update_checker_test extends UnitTestCase { $provider = testable_available_update_checker::instance(); $this->assertTrue($provider instanceof available_update_checker); } - - public function test_merge_components_info() { - $old = (object)array( - '2.2' => (object)array( - 'core' => (object)array( - 'version' => 2011120501.11, - 'release' => '2.2.1+ (Build: 20120301)', - 'maturity' => MATURITY_STABLE, - ), - 'mod_foo' => (object)array( - 'version' => 2011010100, - ), - 'mod_bar' => (object)array( - 'version' => 2011020200, - ) - ) - ); - $new = (object)array( - '2.2' => (object)array( - 'core' => (object)array( - 'version' => 2011120501.12, - 'release' => '2.2.1+ (Build: 20120302)', - 'maturity' => MATURITY_STABLE, - ), - 'mod_bar' => (object)array( - 'version' => 2011020201, - ), - ), - '2.3' => (object)array( - 'core' => (object)array( - 'version' => 2012030100.00, - 'release' => '2.3dev (Build: 20120301)', - 'maturity' => MATURITY_ALPHA, - ), - 'mod_foo' => (object)array( - 'version' => 2012010200, - ) - ) - ); - $checker = testable_available_update_checker::instance(); - $now = time(); - $merged = $checker->merge_components_info($old, $new, $now); - $this->assertEqual($merged->{2.2}->core->version, 2011120501.12); // from $new - $this->assertEqual($merged->{2.2}->mod_bar->version, 2011020201); // from $new - $this->assertEqual($merged->{2.2}->mod_foo->version, 2011010100); // from $old - $this->assertEqual($merged->{2.3}->core->version, 2012030100.00); // from $new - $this->assertFalse(isset($merged->{2.3}->mod_bar)); - } } From c7f3ab438113ba83a1a1f7a4279ab626126b2015 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 16 Mar 2012 22:09:28 +0100 Subject: [PATCH 12/36] MDL-20438 moodle code maturity must be always defined in version.php now This code was a temporary solution used during the development of the maturity checks feature. --- admin/index.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/admin/index.php b/admin/index.php index fb02a218a9f..1de856d1928 100644 --- a/admin/index.php +++ b/admin/index.php @@ -103,11 +103,6 @@ if (!$version or !$release) { print_error('withoutversion', 'debug'); // without version, stop } -if (!isset($maturity)) { - // Fallback for now. Should probably be removed in the future. - $maturity = MATURITY_STABLE; -} - // Turn off xmlstrictheaders during upgrade. $origxmlstrictheaders = !empty($CFG->xmlstrictheaders); $CFG->xmlstrictheaders = false; From 00ea11f82ee81f5a351adc5ec7e361c5081d7491 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 16 Mar 2012 22:11:30 +0100 Subject: [PATCH 13/36] MDL-20438 sesskey check to execute fetch() --- admin/plugins.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/plugins.php b/admin/plugins.php index c7ed6fc06e4..9fc100200ff 100644 --- a/admin/plugins.php +++ b/admin/plugins.php @@ -37,7 +37,8 @@ $pluginman = plugin_manager::instance(); $checker = available_update_checker::instance(); if ($fetchremote) { - $checker->fetch(true); + require_sesskey(); + $checker->fetch(); redirect($PAGE->url); } From 37f75ad4598925bf08b441397c85178f0ea52853 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 16 Mar 2012 22:15:51 +0100 Subject: [PATCH 14/36] MDL-20438 colorize the maturity warning at the admin notifications page --- admin/renderer.php | 2 +- theme/base/style/admin.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/admin/renderer.php b/admin/renderer.php index 43760c59344..edf26aea270 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -397,7 +397,7 @@ class core_admin_renderer extends plugin_renderer_base { return $this->box( get_string('maturitycoreinfo', 'admin', $maturitylevel) . ' ' . $this->doc_link('admin/versions', get_string('morehelp')), - 'generalbox adminwarning maturityinfo'); + 'generalbox adminwarning maturityinfo maturity'.$maturity); } /** diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index 9d564474b23..857a5999ae0 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -84,6 +84,10 @@ #page-admin-index .adminwarning .singlebutton, #page-admin-index #layout-table .singlebutton {margin:20px;} +#page-admin-index .adminwarning.maturityinfo.maturity50 {background-color:#ffd3d9;} +#page-admin-index .adminwarning.maturityinfo.maturity100, +#page-admin-index .adminwarning.maturityinfo.maturity150 {background-color:#f3f2aa;} + #page-admin-user-user_bulk #users .fgroup {white-space: nowrap;} #page-admin-report-stats-index .graph {text-align: center;margin-bottom: 1em;} #page-admin-report-courseoverview-index .graph {text-align: center;margin-bottom: 1em;} From e76113899798dee9d66f010942e58822cf714d3c Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 16 Mar 2012 22:19:51 +0100 Subject: [PATCH 15/36] MDL-20438 colorize available updates for a plugin based on the update's maturity level --- admin/renderer.php | 11 ++++++----- theme/base/style/admin.css | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/admin/renderer.php b/admin/renderer.php index edf26aea270..d3a130127f6 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -799,7 +799,7 @@ class core_admin_renderer extends plugin_renderer_base { } /** - * Helper method to render the information about the available update + * Helper method to render the information about the available plugin update * * The passed objects always provides at least the 'version' property containing * the (higher) version of the plugin available. @@ -808,9 +808,7 @@ class core_admin_renderer extends plugin_renderer_base { */ protected function plugin_available_update_info(available_update_info $updateinfo) { - $box = $this->output->box_start('pluginupdateinfo'); - $box .= html_writer::tag('div', get_string('updateavailable', 'core_plugin', $updateinfo->version), array('class' => 'version')); - + $boxclasses = 'pluginupdateinfo'; $info = array(); if (isset($updateinfo->release)) { @@ -821,6 +819,7 @@ class core_admin_renderer extends plugin_renderer_base { if (isset($updateinfo->maturity)) { $info[] = html_writer::tag('span', get_string('maturity'.$updateinfo->maturity, 'core_admin'), array('class' => 'info maturity')); + $boxclasses .= ' maturity'.$updateinfo->maturity; } if (isset($updateinfo->download)) { @@ -832,7 +831,9 @@ class core_admin_renderer extends plugin_renderer_base { array('class' => 'info more')); } - $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '', 'infobox'); + $box = $this->output->box_start($boxclasses); + $box .= html_writer::tag('div', get_string('updateavailable', 'core_plugin', $updateinfo->version), array('class' => 'version')); + $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), ''); $box .= $this->output->box_end(); return $box; diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index 857a5999ae0..c367b845bc9 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -219,6 +219,9 @@ #page-admin-plugins #plugins-overview-panel .separator {border-left:1px dotted #999;} #page-admin-plugins #plugins-overview-panel .info.updatable {margin-left:10px;background-color:#d2ebff;font-weight:bold;-moz-border-radius: 10px;-webkit-border-radius: 10px;border-radius: 10px;} #page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo {padding:5px 10px;margin:10px;background-color:#d2ebff;-moz-border-radius: 10px;-webkit-border-radius: 10px;border-radius: 10px;} +#page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo.maturity50 {background-color:#ffd3d9;} +#page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo.maturity100, +#page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo.maturity150 {background-color:#f3f2aa;} #page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo span, #page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo a {padding-right:1em;} #page-admin-plugins #plugins-control-panel .notes .pluginupdateinfo .separator {border-left:1px dotted #333;} From 55585f3ac082f83fd36c3013ed67cad944b88652 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 16 Mar 2012 23:34:37 +0100 Subject: [PATCH 16/36] MDL-20438 initial support for checking available core update In case of Moodle code itself, there is no plugin_manager like class available so the checker class itself must be aware of versions and actually do the checks. On the other hand, we can always rely that version, release and maturity are always returned by the remote server. --- admin/index.php | 15 +++- admin/renderer.php | 80 +++++++++++++++++- lang/en/admin.php | 5 ++ lib/pluginlib.php | 141 +++++++++++++++++++------------ lib/simpletest/testpluginlib.php | 25 +++++- theme/base/style/admin.css | 10 +++ 6 files changed, 217 insertions(+), 59 deletions(-) diff --git a/admin/index.php b/admin/index.php index 1de856d1928..c3eab8f8212 100644 --- a/admin/index.php +++ b/admin/index.php @@ -55,6 +55,7 @@ define('NO_OUTPUT_BUFFERING', true); require('../config.php'); require_once($CFG->libdir.'/adminlib.php'); // various admin-only functions require_once($CFG->libdir.'/upgradelib.php'); // general upgrade/install related functions +require_once($CFG->libdir.'/pluginlib.php'); // available updates notifications $id = optional_param('id', '', PARAM_TEXT); $confirmupgrade = optional_param('confirmupgrade', 0, PARAM_BOOL); @@ -62,6 +63,7 @@ $confirmrelease = optional_param('confirmrelease', 0, PARAM_BOOL); $confirmplugins = optional_param('confirmplugincheck', 0, PARAM_BOOL); $showallplugins = optional_param('showallplugins', 0, PARAM_BOOL); $agreelicense = optional_param('agreelicense', 0, PARAM_BOOL); +$fetchupdates = optional_param('fetchupdates', 0, PARAM_BOOL); // Check some PHP server settings @@ -369,7 +371,18 @@ $cronoverdue = ($lastcron < time() - 3600 * 24); $dbproblems = $DB->diagnose(); $maintenancemode = !empty($CFG->maintenance_enabled); +$updateschecker = available_update_checker::instance(); +$availableupdates = $updateschecker->get_core_update_info(MATURITY_STABLE); // todo make it configurable +$availableupdatesfetch = $updateschecker->get_last_timefetched(); + admin_externalpage_setup('adminnotifications'); + +if ($fetchupdates) { + require_sesskey(); + $updateschecker->fetch(); + redirect($PAGE->url); +} + $output = $PAGE->get_renderer('core', 'admin'); echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, - $cronoverdue, $dbproblems, $maintenancemode); + $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch); diff --git a/admin/renderer.php b/admin/renderer.php index d3a130127f6..78cdf4da512 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -202,14 +202,18 @@ class core_admin_renderer extends plugin_renderer_base { * @param bool $cronoverdue warn cron not running * @param bool $dbproblems warn db has problems * @param bool $maintenancemode warn in maintenance mode + * @param array|null $availableupdates array of available_update_info objects or null + * @param int|null $availableupdatesfetch timestamp of the most recent updates fetch or null (unknown) + * * @return string HTML to output. */ public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, - $cronoverdue, $dbproblems, $maintenancemode) { + $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch) { $output = ''; $output .= $this->header(); $output .= $this->maturity_info($maturity); + $output .= $this->available_updates($availableupdates, $availableupdatesfetch); $output .= $this->insecure_dataroot_warning($insecuredataroot); $output .= $this->display_errors_warning($errorsdisplayed); $output .= $this->cron_overdue_warning($cronoverdue); @@ -400,6 +404,80 @@ class core_admin_renderer extends plugin_renderer_base { 'generalbox adminwarning maturityinfo maturity'.$maturity); } + /** + * Displays the info about available Moodle updates + * + * @param array|null $updates array of available_update_info objects or null + * @param int|null $fetch timestamp of the most recent updates fetch or null (unknown) + * @return string + */ + protected function available_updates($updates, $fetch) { + + $updateinfo = $this->box_start('generalbox adminwarning availableupdatesinfo'); + if (is_array($updates)) { + $updateinfo .= $this->heading(get_string('updateavailable', 'core_admin'), 3); + foreach ($updates as $update) { + $updateinfo .= $this->moodle_available_update_info($update); + } + } else { + $updateinfo .= $this->heading(get_string('updateavailablenot', 'core_admin'), 3); + } + + $updateinfo .= $this->container_start('checkforupdates'); + $updateinfo .= $this->single_button(new moodle_url($this->page->url, array('fetchupdates' => 1)), get_string('checkforupdates', 'core_plugin')); + if ($fetch) { + $updateinfo .= $this->container(get_string('checkforupdateslast', 'core_plugin', + userdate($fetch, get_string('strftimedatetime', 'core_langconfig')))); + } + $updateinfo .= $this->container_end(); + + $updateinfo .= $this->box_end(); + + return $updateinfo; + } + + /** + * Helper method to render the information about the available Moodle update + * + * @param available_update_info $updateinfo information about the available Moodle core update + */ + protected function moodle_available_update_info(available_update_info $updateinfo) { + + $boxclasses = 'moodleupdateinfo'; + $info = array(); + + if (isset($updateinfo->release)) { + $info[] = html_writer::tag('span', get_string('updateavailable_release', 'core_admin', $updateinfo->release), + array('class' => 'info release')); + } + + if (isset($updateinfo->version)) { + $info[] = html_writer::tag('span', get_string('updateavailable_version', 'core_admin', $updateinfo->version), + array('class' => 'info version')); + } + + if (isset($updateinfo->maturity)) { + $info[] = html_writer::tag('span', get_string('maturity'.$updateinfo->maturity, 'core_admin'), + array('class' => 'info maturity')); + $boxclasses .= ' maturity'.$updateinfo->maturity; + } + + if (isset($updateinfo->download)) { + $info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download')); + } + + if (isset($updateinfo->url)) { + $info[] = html_writer::link($updateinfo->url, get_string('updateavailable_moreinfo', 'core_plugin'), + array('class' => 'info more')); + } + + $box = $this->output->box_start($boxclasses); + $box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), ''); + $box .= $this->output->box_end(); + + return $box; + } + /** * Display a link to the release notes. * @return string HTML to output. diff --git a/lang/en/admin.php b/lang/en/admin.php index 921dc7d36c2..24427f1f2dc 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -968,6 +968,11 @@ $string['unsupported'] = 'Unsupported'; $string['unsuspenduser'] = 'Activate user account'; $string['updateaccounts'] = 'Update existing accounts'; $string['updatecomponent'] = 'Update component'; +$string['updateavailable'] = 'There is a newer Moodle version available!'; +$string['updateavailable_moreinfo'] = 'More info...'; +$string['updateavailable_release'] = 'Moodle {$a}'; +$string['updateavailable_version'] = 'Version {$a}'; +$string['updateavailablenot'] = 'Your Moodle code seems to be up-to-date'; $string['upgradestart'] = 'Upgrade'; $string['upgradeerror'] = 'Unknown error upgrading {$a->plugin} to version {$a->version}, can not continue.'; $string['upgradeforumread'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.
To use this functionality you need to update your tables.'; diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 7638392aa61..64932c84a37 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -64,11 +64,8 @@ class plugin_manager { /** * Direct initiation not allowed, use the factory method {@link self::instance()} - * - * @todo we might want to specify just a single plugin type to work with */ protected function __construct() { - $this->get_plugins(true); } /** @@ -576,6 +573,12 @@ class available_update_checker { protected $recentfetch = null; /** @var null|array the recent response from the update notification provider */ protected $recentresponse = null; + /** @var null|string the numerical version of the local Moodle code */ + protected $currentversion = null; + /** @var null|string branch of the local Moodle code */ + protected $currentbranch = null; + /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */ + protected $currentplugins = array(); /** * Direct initiation not allowed, use the factory method {@link self::instance()} @@ -601,52 +604,6 @@ class available_update_checker { return self::$singletoninstance; } - /** - * Sets the local version - * - * If the version is set before the request is done, the version info will - * be sent to the remote site as a part of the request. The returned data can - * be filtered so that they contain just information relevant to the sent - * version. - * - * @param string $version our local Moodle version, usually $CFG->version - */ - public function set_local_version($version) { - $this->localversion = $version; - } - - /** - * Sets the local release info - * - * If the release is set before the request is done, the release info will - * be sent to the remote site as a part of the request. The returned data can - * be filtered so that they contain just information relevant to the sent - * release/build. - * - * @param string $version our local Moodle version, usually $CFG->release - */ - public function set_local_release($release) { - $this->localrelease = $release; - } - - /** - * Sets the list of plugins and their version at the local Moodle site - * - * The keys of the passed array are frankenstyle component names of plugins. The - * values are the on-disk versions of these plugins (allowing the null value where - * the version can't be obtained for any reason). - * If the plugins are set before the request is done, their list will be sent - * to the remote site as a part of the request. The returned data can - * be filtered so that they contain just information about the installed plugins. - * To obtain a list of all available plugins, do not set this list prior to calling - * {@link self::fetch()} - * - * @param array $plugins of (string)component => (string)version - */ - public function set_local_plugins(array $plugins) { - $this->localplugins = $plugins; - } - /** * Returns the timestamp of the last execution of {@link fetch()} * @@ -675,6 +632,40 @@ class available_update_checker { $this->store_response($response); } + /** + * Returns the available update information for Moodle core + * + * The returned structure is an array of available_update_info objects or null + * if there no such info available. + * + * @param int $minmaturity minimal maturity level to return, returns all by default + * @return null|stdClass null or array of available_update_info objects + */ + public function get_core_update_info($minmaturity = 0) { + + $this->load_current_environment(); + + $updates = $this->get_update_info('core'); + if (empty($updates)) { + return null; + } + $return = array(); + foreach ($updates as $update) { + if (isset($update->maturity) and ($update->maturity < $minmaturity)) { + continue; + } + if ($update->version > $this->currentversion) { + $return[] = $update; + } + } + + if (empty($return)) { + return null; + } + + return $return; + } + /** * Returns the available update information for the given component * @@ -684,7 +675,7 @@ class available_update_checker { * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'. * * @param string $component frankenstyle - * @return null|stdClass null or array of objects + * @return null|stdClass null or array of available_update_info objects */ public function get_update_info($component) { @@ -818,6 +809,34 @@ class available_update_checker { } } + /** + * Sets the properties currentversion, currentbranch and currentplugins + * + * @param bool $forcereload + */ + protected function load_current_environment($forcereload=false) { + global $CFG; + + if (!is_null($this->currentversion) and !$forcereload) { + // nothing to do + return; + } + + require($CFG->dirroot.'/version.php'); + $this->currentversion = $version; + + $this->currentbranch = moodle_major_version(true); + + $pluginman = plugin_manager::instance(); + foreach ($pluginman->get_plugins() as $type => $plugins) { + foreach ($plugins as $plugin) { + if (!$plugin->is_standard()) { + $this->currentplugins[$plugin->component] = $plugin->versiondisk; + } + } + } + } + /** * Returns the list of HTTP params to be sent to the updates provider URL * @@ -826,6 +845,7 @@ class available_update_checker { protected function prepare_request_params() { global $CFG; + $this->load_current_environment(); $this->restore_response(); $params = array(); @@ -835,15 +855,26 @@ class available_update_checker { $params['ticket'] = $this->recentresponse['ticket']; } - if (isset($this->localversion)) { - $params['version'] = $this->localversion; + if (isset($this->currentversion)) { + $params['version'] = $this->currentversion; + } else { + throw new coding_exception('Main Moodle version must be already known here'); } - if (isset($this->localrelease)) { - $params['release'] = $this->localrelease; + if (isset($this->currentbranch)) { + $params['branch'] = $this->currentbranch; + } else { + throw new coding_exception('Moodle release must be already known here'); + } + + $plugins = array(); + foreach ($this->currentplugins as $plugin => $version) { + $plugins[] = $plugin.'@'.$version; + } + if (!empty($plugins)) { + $params['plugins'] = implode(',', $plugins); } - // todo localplugins return $params; } } diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index 488df5d6d96..a15dded3c07 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -139,6 +139,15 @@ class testable_available_update_checker extends available_update_checker { $this->recentresponse = $this->decode_response($this->get_fake_response()); } + protected function load_current_environment($forcereload=false) { + } + + public function fake_current_environment($version, $branch, array $plugins) { + $this->currentversion = $version; + $this->currentbranch = $branch; + $this->currentplugins = $plugins; + } + private function get_fake_response() { $fakeresponse = array( 'status' => 'OK', @@ -159,8 +168,8 @@ class testable_available_update_checker extends available_update_checker { ), array( 'version' => 2012120100.00, - 'release' => '2.4 (Build: 20121201)', - 'maturity' => 200, + 'release' => '2.4dev (Build: 20121201)', + 'maturity' => 50, 'url' => 'http://download.moodle.org/', 'download' => 'http://download.moodle.org/download.php/MOODLE_24_STABLE/moodle-2.4.0-latest.zip', ), @@ -234,5 +243,17 @@ class available_update_checker_test extends UnitTestCase { public function test_core_available_update() { $provider = testable_available_update_checker::instance(); $this->assertTrue($provider instanceof available_update_checker); + + $provider->fake_current_environment(2012060102.00, '2.3', array()); + $updates = $provider->get_core_update_info(); + $this->assertEqual(count($updates), 2); + + $provider->fake_current_environment(2012060103.00, '2.3', array()); + $updates = $provider->get_core_update_info(); + $this->assertEqual(count($updates), 1); + + $provider->fake_current_environment(2012060103.00, '2.3', array()); + $updates = $provider->get_core_update_info(MATURITY_STABLE); + $this->assertNull($updates); } } diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index c367b845bc9..61e8adbcbfa 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -88,6 +88,16 @@ #page-admin-index .adminwarning.maturityinfo.maturity100, #page-admin-index .adminwarning.maturityinfo.maturity150 {background-color:#f3f2aa;} +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo {margin:10px auto;width:60%;text-align:left;} +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo .info.release {margin-right:10px;padding:5px 10px;-moz-border-radius: 10px;-webkit-border-radius: 10px;border-radius: 10px;} +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity50 .info.release {background-color:#ffd3d9;} +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity100 .info.release, +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity150 .info.release {background-color:#f3f2aa;} +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo.maturity200 .info.release {background-color:#d2ebff;} +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo span, +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo a {padding-right:1em;} +#page-admin-index .adminwarning.availableupdatesinfo .moodleupdateinfo .separator {border-left:1px dotted #333;} + #page-admin-user-user_bulk #users .fgroup {white-space: nowrap;} #page-admin-report-stats-index .graph {text-align: center;margin-bottom: 1em;} #page-admin-report-courseoverview-index .graph {text-align: center;margin-bottom: 1em;} From 2f5904d92090c9d87e59469036ee5ad514963d84 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Sat, 17 Mar 2012 01:57:47 +0100 Subject: [PATCH 17/36] MDL-20438 new administration settings for the update notifications machinery --- admin/settings/server.php | 16 ++++++++++++++++ lang/en/admin.php | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/admin/settings/server.php b/admin/settings/server.php index 90c8832fe92..b8b674cc41f 100644 --- a/admin/settings/server.php +++ b/admin/settings/server.php @@ -221,4 +221,20 @@ $ADMIN->add('server', $temp); $ADMIN->add('server', new admin_externalpage('adminregistration', new lang_string('registration','admin'), "$CFG->wwwroot/$CFG->admin/registration/index.php")); +// "update notifications" settingpage +$temp = new admin_settingpage('updatenotifications', new lang_string('updatenotifications', 'core_admin')); +$temp->add(new admin_setting_configcheckbox('updateautocheck', new lang_string('updateautocheck', 'core_admin'), + new lang_string('updateautocheck_desc', 'core_admin'), 1)); +$temp->add(new admin_setting_configselect('updateminmaturity', new lang_string('updateminmaturity', 'core_admin'), + new lang_string('updateminmaturity_desc', 'core_admin'), MATURITY_STABLE, + array( + MATURITY_ALPHA => new lang_string('maturity'.MATURITY_ALPHA, 'core_admin'), + MATURITY_BETA => new lang_string('maturity'.MATURITY_BETA, 'core_admin'), + MATURITY_RC => new lang_string('maturity'.MATURITY_RC, 'core_admin'), + MATURITY_STABLE => new lang_string('maturity'.MATURITY_STABLE, 'core_admin'), + ))); +$temp->add(new admin_setting_configcheckbox('updatenotifybuilds', new lang_string('updatenotifybuilds', 'core_admin'), + new lang_string('updatenotifybuilds_desc', 'core_admin'), 0)); +$ADMIN->add('server', $temp); + } // end of speedup diff --git a/lang/en/admin.php b/lang/en/admin.php index 24427f1f2dc..1e9ed6bd92d 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -973,6 +973,13 @@ $string['updateavailable_moreinfo'] = 'More info...'; $string['updateavailable_release'] = 'Moodle {$a}'; $string['updateavailable_version'] = 'Version {$a}'; $string['updateavailablenot'] = 'Your Moodle code seems to be up-to-date'; +$string['updatenotifications'] = 'Update notifications'; +$string['updateautocheck'] = 'Automatically check for available updates'; +$string['updateautocheck_desc'] = 'If enabled, your site will automatically check for available updates for both Moodle code and all installed extensions. If there is a new update available, a notification will be sent to site admins.'; +$string['updateminmaturity'] = 'Required code maturity'; +$string['updateminmaturity_desc'] = 'Notify about available updates only if the available code has the selected maturity level at least. Updates for extensions that do not declare their code maturity level are always reported regardless this setting.'; +$string['updatenotifybuilds'] = 'Notify about new builds'; +$string['updatenotifybuilds_desc'] = 'If enabled, the available update for Moodle code is also reported when a new build for the current version is available. Builds are continuous improvements of a given Moodle version. They are generally released every week. If disabled, the available update will be reported only when there is a higher version of Moodle released. Checks for extension plugins are not affected by this setting.'; $string['upgradestart'] = 'Upgrade'; $string['upgradeerror'] = 'Unknown error upgrading {$a->plugin} to version {$a->version}, can not continue.'; $string['upgradeforumread'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.
To use this functionality you need to update your tables.'; From d8f18e5dd080199312740b015dc804547ec4e3db Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Sat, 17 Mar 2012 01:58:37 +0100 Subject: [PATCH 18/36] MDL-20438 admin/index.php now respects required maturity setting --- admin/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/index.php b/admin/index.php index c3eab8f8212..341c21a6c45 100644 --- a/admin/index.php +++ b/admin/index.php @@ -372,7 +372,7 @@ $dbproblems = $DB->diagnose(); $maintenancemode = !empty($CFG->maintenance_enabled); $updateschecker = available_update_checker::instance(); -$availableupdates = $updateschecker->get_core_update_info(MATURITY_STABLE); // todo make it configurable +$availableupdates = $updateschecker->get_core_update_info($CFG->updateminmaturity); $availableupdatesfetch = $updateschecker->get_last_timefetched(); admin_externalpage_setup('adminnotifications'); From 9bdedf322a0a96f4cb919fdb3b7e17d0d26c079c Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Sat, 17 Mar 2012 02:48:43 +0100 Subject: [PATCH 19/36] MDL-20438 admin/plugins.php now respects required maturity setting --- lib/pluginlib.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 64932c84a37..35b80ed657d 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -1232,14 +1232,23 @@ abstract class plugininfo_base { * @return array|null */ public function available_updates() { + global $CFG; if (empty($this->availableupdates) or !is_array($this->availableupdates)) { return null; } + if (!isset($CFG->updateminmaturity)) { + // this may happen during the very first upgrade to 2.3 + $CFG->updateminmaturity = MATURITY_STABLE; + } + $updates = array(); foreach ($this->availableupdates as $availableupdate) { + if (isset($availableupdate->maturity) and $availableupdate->maturity < $CFG->updateminmaturity) { + continue; + } if ($availableupdate->version > $this->versiondisk) { $updates[] = $availableupdate; } From f9286139d95b00247f39b3d8c9ad134bb7bc55b9 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 19 Mar 2012 14:34:27 +0100 Subject: [PATCH 20/36] MDL-20438 make sure that the curl class has been included The filelib is usually loaded already. However, during some steps of the upgrade the curl class must be available to perform fetches of updates and the library does not need to be loaded yet. --- lib/pluginlib.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 35b80ed657d..bc145359146 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -30,6 +30,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir.'/filelib.php'); // curl class needed here + /** * Singleton class providing general plugins management functionality */ From bc06cbc36b44b782db4e281a81ab94bf974a5f08 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 19 Mar 2012 14:36:39 +0100 Subject: [PATCH 21/36] MDL-20438 rewording confusing status_uptodate The status_uptodate is a flag that the plugin's on-disk version and the in-db version are in sync, that is no upgrade will be performed. However, there can be a newer version of the plugin available. So using the term "up-to-date" may be confusing. --- lang/en/plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 4dffab6dee2..4c6a575a243 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -64,7 +64,7 @@ $string['status_missing'] = 'Missing from disk!'; $string['status_new'] = 'To be installed'; $string['status_nodb'] = 'No database'; $string['status_upgrade'] = 'To be upgraded'; -$string['status_uptodate'] = 'Up-to-date'; +$string['status_uptodate'] = 'Installed'; $string['systemname'] = 'Identifier'; $string['type_auth'] = 'Authentication method'; $string['type_auth_plural'] = 'Authentication methods'; From 96dd92376f90e9daeee687039292821cfc6cb146 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 19 Mar 2012 14:52:17 +0100 Subject: [PATCH 22/36] MDL-20438 display available updates during the upgrade, too We are displaying available updates for installed plugins only. It was decided (by me and Eloy) that we will not display info about available Moodle update during the upgrade. It seems to be redundant as the admin just uploaded a new version of Moodle so they probably chose what they wanted. --- admin/index.php | 23 +++++++++++++++++++---- admin/renderer.php | 29 +++++++++++++++++++++++++---- theme/base/style/admin.css | 9 +++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/admin/index.php b/admin/index.php index 341c21a6c45..2a0d7b0c48b 100644 --- a/admin/index.php +++ b/admin/index.php @@ -232,9 +232,17 @@ if ($version > $CFG->version) { // upgrade $PAGE->set_heading($strplugincheck); $PAGE->set_cacheable(false); + $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1)); + + if ($fetchupdates) { + require_sesskey(); + available_update_checker::instance()->fetch(); + redirect($reloadurl); + } + $output = $PAGE->get_renderer('core', 'admin'); - echo $output->upgrade_plugin_check_page(plugin_manager::instance(), $version, $showallplugins, - new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1)), + echo $output->upgrade_plugin_check_page(plugin_manager::instance(), available_update_checker::instance(), + $version, $showallplugins, $reloadurl, new moodle_url('/admin/index.php', array('confirmupgrade'=>1, 'confirmrelease'=>1, 'confirmplugincheck'=>1))); die(); @@ -265,9 +273,16 @@ if (moodle_needs_upgrading()) { $PAGE->set_heading($strplugincheck); $PAGE->set_cacheable(false); + if ($fetchupdates) { + require_sesskey(); + available_update_checker::instance()->fetch(); + redirect($PAGE->url); + } + $output = $PAGE->get_renderer('core', 'admin'); - echo $output->upgrade_plugin_check_page(plugin_manager::instance(), $version, $showallplugins, - new moodle_url('/admin/index.php'), + echo $output->upgrade_plugin_check_page(plugin_manager::instance(), available_update_checker::instance(), + $version, $showallplugins, + new moodle_url($PAGE->url), new moodle_url('/admin/index.php', array('confirmplugincheck'=>1))); die(); } diff --git a/admin/renderer.php b/admin/renderer.php index 78cdf4da512..b21c33c3cd2 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -165,18 +165,31 @@ class core_admin_renderer extends plugin_renderer_base { /** * Display the upgrade page that lists all the plugins that require attention. * @param plugin_manager $pluginman provides information about the plugins. + * @param available_update_checker $checker provides information about available updates. * @param int $version the version of the Moodle code from version.php. * @param bool $showallplugins * @param moodle_url $reloadurl * @param moodle_url $continueurl * @return string HTML to output. */ - public function upgrade_plugin_check_page(plugin_manager $pluginman, $version, $showallplugins, $reloadurl, $continueurl) { + public function upgrade_plugin_check_page(plugin_manager $pluginman, available_update_checker $checker, + $version, $showallplugins, $reloadurl, $continueurl) { + $output = ''; $output .= $this->header(); $output .= $this->box_start('generalbox'); - $output .= $this->container(get_string('pluginchecknotice', 'core_plugin'), 'generalbox', 'notice'); + $output .= $this->container_start('generalbox', 'notice'); + $output .= html_writer::tag('p', get_string('pluginchecknotice', 'core_plugin')); + $output .= $this->container_start('checkforupdates'); + $output .= $this->single_button(new moodle_url($reloadurl, array('fetchupdates' => 1)), get_string('checkforupdates', 'core_plugin')); + if ($timefetched = $checker->get_last_timefetched()) { + $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', + userdate($timefetched, get_string('strftimedatetime', 'core_langconfig')))); + } + $output .= $this->container_end(); + $output .= $this->container_end(); + $output .= $this->plugins_check_table($pluginman, $version, array('full' => $showallplugins)); $output .= $this->box_end(); $output .= $this->upgrade_reload($reloadurl); @@ -592,8 +605,16 @@ class core_admin_renderer extends plugin_renderer_base { $statuscode = $plugin->get_status(); $row->attributes['class'] .= ' status-' . $statuscode; + $status = get_string('status_' . $statuscode, 'core_plugin'); - $status = new html_table_cell(get_string('status_' . $statuscode, 'core_plugin')); + $availableupdates = $plugin->available_updates(); + if (!empty($availableupdates)) { + foreach ($availableupdates as $availableupdate) { + $status .= $this->plugin_available_update_info($availableupdate); + } + } + + $status = new html_table_cell($status); $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version)); @@ -601,7 +622,7 @@ class core_admin_renderer extends plugin_renderer_base { plugin_manager::PLUGIN_STATUS_NODB, plugin_manager::PLUGIN_STATUS_UPTODATE)); $dependenciesok = $pluginman->are_dependencies_satisfied( $plugin->get_other_required_plugins()); - if ($isstandard and $statusisboring and $dependenciesok) { + if ($isstandard and $statusisboring and $dependenciesok and empty($availableupdates)) { if (empty($options['full'])) { continue; } diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index 61e8adbcbfa..c06d82075bf 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -193,7 +193,9 @@ .dir-rtl.path-admin .roleassigntable p {text-align: right;} /** Plugins check */ +#page-admin-index #notice .checkforupdates {text-align:center;} #page-admin-index #plugins-check-info {text-align:center;margin:1em;} +#page-admin-index #plugins-check td {vertical-align: top;} #page-admin-index #plugins-check {margin-left:auto; margin-right:auto;} #page-admin-index #plugins-check .displayname .pluginicon {width:16px;} #page-admin-index #plugins-check .missingfromdisk .displayname {background-color:#ffd3d9;} @@ -211,6 +213,13 @@ #page-admin-index #plugins-check .requires li {display:block;} #page-admin-index #plugins-check .requires-ok {color:#999;} #page-admin-index #plugins-check .requires-failed {background-color:#ffd3d9;} +#page-admin-index #plugins-check .status .pluginupdateinfo {padding:5px 10px;margin:10px;background-color:#d2ebff;-moz-border-radius: 10px;-webkit-border-radius: 10px;border-radius: 10px;} +#page-admin-index #plugins-check .status .pluginupdateinfo.maturity50 {background-color:#ffd3d9;} +#page-admin-index #plugins-check .status .pluginupdateinfo.maturity100, +#page-admin-index #plugins-check .status .pluginupdateinfo.maturity150 {background-color:#f3f2aa;} +#page-admin-index #plugins-check .status .pluginupdateinfo span, +#page-admin-index #plugins-check .status .pluginupdateinfo a {padding-right:1em;} +#page-admin-index #plugins-check .status .pluginupdateinfo .separator {border-left:1px dotted #333;} /** Plugins management */ #page-admin-plugins .checkforupdates {margin:0 auto 1em;text-align:center;} From be3788805095289903737e9b535139df476f5f25 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Wed, 21 Mar 2012 17:24:05 +0100 Subject: [PATCH 23/36] MDL-20438 Initial support for cron based notifications If enabled, Moodle sites will check for updates every night sometimes between 01:00 AM and 06:00 AM local time. The actual offset will be generated randomly at the beginning and used consistently since then. This way, the queries against the server are supposed to be spread in time. Serious site admins should not try to upgrade their sites later than 01:00 AM as they are too tired already. Neither should they do it before 06:00 AM as they did not have a morning coffee yet and are too tired yet :-) --- lib/pluginlib.php | 146 +++++++++++++++++++++++++++++++ lib/simpletest/testpluginlib.php | 105 +++++++++++++++++++++- 2 files changed, 249 insertions(+), 2 deletions(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index bc145359146..4c47e4153a1 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -694,6 +694,41 @@ class available_update_checker { } } + /** + * The method being run via cron.php + */ + public function cron() { + global $CFG; + + if (!$this->cron_autocheck_enabled()) { + $this->cron_mtrace('Automatic check for available updates not enabled, skipping.'); + return; + } + + $now = $this->cron_current_timestamp(); + + if ($this->cron_has_fresh_fetch($now)) { + $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.'); + return; + } + + if ($this->cron_has_outdated_fetch($now)) { + $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', ''); + $this->cron_execute(); + return; + } + + $offset = $this->cron_execution_offset(); + $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time + if ($now > $start + $offset) { + $this->cron_mtrace('Regular daily check for available updates ... ', ''); + $this->cron_execute(); + return; + } + } + + /// end of public API ////////////////////////////////////////////////////// + /** * Makes cURL request to get data from the remote site * @@ -879,6 +914,117 @@ class available_update_checker { return $params; } + + /** + * Returns the current timestamp + * + * @return int the timestamp + */ + protected function cron_current_timestamp() { + return time(); + } + + /** + * Output cron debugging info + * + * @see mtrace() + * @param string $msg output message + * @param string $eol end of line + */ + protected function cron_mtrace($msg, $eol = PHP_EOL) { + mtrace($msg, $eol); + } + + /** + * Decide if the autocheck feature is disabled in the server setting + * + * @return bool true if autocheck enabled, false if disabled + */ + protected function cron_autocheck_enabled() { + if (empty($CFG->updateautocheck)) { + return false; + } else { + return true; + } + } + + /** + * Decide if the recently fetched data are still fresh enough + * + * @param int $now current timestamp + * @return bool true if no need to re-fetch, false otherwise + */ + protected function cron_has_fresh_fetch($now) { + $recent = $this->get_last_timefetched(); + + if (empty($recent)) { + return false; + } + + if ($now < $recent) { + $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!'); + return true; + } + + if ($now - $recent > HOURSECS) { + return false; + } + + return true; + } + + /** + * Decide if the fetch is outadated or even missing + * + * @param int $now current timestamp + * @return bool false if no need to re-fetch, true otherwise + */ + protected function cron_has_outdated_fetch($now) { + $recent = $this->get_last_timefetched(); + + if (empty($recent)) { + return true; + } + + if ($now < $recent) { + $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!'); + return false; + } + + if ($now - $recent > 48 * HOURSECS) { + return true; + } + + return false; + } + + /** + * Returns the cron execution offset for this site + * + * The main {@link self::cron()} is supposed to run every night in some random time + * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called + * execution offset, that is the amount of time after 01:00 AM. The offset value is + * initially generated randomly and then used consistently at the site. This way, the + * regular checks against the download.moodle.org server are spread in time. + * + * @return int the offset number of seconds from range 1 sec to 5 hours + */ + protected function cron_execution_offset() { + global $CFG; + + if (empty($CFG->updatecronoffset)) { + set_config('updatecronoffset', rand(1, 5 * HOURSECS)); + } + + return $CFG->updatecronoffset; + } + + /** + * Fetch available updates info and eventually send notification to site admins + */ + protected function cron_execute() { + // todo + } } diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index a15dded3c07..aaddc5bb422 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -111,7 +111,11 @@ class testable_plugin_manager extends plugin_manager { class testable_available_update_checker extends available_update_checker { /** @var replaces the default DB table storage for the fetched response */ - protected $responsestorage; + protected $fakeresponsestorage; + /** @var int stores the fake recentfetch value */ + public $fakerecentfetch = -1; + /** @var int stores the fake value of time() */ + public $fakecurrenttimestamp = -1; /** * Factory method for this class @@ -131,7 +135,7 @@ class testable_available_update_checker extends available_update_checker { } protected function store_response($response) { - $this->responsestorage = $response; + $this->fakeresponsestorage = $response; } protected function restore_response($forcereload = false) { @@ -148,6 +152,14 @@ class testable_available_update_checker extends available_update_checker { $this->currentplugins = $plugins; } + public function get_last_timefetched() { + if ($this->fakerecentfetch == -1) { + return parent::get_last_timefetched(); + } else { + return $this->fakerecentfetch; + } + } + private function get_fake_response() { $fakeresponse = array( 'status' => 'OK', @@ -196,8 +208,38 @@ class testable_available_update_checker extends available_update_checker { return json_encode($fakeresponse); } + + protected function cron_current_timestamp() { + if ($this->fakecurrenttimestamp == -1) { + return parent::cron_current_timestamp(); + } else { + return $this->fakecurrenttimestamp; + } + } + + protected function cron_mtrace($msg, $eol = PHP_EOL) { + } + + protected function cron_autocheck_enabled() { + return true; + } + + protected function cron_execution_offset() { + // autofetch should run by the first cron after 01:42 AM + return 42 * MINSECS; + } + + protected function cron_execute() { + throw new testable_available_update_checker_cron_executed('Cron executed but it should not!'); + } } +/** + * Exception used to detect {@link available_update_checker::cron_execute()} calls + */ +class testable_available_update_checker_cron_executed extends Exception { + +} /** * Tests of the basic API of the plugin manager @@ -256,4 +298,63 @@ class available_update_checker_test extends UnitTestCase { $updates = $provider->get_core_update_info(MATURITY_STABLE); $this->assertNull($updates); } + + /** + * If there are no fetched data yet, the first cron should fetch them + */ + public function test_cron_initial_fetch() { + $provider = testable_available_update_checker::instance(); + $provider->fakerecentfetch = null; + $provider->fakecurrenttimestamp = -1; + $this->expectException('testable_available_update_checker_cron_executed'); + $provider->cron(); + } + + /** + * If there is a fresh fetch available, no cron execution is expected + */ + public function test_cron_has_fresh_fetch() { + $provider = testable_available_update_checker::instance(); + $provider->fakerecentfetch = time() - 59 * MINSECS; // fetched an hour ago + $provider->fakecurrenttimestamp = -1; + $provider->cron(); + $this->assertTrue(true); // we should get here with no exception thrown + } + + /** + * If there is an outdated fetch, the cron execution is expected + */ + public function test_cron_has_outdated_fetch() { + $provider = testable_available_update_checker::instance(); + $provider->fakerecentfetch = time() - 49 * HOURSECS; // fetched 49 hours ago + $provider->fakecurrenttimestamp = -1; + $this->expectException('testable_available_update_checker_cron_executed'); + $provider->cron(); + } + + /** + * The first cron after 01:42 AM today should fetch the data + * + * @see testable_available_update_checker::cron_execution_offset() + */ + public function test_cron_offset_execution_not_yet() { + $provider = testable_available_update_checker::instance(); + $provider->fakerecentfetch = time() - 24 * HOURSECS; + $provider->fakecurrenttimestamp = mktime(1, 40, 02); // 01:40:02 AM + $provider->cron(); + $this->assertTrue(true); // we should get here with no exception thrown + } + + /** + * The first cron after 01:42 AM today should fetch the data + * + * @see testable_available_update_checker::cron_execution_offset() + */ + public function test_cron_offset_execution() { + $provider = testable_available_update_checker::instance(); + $provider->fakerecentfetch = time() - 24 * HOURSECS; + $provider->fakecurrenttimestamp = mktime(1, 45, 02); // 01:45:02 AM + $this->expectException('testable_available_update_checker_cron_executed'); + $provider->cron(); + } } From 7b35553b9b5963fdbcecdd4d75dc4906e0737e07 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Wed, 21 Mar 2012 22:54:15 +0100 Subject: [PATCH 24/36] MDL-20438 Introducing new compare_responses() method --- lib/pluginlib.php | 62 ++++++++++++++- lib/simpletest/testpluginlib.php | 129 +++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 4c47e4153a1..629f65f629e 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -828,6 +828,60 @@ class available_update_checker { } } + /** + * Compares two raw {@link $recentresponse} records and returns the list of changed updates + * + * This method is used to populate potential update info to be sent to site admins. + * + * @param null|array $old + * @param null|array $new + * @throws available_update_checker_exception + * @return array parts of $new['updates'] that have changed + */ + protected function compare_responses($old, $new) { + + if (is_null($new)) { + return array(); + } + + if (!array_key_exists('updates', $new)) { + throw new available_update_checker_exception('err_response_format'); + } + + if (is_null($old)) { + return $new['updates']; + } + + if (!array_key_exists('updates', $old)) { + throw new available_update_checker_exception('err_response_format'); + } + + $changes = array(); + + foreach ($new['updates'] as $newcomponent => $newcomponentupdates) { + if (empty($old['updates'][$newcomponent])) { + $changes[$newcomponent] = $newcomponentupdates; + continue; + } + foreach ($newcomponentupdates as $newcomponentupdate) { + $inold = false; + foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) { + if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) { + $inold = true; + } + } + if (!$inold) { + if (!isset($changes[$newcomponent])) { + $changes[$newcomponent] = array(); + } + $changes[$newcomponent][] = $newcomponentupdate; + } + } + } + + return $changes; + } + /** * Returns the URL to send update requests to * @@ -1023,7 +1077,13 @@ class available_update_checker { * Fetch available updates info and eventually send notification to site admins */ protected function cron_execute() { - // todo + + $this->restore_response(); + $previous = $this->recentresponse; + $this->fetch(); + $this->restore_response(true); + $current = $this->recentresponse; + $changes = $this->compare_responses($previous, $current); } } diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index aaddc5bb422..a9224c0051c 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -143,6 +143,10 @@ class testable_available_update_checker extends available_update_checker { $this->recentresponse = $this->decode_response($this->get_fake_response()); } + public function compare_responses($old, $new) { + return parent::compare_responses($old, $new); + } + protected function load_current_environment($forcereload=false) { } @@ -357,4 +361,129 @@ class available_update_checker_test extends UnitTestCase { $this->expectException('testable_available_update_checker_cron_executed'); $provider->cron(); } + + public function test_compare_responses_both_null() { + $provider = testable_available_update_checker::instance(); + $old = null; + $new = null; + $cmp = $provider->compare_responses($old, $new); + $this->assertIsA($cmp, 'array'); + $this->assertTrue(empty($cmp)); + } + + public function test_compare_responses_old_null() { + $provider = testable_available_update_checker::instance(); + $old = null; + $new = array( + 'updates' => array( + 'core' => array( + array( + 'version' => 2012060103 + ) + ) + ) + ); + $cmp = $provider->compare_responses($old, $new); + $this->assertIsA($cmp, 'array'); + $this->assertFalse(empty($cmp)); + $this->assertTrue(isset($cmp['core'][0]['version'])); + $this->assertEqual($cmp['core'][0]['version'], 2012060103); + } + + public function test_compare_responses_no_change() { + $provider = testable_available_update_checker::instance(); + $old = $new = array( + 'updates' => array( + 'core' => array( + array( + 'version' => 2012060104 + ), + array( + 'version' => 2012120100 + ) + ), + 'mod_foo' => array( + array( + 'version' => 2011010101 + ) + ) + ) + ); + $cmp = $provider->compare_responses($old, $new); + $this->assertIsA($cmp, 'array'); + $this->assertTrue(empty($cmp)); + } + + public function test_compare_responses_new_and_missing_update() { + $provider = testable_available_update_checker::instance(); + $old = array( + 'updates' => array( + 'core' => array( + array( + 'version' => 2012060104 + ) + ), + 'mod_foo' => array( + array( + 'version' => 2011010101 + ) + ) + ) + ); + $new = array( + 'updates' => array( + 'core' => array( + array( + 'version' => 2012060104 + ), + array( + 'version' => 2012120100 + ) + ) + ) + ); + $cmp = $provider->compare_responses($old, $new); + $this->assertIsA($cmp, 'array'); + $this->assertFalse(empty($cmp)); + $this->assertEqual(count($cmp), 1); + $this->assertEqual(count($cmp['core']), 1); + $this->assertEqual($cmp['core'][0]['version'], 2012120100); + } + + public function test_compare_responses_modified_update() { + $provider = testable_available_update_checker::instance(); + $old = array( + 'updates' => array( + 'mod_foo' => array( + array( + 'version' => 2011010101 + ) + ) + ) + ); + $new = array( + 'updates' => array( + 'mod_foo' => array( + array( + 'version' => 2011010102 + ) + ) + ) + ); + $cmp = $provider->compare_responses($old, $new); + $this->assertIsA($cmp, 'array'); + $this->assertFalse(empty($cmp)); + $this->assertEqual(count($cmp), 1); + $this->assertEqual(count($cmp['mod_foo']), 1); + $this->assertEqual($cmp['mod_foo'][0]['version'], 2011010102); + } + + public function test_compare_responses_invalid_format() { + $provider = testable_available_update_checker::instance(); + $broken = array( + 'status' => 'ERROR' // no 'updates' key here + ); + $this->expectException('available_update_checker_exception'); + $cmp = $provider->compare_responses($broken, $broken); + } } From c6f008e747cdbf97228c0f5bc659c8447714325a Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 22 Mar 2012 13:06:58 +0100 Subject: [PATCH 25/36] MDL-20438 dropping get_core_update_info() method There is now a single method get_update_info() used for both core and plugins. --- admin/index.php | 3 +- lib/pluginlib.php | 105 +++++++++++++++---------------- lib/simpletest/testpluginlib.php | 6 +- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/admin/index.php b/admin/index.php index 2a0d7b0c48b..b1c5c8e222a 100644 --- a/admin/index.php +++ b/admin/index.php @@ -387,7 +387,8 @@ $dbproblems = $DB->diagnose(); $maintenancemode = !empty($CFG->maintenance_enabled); $updateschecker = available_update_checker::instance(); -$availableupdates = $updateschecker->get_core_update_info($CFG->updateminmaturity); +$availableupdates = $updateschecker->get_update_info('core', + array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds)); $availableupdatesfetch = $updateschecker->get_last_timefetched(); admin_externalpage_setup('adminnotifications'); diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 629f65f629e..dbd5d811f16 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -634,40 +634,6 @@ class available_update_checker { $this->store_response($response); } - /** - * Returns the available update information for Moodle core - * - * The returned structure is an array of available_update_info objects or null - * if there no such info available. - * - * @param int $minmaturity minimal maturity level to return, returns all by default - * @return null|stdClass null or array of available_update_info objects - */ - public function get_core_update_info($minmaturity = 0) { - - $this->load_current_environment(); - - $updates = $this->get_update_info('core'); - if (empty($updates)) { - return null; - } - $return = array(); - foreach ($updates as $update) { - if (isset($update->maturity) and ($update->maturity < $minmaturity)) { - continue; - } - if ($update->version > $this->currentversion) { - $return[] = $update; - } - } - - if (empty($return)) { - return null; - } - - return $return; - } - /** * Returns the available update information for the given component * @@ -676,22 +642,54 @@ class available_update_checker { * component. Each update info is an object with at least one property called * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'. * + * For the 'core' component, the method returns real updates only (those with higher version). + * For all other components, the list of all known remote updates is returned and the caller + * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions. + * * @param string $component frankenstyle - * @return null|stdClass null or array of available_update_info objects + * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds' + * @return null|array null or array of available_update_info objects */ - public function get_update_info($component) { + public function get_update_info($component, array $options = array()) { + + if (!isset($options['minmaturity'])) { + $options['minmaturity'] = 0; + } + + if (!isset($options['notifybuilds'])) { + $options['notifybuilds'] = false; + } + + if ($component == 'core') { + $this->load_current_environment(); + } $this->restore_response(); - if (!empty($this->recentresponse['updates'][$component])) { - $updates = array(); - foreach ($this->recentresponse['updates'][$component] as $info) { - $updates[] = new available_update_info($component, $info); - } - return $updates; - } else { + if (empty($this->recentresponse['updates'][$component])) { return null; } + + $updates = array(); + foreach ($this->recentresponse['updates'][$component] as $info) { + $update = new available_update_info($component, $info); + if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) { + continue; + } + if ($component == 'core') { + if ($update->version <= $this->currentversion) { + continue; + } + // todo notifybuild check + } + $updates[] = $update; + } + + if (empty($updates)) { + return null; + } + + return $updates; } /** @@ -1427,7 +1425,17 @@ abstract class plugininfo_base { * @param available_update_checker $provider the class providing the available update info */ public function check_available_updates(available_update_checker $provider) { - $this->availableupdates = $provider->get_update_info($this->component); + global $CFG; + + if (isset($CFG->updateminmaturity)) { + $minmaturity = $CFG->updateminmaturity; + } else { + // this can happen during the very first upgrade to 2.3 + $minmaturity = MATURITY_STABLE; + } + + $this->availableupdates = $provider->get_update_info($this->component, + array('minmaturity' => $minmaturity)); } /** @@ -1440,23 +1448,14 @@ abstract class plugininfo_base { * @return array|null */ public function available_updates() { - global $CFG; if (empty($this->availableupdates) or !is_array($this->availableupdates)) { return null; } - if (!isset($CFG->updateminmaturity)) { - // this may happen during the very first upgrade to 2.3 - $CFG->updateminmaturity = MATURITY_STABLE; - } - $updates = array(); foreach ($this->availableupdates as $availableupdate) { - if (isset($availableupdate->maturity) and $availableupdate->maturity < $CFG->updateminmaturity) { - continue; - } if ($availableupdate->version > $this->versiondisk) { $updates[] = $availableupdate; } diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index a9224c0051c..9d55e1f1f23 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -291,15 +291,15 @@ class available_update_checker_test extends UnitTestCase { $this->assertTrue($provider instanceof available_update_checker); $provider->fake_current_environment(2012060102.00, '2.3', array()); - $updates = $provider->get_core_update_info(); + $updates = $provider->get_update_info('core'); $this->assertEqual(count($updates), 2); $provider->fake_current_environment(2012060103.00, '2.3', array()); - $updates = $provider->get_core_update_info(); + $updates = $provider->get_update_info('core'); $this->assertEqual(count($updates), 1); $provider->fake_current_environment(2012060103.00, '2.3', array()); - $updates = $provider->get_core_update_info(MATURITY_STABLE); + $updates = $provider->get_update_info('core', array('minmaturity' => MATURITY_STABLE)); $this->assertNull($updates); } From 19d11b3b3463483c913581a0e95a6a55d961943b Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 22 Mar 2012 13:51:12 +0100 Subject: [PATCH 26/36] MDL-20438 introducing the cron_notifications() method This method accepts all detected changes in cron-based fetch and picks those the site admins want to be informed about (based on the maturity level etc). --- lib/cronlib.php | 4 ++ lib/pluginlib.php | 66 +++++++++++++++++++++++++++++--- lib/simpletest/testpluginlib.php | 10 ++--- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/lib/cronlib.php b/lib/cronlib.php index 818a774bdc4..74fc4eff05d 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -380,6 +380,10 @@ function cron_run() { $registrationmanager->cron(); mtrace(get_string('siteupdatesend', 'hub')); + // If enabled, fetch information about available updates and eventually notify site admins + require_once($CFG->libdir.'/pluginlib.php'); + $updateschecker = available_update_checker::instance(); + $updateschecker->cron(); //cleanup old session linked tokens //deletes the session linked tokens that are over a day old. diff --git a/lib/pluginlib.php b/lib/pluginlib.php index dbd5d811f16..8d13c4c8473 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -831,14 +831,14 @@ class available_update_checker { * * This method is used to populate potential update info to be sent to site admins. * - * @param null|array $old - * @param null|array $new + * @param array $old + * @param array $new * @throws available_update_checker_exception * @return array parts of $new['updates'] that have changed */ - protected function compare_responses($old, $new) { + protected function compare_responses(array $old, array $new) { - if (is_null($new)) { + if (empty($new)) { return array(); } @@ -846,7 +846,7 @@ class available_update_checker { throw new available_update_checker_exception('err_response_format'); } - if (is_null($old)) { + if (empty($old)) { return $new['updates']; } @@ -1081,7 +1081,61 @@ class available_update_checker { $this->fetch(); $this->restore_response(true); $current = $this->recentresponse; - $changes = $this->compare_responses($previous, $current); + try { + $changes = $this->compare_responses($previous, $current); + $notifications = $this->cron_notifications($changes); + $this->cron_notify($notifications); + $this->cron_mtrace('OK'); + } catch (available_update_checker_exception $e) { + $this->cron_mtrace('FAILED!'); + } + } + + /** + * Given the list of changes in available updates, pick those to send to site admins + * + * @param array $changes as returned by {@link self::compare_responses()} + * @return array of available_update_info objects to send to site admins + */ + protected function cron_notifications(array $changes) { + global $CFG; + + $notifications = array(); + $pluginman = plugin_manager::instance(); + $plugins = $pluginman->get_plugins(true); + + foreach ($changes as $component => $componentchanges) { + $componentupdates = $this->get_update_info($component, + array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds)); + // notify only about those $componentchanges that are present in $componentupdates + // to respect the preferences + foreach ($componentchanges as $componentchange) { + foreach ($componentupdates as $componentupdate) { + if ($componentupdate->version == $componentchange['version']) { + if ($component == 'core') { + // in case of 'core' this is enough, we already know that the + // $componentupdate is a real update with higher version + $notifications[] = $componentupdate; + } else { + // use the plugin_manager to check if the reported $componentchange + // is a real update with higher version. such a real update must be + // present in the 'availableupdates' property of one of the component's + // available_update_info object + list($plugintype, $pluginname) = normalize_component($component); + if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) { + foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) { + if ($availableupdate->version == $componentchange['version']) { + $notifications[] = $componentupdate; + } + } + } + } + } + } + } + } + + return $notifications; } } diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index 9d55e1f1f23..40e86f9be7f 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -362,18 +362,18 @@ class available_update_checker_test extends UnitTestCase { $provider->cron(); } - public function test_compare_responses_both_null() { + public function test_compare_responses_both_empty() { $provider = testable_available_update_checker::instance(); - $old = null; - $new = null; + $old = array(); + $new = array(); $cmp = $provider->compare_responses($old, $new); $this->assertIsA($cmp, 'array'); $this->assertTrue(empty($cmp)); } - public function test_compare_responses_old_null() { + public function test_compare_responses_old_empty() { $provider = testable_available_update_checker::instance(); - $old = null; + $old = array(); $new = array( 'updates' => array( 'core' => array( From a77141a7dc0735c6bb5f33850bcc36f19febcdc9 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 22 Mar 2012 17:19:57 +0100 Subject: [PATCH 27/36] MDL-20438 send notifications via messaging API --- lang/en/admin.php | 4 ++ lang/en/moodle.php | 1 + lib/db/messages.php | 9 ++++ lib/pluginlib.php | 120 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 133 insertions(+), 1 deletion(-) diff --git a/lang/en/admin.php b/lang/en/admin.php index 1e9ed6bd92d..a83a8af3c50 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -969,11 +969,15 @@ $string['unsuspenduser'] = 'Activate user account'; $string['updateaccounts'] = 'Update existing accounts'; $string['updatecomponent'] = 'Update component'; $string['updateavailable'] = 'There is a newer Moodle version available!'; +$string['updateavailabledetailslink'] = 'See {$a->url} for more details'; +$string['updateavailableforplugin'] = 'There is a newer version for some of your plugins available!'; $string['updateavailable_moreinfo'] = 'More info...'; $string['updateavailable_release'] = 'Moodle {$a}'; $string['updateavailable_version'] = 'Version {$a}'; $string['updateavailablenot'] = 'Your Moodle code seems to be up-to-date'; $string['updatenotifications'] = 'Update notifications'; +$string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Settings block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.'; +$string['updatenotificationsubject'] = 'There are available updates for your Moodle site'; $string['updateautocheck'] = 'Automatically check for available updates'; $string['updateautocheck_desc'] = 'If enabled, your site will automatically check for available updates for both Moodle code and all installed extensions. If there is a new update available, a notification will be sent to site admins.'; $string['updateminmaturity'] = 'Required code maturity'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 764d5db4fb6..d76e8115b84 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -988,6 +988,7 @@ $string['memberincourse'] = 'People in the course'; $string['messagebody'] = 'Message body'; $string['messagedselectedusers'] = 'Selected users have been messaged and the recipient list has been reset.'; $string['messagedselectedusersfailed'] = 'Something went wrong while messaging selected users. Some may have received the email.'; +$string['messageprovider:availableupdate'] = 'Available update notifications'; $string['messageprovider:backup'] = 'Backup notifications'; $string['messageprovider:courserequestapproved'] = 'Course creation request approval notification'; $string['messageprovider:courserequested'] = 'Course creation request notification'; diff --git a/lib/db/messages.php b/lib/db/messages.php index db087fb5f3d..b736822c862 100644 --- a/lib/db/messages.php +++ b/lib/db/messages.php @@ -38,6 +38,15 @@ $messageproviders = array ( 'capability' => 'moodle/site:config' ), + // cron-based notifications about available moodle and/or additional plugin updates + 'availableupdate' => array( + 'capability' => 'moodle/site:config', + 'defaults' => array( + 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF + ), + + ), + 'instantmessage' => array ( 'defaults' => array( 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 8d13c4c8473..401a6de1793 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -1085,7 +1085,7 @@ class available_update_checker { $changes = $this->compare_responses($previous, $current); $notifications = $this->cron_notifications($changes); $this->cron_notify($notifications); - $this->cron_mtrace('OK'); + $this->cron_mtrace('done'); } catch (available_update_checker_exception $e) { $this->cron_mtrace('FAILED!'); } @@ -1137,6 +1137,124 @@ class available_update_checker { return $notifications; } + + /** + * Sends the given notifications to site admins via messaging API + * + * @param array $notifications array of available_update_info objects to send + */ + protected function cron_notify(array $notifications) { + global $CFG; + + if (empty($notifications)) { + return; + } + + $admins = get_admins(); + + if (empty($admins)) { + return; + } + + $this->cron_mtrace('sending notifications ... ', ''); + + $text = get_string('updatenotifications', 'core_admin') . PHP_EOL; + $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL; + + $coreupdates = array(); + $pluginupdates = array(); + + foreach($notifications as $notification) { + if ($notification->component == 'core') { + $coreupdates[] = $notification; + } else { + $pluginupdates[] = $notification; + } + } + + if (!empty($coreupdates)) { + $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL; + $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL; + $html .= html_writer::start_tag('ul') . PHP_EOL; + foreach ($coreupdates as $coreupdate) { + $html .= html_writer::start_tag('li'); + if (isset($coreupdate->release)) { + $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release); + $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release)); + } + if (isset($coreupdate->version)) { + $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version); + $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version); + } + if (isset($coreupdate->maturity)) { + $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')'; + $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')'; + } + $text .= PHP_EOL; + $html .= html_writer::end_tag('li') . PHP_EOL; + } + $text .= PHP_EOL; + $html .= html_writer::end_tag('ul') . PHP_EOL; + + $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php'); + $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL; + $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php')); + $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL; + } + + if (!empty($pluginupdates)) { + $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL; + $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL; + + $html .= html_writer::start_tag('ul') . PHP_EOL; + foreach ($pluginupdates as $pluginupdate) { + $html .= html_writer::start_tag('li'); + $text .= get_string('pluginname', $pluginupdate->component); + $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component)); + + $text .= ' ('.$pluginupdate->component.')'; + $html .= ' ('.$pluginupdate->component.')'; + + $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version); + $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version); + + $text .= PHP_EOL; + $html .= html_writer::end_tag('li') . PHP_EOL; + } + $text .= PHP_EOL; + $html .= html_writer::end_tag('ul') . PHP_EOL; + + $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'); + $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL; + $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php')); + $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL; + } + + $a = array('siteurl' => $CFG->wwwroot); + $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL; + $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot)); + $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a), + array('style' => 'font-size:smaller; color:#333;'))); + + $mainadmin = reset($admins); + + foreach ($admins as $admin) { + $message = new stdClass(); + $message->component = 'moodle'; + $message->name = 'availableupdate'; + $message->userfrom = $mainadmin; + $message->userto = $admin; + $message->subject = get_string('updatenotifications', 'core_admin'); + $message->fullmessage = $text; + $message->fullmessageformat = FORMAT_PLAIN; + $message->fullmessagehtml = $html; + $message->smallmessage = 'TODO'; + $message->notification = 0; + $message->contexturl = 'http://glum/admin/TODO'; + $message->contexturlname = 'View details TODO'; + message_send($message); + } + } } From 21cde6ac142e1a3518b0b1aa22b5fb9e2f959371 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 22 Mar 2012 17:20:59 +0100 Subject: [PATCH 28/36] MDL-20438 fixing the message_send() docblock This is not directly relevant to the issue. I just spotted it while working on the messaging API. Consider it as a well meant Trojan horse on this branch ;-) --- lib/messagelib.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/messagelib.php b/lib/messagelib.php index b86ac100095..5d2f6533a14 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -31,18 +31,21 @@ require_once(dirname(dirname(__FILE__)) . '/message/lib.php'); * This functions checks the message recipient's message processor configuration then * sends the message to the configured processors * - * Required parameter $eventdata structure: + * Required parameters of the $eventdata object: * component string component name. must exist in message_providers * name string message type name. must exist in message_providers * userfrom object|int the user sending the message * userto object|int the message recipient * subject string the message subject - * fullmessage - the full message in a given format - * fullmessageformat - the format if the full message (FORMAT_MOODLE, FORMAT_HTML, ..) - * fullmessagehtml - the full version (the message processor will choose with one to use) - * smallmessage - the small version of the message - * contexturl - if this is a notification then you can specify a url to view the event. For example the forum post the user is being notified of. - * contexturlname - the display text for contexturl + * fullmessage string the full message in a given format + * fullmessageformat int the format if the full message (FORMAT_MOODLE, FORMAT_HTML, ..) + * fullmessagehtml string the full version (the message processor will choose with one to use) + * smallmessage string the small version of the message + * + * Optional parameters of the $eventdata object: + * notification bool should the message be considered as a notification rather than a personal message + * contexturl string if this is a notification then you can specify a url to view the event. For example the forum post the user is being notified of. + * contexturlname string the display text for contexturl * * @category message * @param object $eventdata information about the message (component, userfrom, userto, ...) From 4442cc80362a9c013f5d924693e7378ea884d007 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 26 Mar 2012 11:59:14 +0200 Subject: [PATCH 29/36] MDL-20438 finishing the updatenotifybuilds feature Unit tests say that all. --- lib/pluginlib.php | 39 +++++++++++++++++++++++++++++--- lib/simpletest/testpluginlib.php | 36 +++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 401a6de1793..172ec5bc960 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -577,6 +577,8 @@ class available_update_checker { protected $recentresponse = null; /** @var null|string the numerical version of the local Moodle code */ protected $currentversion = null; + /** @var null|string the release info of the local Moodle code */ + protected $currentrelease = null; /** @var null|string branch of the local Moodle code */ protected $currentbranch = null; /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */ @@ -680,7 +682,9 @@ class available_update_checker { if ($update->version <= $this->currentversion) { continue; } - // todo notifybuild check + if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) { + continue; + } } $updates[] = $update; } @@ -899,7 +903,7 @@ class available_update_checker { } /** - * Sets the properties currentversion, currentbranch and currentplugins + * Sets the properties currentversion, currentrelease, currentbranch and currentplugins * * @param bool $forcereload */ @@ -913,7 +917,7 @@ class available_update_checker { require($CFG->dirroot.'/version.php'); $this->currentversion = $version; - + $this->currentrelease = $release; $this->currentbranch = moodle_major_version(true); $pluginman = plugin_manager::instance(); @@ -1255,6 +1259,35 @@ class available_update_checker { message_send($message); } } + + /** + * Compare two release labels and decide if they are the same + * + * @param string $remote release info of the available update + * @param null|string $local release info of the local code, defaults to $release defined in version.php + * @return boolean true if the releases declare the same minor+major version + */ + protected function is_same_release($remote, $local=null) { + + if (is_null($local)) { + $this->load_current_environment(); + $local = $this->currentrelease; + } + + $pattern = '/^([0-9\.\+]+)([^(]*)/'; + + preg_match($pattern, $remote, $remotematches); + preg_match($pattern, $local, $localmatches); + + $remotematches[1] = str_replace('+', '', $remotematches[1]); + $localmatches[1] = str_replace('+', '', $localmatches[1]); + + if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) { + return true; + } else { + return false; + } + } } diff --git a/lib/simpletest/testpluginlib.php b/lib/simpletest/testpluginlib.php index 40e86f9be7f..59cea4778fa 100644 --- a/lib/simpletest/testpluginlib.php +++ b/lib/simpletest/testpluginlib.php @@ -147,11 +147,16 @@ class testable_available_update_checker extends available_update_checker { return parent::compare_responses($old, $new); } + public function is_same_release($remote, $local=null) { + return parent::is_same_release($remote, $local); + } + protected function load_current_environment($forcereload=false) { } - public function fake_current_environment($version, $branch, array $plugins) { + public function fake_current_environment($version, $release, $branch, array $plugins) { $this->currentversion = $version; + $this->currentrelease = $release; $this->currentbranch = $branch; $this->currentplugins = $plugins; } @@ -290,15 +295,15 @@ class available_update_checker_test extends UnitTestCase { $provider = testable_available_update_checker::instance(); $this->assertTrue($provider instanceof available_update_checker); - $provider->fake_current_environment(2012060102.00, '2.3', array()); + $provider->fake_current_environment(2012060102.00, '2.3.2 (Build: 20121012)', '2.3', array()); $updates = $provider->get_update_info('core'); $this->assertEqual(count($updates), 2); - $provider->fake_current_environment(2012060103.00, '2.3', array()); + $provider->fake_current_environment(2012060103.00, '2.3.3 (Build: 20121212)', '2.3', array()); $updates = $provider->get_update_info('core'); $this->assertEqual(count($updates), 1); - $provider->fake_current_environment(2012060103.00, '2.3', array()); + $provider->fake_current_environment(2012060103.00, '2.3.3 (Build: 20121212)', '2.3', array()); $updates = $provider->get_update_info('core', array('minmaturity' => MATURITY_STABLE)); $this->assertNull($updates); } @@ -486,4 +491,27 @@ class available_update_checker_test extends UnitTestCase { $this->expectException('available_update_checker_exception'); $cmp = $provider->compare_responses($broken, $broken); } + + public function test_is_same_release_explicit() { + $provider = testable_available_update_checker::instance(); + $this->assertTrue($provider->is_same_release('2.3dev (Build: 20120323)', '2.3dev (Build: 20120323)')); + $this->assertTrue($provider->is_same_release('2.3dev (Build: 20120323)', '2.3dev (Build: 20120330)')); + $this->assertFalse($provider->is_same_release('2.3dev (Build: 20120529)', '2.3 (Build: 20120601)')); + $this->assertFalse($provider->is_same_release('2.3dev', '2.3 dev')); + $this->assertFalse($provider->is_same_release('2.3.1', '2.3')); + $this->assertFalse($provider->is_same_release('2.3.1', '2.3.2')); + $this->assertTrue($provider->is_same_release('2.3.2+', '2.3.2')); // yes, really + $this->assertTrue($provider->is_same_release('2.3.2 (Build: 123456)', '2.3.2+ (Build: 123457)')); + $this->assertFalse($provider->is_same_release('3.0 Community Edition', '3.0 Enterprise Edition')); + $this->assertTrue($provider->is_same_release('3.0 Community Edition', '3.0 Community Edition (Build: 20290101)')); + } + + public function test_is_same_release_implicit() { + $provider = testable_available_update_checker::instance(); + $provider->fake_current_environment(2012060102.00, '2.3.2 (Build: 20121012)', '2.3', array()); + $this->assertTrue($provider->is_same_release('2.3.2')); + $this->assertTrue($provider->is_same_release('2.3.2+')); + $this->assertTrue($provider->is_same_release('2.3.2+ (Build: 20121013)')); + $this->assertFalse($provider->is_same_release('2.4dev (Build: 20121012)')); + } } From 718eb2a59f3384118658fc6e2548c2d3494584fe Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 26 Mar 2012 12:00:24 +0200 Subject: [PATCH 30/36] MDL-20438 small fixes spotted during various testing --- lib/pluginlib.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 172ec5bc960..8923914ed72 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -997,6 +997,8 @@ class available_update_checker { * @return bool true if autocheck enabled, false if disabled */ protected function cron_autocheck_enabled() { + global $CFG; + if (empty($CFG->updateautocheck)) { return false; } else { @@ -1109,8 +1111,14 @@ class available_update_checker { $plugins = $pluginman->get_plugins(true); foreach ($changes as $component => $componentchanges) { + if (empty($componentchanges)) { + continue; + } $componentupdates = $this->get_update_info($component, array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds)); + if (empty($componentupdates)) { + continue; + } // notify only about those $componentchanges that are present in $componentupdates // to respect the preferences foreach ($componentchanges as $componentchange) { From 18e55c294d09d46ab0fc90e97f3c1fbf999caa87 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 26 Mar 2012 14:58:10 +0200 Subject: [PATCH 31/36] MDL-20438 removing the word 'extension' It was suggested not to use this term as in moodleverse, non-standard plugins have always been referred to as 'contributed plugins'. --- lang/en/admin.php | 6 +++--- lang/en/plugin.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lang/en/admin.php b/lang/en/admin.php index a83a8af3c50..062d68cbb48 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -979,11 +979,11 @@ $string['updatenotifications'] = 'Update notifications'; $string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configured to automatically check for available updates. You are receiving this message as the administrator of the site. You can disable automatic checks for available updates in the Site administration section of the Settings block. You can customize the delivery of this message via your personal Messaging setting in the My profile settings section.'; $string['updatenotificationsubject'] = 'There are available updates for your Moodle site'; $string['updateautocheck'] = 'Automatically check for available updates'; -$string['updateautocheck_desc'] = 'If enabled, your site will automatically check for available updates for both Moodle code and all installed extensions. If there is a new update available, a notification will be sent to site admins.'; +$string['updateautocheck_desc'] = 'If enabled, your site will automatically check for available updates for both Moodle code and all additional plugins. If there is a new update available, a notification will be sent to site admins.'; $string['updateminmaturity'] = 'Required code maturity'; -$string['updateminmaturity_desc'] = 'Notify about available updates only if the available code has the selected maturity level at least. Updates for extensions that do not declare their code maturity level are always reported regardless this setting.'; +$string['updateminmaturity_desc'] = 'Notify about available updates only if the available code has the selected maturity level at least. Updates for plugins that do not declare their code maturity level are always reported regardless this setting.'; $string['updatenotifybuilds'] = 'Notify about new builds'; -$string['updatenotifybuilds_desc'] = 'If enabled, the available update for Moodle code is also reported when a new build for the current version is available. Builds are continuous improvements of a given Moodle version. They are generally released every week. If disabled, the available update will be reported only when there is a higher version of Moodle released. Checks for extension plugins are not affected by this setting.'; +$string['updatenotifybuilds_desc'] = 'If enabled, the available update for Moodle code is also reported when a new build for the current version is available. Builds are continuous improvements of a given Moodle version. They are generally released every week. If disabled, the available update will be reported only when there is a higher version of Moodle released. Checks for plugins are not affected by this setting.'; $string['upgradestart'] = 'Upgrade'; $string['upgradeerror'] = 'Unknown error upgrading {$a->plugin} to version {$a->version}, can not continue.'; $string['upgradeforumread'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.
To use this functionality you need to update your tables.'; diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 4c6a575a243..130f9eb9bd7 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -37,7 +37,7 @@ $string['noneinstalled'] = 'No plugins of this type are installed'; $string['notes'] = 'Notes'; $string['numtotal'] = 'Installed: {$a}'; $string['numdisabled'] = 'Disabled: {$a}'; -$string['numextension'] = 'Extensions: {$a}'; +$string['numextension'] = 'Contributions: {$a}'; $string['numupdatable'] = 'Updates available: {$a}'; $string['otherplugin'] = '{$a->component}'; $string['otherpluginversion'] = '{$a->component} ({$a->version})'; @@ -55,7 +55,7 @@ $string['settings'] = 'Settings'; $string['somehighlighted'] = 'Number of plugins requiring attention during this upgrade: {$a}'; $string['somehighlightedinfo'] = 'Display the full list of installed plugins'; $string['source'] = 'Source'; -$string['sourceext'] = 'Extension'; +$string['sourceext'] = 'Contributed'; $string['sourcestd'] = 'Standard'; $string['status'] = 'Status'; $string['status_delete'] = 'To be deleted'; From 5344ddd1161d3880a8a5e009926e7fb83c169397 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 26 Mar 2012 15:59:25 +0200 Subject: [PATCH 32/36] MDL-20438 warn the admin that we are just showing available updates This warning is displayed so that folks do not expect any sort of auto-update ala Wordpress (yet). --- admin/renderer.php | 5 +++++ lang/en/admin.php | 6 +++++- lib/pluginlib.php | 17 +++++++++++++++++ theme/base/style/admin.css | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/admin/renderer.php b/admin/renderer.php index b21c33c3cd2..8bed3910dfa 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -195,6 +195,11 @@ class core_admin_renderer extends plugin_renderer_base { $output .= $this->upgrade_reload($reloadurl); if ($pluginman->all_plugins_ok($version)) { + if ($pluginman->some_plugins_updatable()) { + $output .= $this->container_start('upgradepluginsinfo'); + $output .= $this->help_icon('upgradepluginsinfo', 'core_admin', get_string('upgradepluginsfirst', 'core_admin')); + $output .= $this->container_end(); + } $button = new single_button($continueurl, get_string('upgradestart', 'admin'), 'get'); $button->class = 'continuebutton'; $output .= $this->render($button); diff --git a/lang/en/admin.php b/lang/en/admin.php index 062d68cbb48..762ec715517 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -984,7 +984,11 @@ $string['updateminmaturity'] = 'Required code maturity'; $string['updateminmaturity_desc'] = 'Notify about available updates only if the available code has the selected maturity level at least. Updates for plugins that do not declare their code maturity level are always reported regardless this setting.'; $string['updatenotifybuilds'] = 'Notify about new builds'; $string['updatenotifybuilds_desc'] = 'If enabled, the available update for Moodle code is also reported when a new build for the current version is available. Builds are continuous improvements of a given Moodle version. They are generally released every week. If disabled, the available update will be reported only when there is a higher version of Moodle released. Checks for plugins are not affected by this setting.'; -$string['upgradestart'] = 'Upgrade'; +$string['upgradestart'] = 'Upgrade Moodle database now'; +$string['upgradepluginsfirst'] = 'You have to download and install available updates manually'; +$string['upgradepluginsinfo'] = 'Updating plugins'; +$string['upgradepluginsinfo_help'] = 'There are available updates for some of your plugins. Moodle does not update them automatically yet. You have to download the update and install it at your server manually.'; +$string['upgradepluginsinfo_link'] = 'admin/upgradepluginsinfo'; $string['upgradeerror'] = 'Unknown error upgrading {$a->plugin} to version {$a->version}, can not continue.'; $string['upgradeforumread'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts.
To use this functionality you need to update your tables.'; $string['upgradeforumreadinfo'] = 'A new feature has been added in Moodle 1.5 to track read/unread forum posts. To use this functionality you need to update your tables with all the tracking information for existing posts. Depending on the size of your site this can take a long time (hours) and can be quite taxing on the database, so it\'s best to do it during a quiet period. However, your site will continue functioning during this upgrade and users won\'t be affected. Once you start this process you should let it finish (keep your browser window open). However, if you stop the process by closing the window: don\'t worry, you can start over.

Do you want to start the upgrading process now?'; diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 8923914ed72..845de2ee8ff 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -298,6 +298,23 @@ class plugin_manager { return true; } + /** + * Checks if there are some plugins with a known available update + * + * @return bool true if there is at least one available update + */ + public function some_plugins_updatable() { + foreach ($this->get_plugins() as $type => $plugins) { + foreach ($plugins as $plugin) { + if ($plugin->available_updates()) { + return true; + } + } + } + + return false; + } + /** * Defines a list of all plugins that were originally shipped in the standard Moodle distribution, * but are not anymore and are deleted during upgrades. diff --git a/theme/base/style/admin.css b/theme/base/style/admin.css index c06d82075bf..58ba4e7a451 100644 --- a/theme/base/style/admin.css +++ b/theme/base/style/admin.css @@ -220,6 +220,7 @@ #page-admin-index #plugins-check .status .pluginupdateinfo span, #page-admin-index #plugins-check .status .pluginupdateinfo a {padding-right:1em;} #page-admin-index #plugins-check .status .pluginupdateinfo .separator {border-left:1px dotted #333;} +#page-admin-index .upgradepluginsinfo {text-align:center;} /** Plugins management */ #page-admin-plugins .checkforupdates {margin:0 auto 1em;text-align:center;} From a687fcac7a37025b987f1f6993af7800d985e019 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 26 Mar 2012 16:07:02 +0200 Subject: [PATCH 33/36] MDL-20438 added a link to toggle the plugins check page back to the shorter list --- admin/renderer.php | 4 ++++ lang/en/plugin.php | 1 + 2 files changed, 5 insertions(+) diff --git a/admin/renderer.php b/admin/renderer.php index 8bed3910dfa..20cbb084d67 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -667,6 +667,10 @@ class core_admin_renderer extends plugin_renderer_base { $out .= html_writer::link(new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1)), get_string('somehighlightedinfo', 'core_plugin')); + } else { + $out .= html_writer::link(new moodle_url('/admin/index.php', + array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0)), + get_string('somehighlightedonly', 'core_plugin')); } $out .= $this->output->container_end(); } diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 130f9eb9bd7..1b6ae6cbd26 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -54,6 +54,7 @@ $string['rootdir'] = 'Directory'; $string['settings'] = 'Settings'; $string['somehighlighted'] = 'Number of plugins requiring attention during this upgrade: {$a}'; $string['somehighlightedinfo'] = 'Display the full list of installed plugins'; +$string['somehighlightedonly'] = 'Display only plugins requiring your attention'; $string['source'] = 'Source'; $string['sourceext'] = 'Contributed'; $string['sourcestd'] = 'Standard'; From 8342171b5240efdc7330c01b98175e8728572165 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 26 Mar 2012 16:24:22 +0200 Subject: [PATCH 34/36] MDL-20438 reintroducing plugin_management_page renderer method See http://moodle.org/local/chatlogs/index.php?conversationid=9784#c340340 for the whole saga. --- admin/plugins.php | 16 +--------------- admin/renderer.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/admin/plugins.php b/admin/plugins.php index 9fc100200ff..943f23a5ea8 100644 --- a/admin/plugins.php +++ b/admin/plugins.php @@ -43,18 +43,4 @@ if ($fetchremote) { } $output = $PAGE->get_renderer('core', 'admin'); - -echo $output->header(); -echo $output->heading(get_string('pluginsoverview', 'core_admin')); -echo $output->plugins_overview_panel($pluginman); - -echo $output->container_start('checkforupdates'); -echo $output->single_button(new moodle_url($PAGE->url, array('fetchremote' => 1)), get_string('checkforupdates', 'core_plugin')); -if ($timefetched = $checker->get_last_timefetched()) { - echo $output->container(get_string('checkforupdateslast', 'core_plugin', - userdate($timefetched, get_string('strftimedatetime', 'core_langconfig')))); -} -echo $output->container_end(); - -echo $output->box($output->plugins_control_panel($pluginman), 'generalbox'); -echo $output->footer(); +echo $output->plugin_management_page($pluginman, $checker); diff --git a/admin/renderer.php b/admin/renderer.php index 20cbb084d67..f6a2c111f4e 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -248,6 +248,34 @@ class core_admin_renderer extends plugin_renderer_base { return $output; } + /** + * Display the plugin management page (admin/plugins.php). + * + * @param plugin_manager $pluginman + * @param available_update_checker $checker + * @return string HTML to output. + */ + public function plugin_management_page(plugin_manager $pluginman, available_update_checker $checker) { + $output = ''; + + $output .= $this->header(); + $output .= $this->heading(get_string('pluginsoverview', 'core_admin')); + $output .= $this->plugins_overview_panel($pluginman); + + $output .= $this->container_start('checkforupdates'); + $output .= $this->single_button(new moodle_url($this->page->url, array('fetchremote' => 1)), get_string('checkforupdates', 'core_plugin')); + if ($timefetched = $checker->get_last_timefetched()) { + $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', + userdate($timefetched, get_string('strftimedatetime', 'core_langconfig')))); + } + $output .= $this->container_end(); + + $output .= $this->box($this->plugins_control_panel($pluginman), 'generalbox'); + $output .= $this->footer(); + + return $output; + } + /** * Display the plugin management page (admin/environment.php). * @param array $versions From cd89994d88ebe6663af8c3c1c257fec250fd36e9 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Mon, 26 Mar 2012 16:32:18 +0200 Subject: [PATCH 35/36] MDL-20438 getting rid of left to-do notes --- lib/pluginlib.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 845de2ee8ff..758d0bddf23 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -1277,10 +1277,8 @@ class available_update_checker { $message->fullmessage = $text; $message->fullmessageformat = FORMAT_PLAIN; $message->fullmessagehtml = $html; - $message->smallmessage = 'TODO'; - $message->notification = 0; - $message->contexturl = 'http://glum/admin/TODO'; - $message->contexturlname = 'View details TODO'; + $message->smallmessage = get_string('updatenotifications', 'core_admin'); + $message->notification = 1; message_send($message); } } From 5a08e363f09a14d30c40012c6517d4b1d16f90a4 Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Fri, 30 Mar 2012 11:08:48 +0200 Subject: [PATCH 36/36] MDL-20438 removing sesskey checks from places where there is no sesskey support guaranteed yet Thanks Petr Skoda for spotting this during the peer-review. --- admin/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/index.php b/admin/index.php index b1c5c8e222a..3f8ab67260c 100644 --- a/admin/index.php +++ b/admin/index.php @@ -235,7 +235,7 @@ if ($version > $CFG->version) { // upgrade $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1)); if ($fetchupdates) { - require_sesskey(); + // no sesskey support guaranteed here available_update_checker::instance()->fetch(); redirect($reloadurl); } @@ -274,7 +274,7 @@ if (moodle_needs_upgrading()) { $PAGE->set_cacheable(false); if ($fetchupdates) { - require_sesskey(); + // no sesskey support guaranteed here available_update_checker::instance()->fetch(); redirect($PAGE->url); }