Commit bde002b8 authored by Petr Škoda's avatar Petr Škoda
Browse files

MDL-41437 rework plugin_manager caching and version info in blocks and modules

This patch includes:

* version column removed from modules table, now using standard config, this allows decimal version for modules
* version column removed from block table, now using standard config, this allows decimal version for blocks
* module version.php can safely use $plugins instead of module
* new plugin_manager bulk caching, this should help with MUC performance when logged in as admin
* all missing plugins are now in plugin overview (previously only blocks and modules)
* simplified code and improved coding style
* reworked plugin_manager unit tests - now using real plugins instead of mocks
* unit tests now fail if any plugin does not contain proper version.php file
* allow uninstall of deleted filters
parent 81881cb9
<?php
$module->version = 2013041900;
$module->requires = 2012010100;
$module->component = 'mod_new';
<?php
$plugin->version = 2013041103;
$plugin->requires = 2013010100;
$plugin->component = 'quxcat_one';
$plugin->dependencies = array('bazmeg_one' => 2013010100);
<?php
$plugin->version = 2013041103;
$plugin->requires = 2013010100;
$plugin->component = 'mod_qux';
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for plugin manager class.
*
* @package core
* @category phpunit
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir.'/pluginlib.php');
/**
* Tests of the basic API of the plugin manager.
*/
class core_plugin_manager_testcase extends advanced_testcase {
public function test_instance() {
$pluginman = plugin_manager::instance();
$this->assertInstanceOf('plugin_manager', $pluginman);
$pluginman2 = plugin_manager::instance();
$this->assertSame($pluginman, $pluginman2);
}
public function test_reset_caches() {
// Make sure there are no warnings or errors.
plugin_manager::reset_caches();
}
public function test_get_plugin_types() {
// Make sure there are no warnings or errors.
$types = plugin_manager::instance()->get_plugin_types();
$this->assertInternalType('array', $types);
foreach ($types as $type => $fulldir) {
$this->assertFileExists($fulldir);
}
}
public function test_get_installed_plugins() {
$types = plugin_manager::instance()->get_plugin_types();
foreach ($types as $type => $fulldir) {
$installed = plugin_manager::instance()->get_installed_plugins($type);
foreach ($installed as $plugin => $version) {
$this->assertRegExp('/^[a-z]+[a-z0-9_]*$/', $plugin);
$this->assertTrue(is_numeric($version), 'All plugins should have a version, plugin '.$type.'_'.$plugin.' does not have version info.');
}
}
}
public function test_get_enabled_plugins() {
$types = plugin_manager::instance()->get_plugin_types();
foreach ($types as $type => $fulldir) {
$enabled = plugin_manager::instance()->get_enabled_plugins($type);
if (is_array($enabled)) {
foreach ($enabled as $key => $val) {
$this->assertRegExp('/^[a-z]+[a-z0-9_]*$/', $key);
$this->assertSame($key, $val);
}
} else {
$this->assertNull($enabled);
}
}
}
public function test_get_present_plugins() {
$types = plugin_manager::instance()->get_plugin_types();
foreach ($types as $type => $fulldir) {
$present = plugin_manager::instance()->get_present_plugins($type);
if (is_array($present)) {
foreach ($present as $plugin => $version) {
$this->assertRegExp('/^[a-z]+[a-z0-9_]*$/', $plugin, 'All plugins are supposed to have version.php file.');
$this->assertInternalType('object', $version);
$this->assertTrue(is_numeric($version->version), 'All plugins should have a version, plugin '.$type.'_'.$plugin.' does not have version info.');
}
} else {
// No plugins of this type exist.
$this->assertNull($present);
}
}
}
public function test_get_plugins() {
$plugininfos = plugin_manager::instance()->get_plugins();
foreach ($plugininfos as $type => $infos) {
foreach ($infos as $name => $info) {
$this->assertInstanceOf('plugininfo_base', $info);
}
}
}
public function test_get_plugins_of_type() {
$plugininfos = plugin_manager::instance()->get_plugins();
foreach ($plugininfos as $type => $infos) {
$this->assertSame($infos, plugin_manager::instance()->get_plugins_of_type($type));
}
}
public function test_get_subplugins_of_plugin() {
global $CFG;
// Any standard plugin with subplugins is suitable.
$this->assertFileExists("$CFG->dirroot/lib/editor/tinymce", 'TinyMCE is not present.');
$subplugins = plugin_manager::instance()->get_subplugins_of_plugin('editor_tinymce');
foreach ($subplugins as $component => $info) {
$this->assertInstanceOf('plugininfo_base', $info);
}
}
public function test_get_subplugins() {
// Tested already indirectly from test_get_subplugins_of_plugin().
$subplugins = plugin_manager::instance()->get_subplugins();
$this->assertInternalType('array', $subplugins);
}
public function test_get_parent_of_subplugin() {
global $CFG;
// Any standard plugin with subplugins is suitable.
$this->assertFileExists("$CFG->dirroot/lib/editor/tinymce", 'TinyMCE is not present.');
$parent = plugin_manager::instance()->get_parent_of_subplugin('tinymce');
$this->assertSame('editor_tinymce', $parent);
}
public function test_plugin_name() {
global $CFG;
// Any standard plugin is suitable.
$this->assertFileExists("$CFG->dirroot/lib/editor/tinymce", 'TinyMCE is not present.');
$name = plugin_manager::instance()->plugin_name('editor_tinymce');
$this->assertSame(get_string('pluginname', 'editor_tinymce'), $name);
}
public function test_plugintype_name() {
$name = plugin_manager::instance()->plugintype_name('editor');
$this->assertSame(get_string('type_editor', 'core_plugin'), $name);
}
public function test_plugintype_name_plural() {
$name = plugin_manager::instance()->plugintype_name_plural('editor');
$this->assertSame(get_string('type_editor_plural', 'core_plugin'), $name);
}
public function test_get_plugin_info() {
global $CFG;
// Any standard plugin is suitable.
$this->assertFileExists("$CFG->dirroot/lib/editor/tinymce", 'TinyMCE is not present.');
$info = plugin_manager::instance()->get_plugin_info('editor_tinymce');
$this->assertInstanceOf('plugininfo_editor', $info);
}
public function test_can_uninstall_plugin() {
global $CFG;
// Any standard plugin that is required by some other standard plugin is ok.
$this->assertFileExists("$CFG->dirroot/$CFG->admin/tool/assignmentupgrade", 'assign upgrade tool is not present');
$this->assertFileExists("$CFG->dirroot/mod/assign", 'assign module is not present');
$this->assertFalse(plugin_manager::instance()->can_uninstall_plugin('mod_assign'));
$this->assertTrue(plugin_manager::instance()->can_uninstall_plugin('tool_assignmentupgrade'));
}
public function test_plugin_states() {
global $CFG;
$this->resetAfterTest();
// Any standard plugin that is ok.
$this->assertFileExists("$CFG->dirroot/mod/assign", 'assign module is not present');
$this->assertFileExists("$CFG->dirroot/mod/forum", 'forum module is not present');
$this->assertFileExists("$CFG->dirroot/$CFG->admin/tool/phpunit", 'phpunit tool is not present');
$this->assertFileNotExists("$CFG->dirroot/mod/xxxxxxx");
$this->assertFileNotExists("$CFG->dirroot/enrol/autorize");
// Ready for upgrade.
$assignversion = get_config('mod_assign', 'version');
set_config('version', $assignversion - 1, 'mod_assign');
// Downgrade problem.
$forumversion = get_config('mod_forum', 'version');
set_config('version', $forumversion + 1, 'mod_forum');
// Not installed yet.
unset_config('version', 'tool_phpunit');
// Missing already installed.
set_config('version', 2013091300, 'mod_xxxxxxx');
// Deleted present.
set_config('version', 2013091300, 'enrol_authorize');
plugin_manager::reset_caches();
$plugininfos = plugin_manager::instance()->get_plugins();
foreach ($plugininfos as $type => $infos) {
foreach ($infos as $name => $info) {
/** @var plugininfo_base $info */
if ($info->component === 'mod_assign') {
$this->assertSame(plugin_manager::PLUGIN_STATUS_UPGRADE, $info->get_status(), 'Invalid '.$info->component.' state');
} else if ($info->component === 'mod_forum') {
$this->assertSame(plugin_manager::PLUGIN_STATUS_DOWNGRADE, $info->get_status(), 'Invalid '.$info->component.' state');
} else if ($info->component === 'tool_phpunit') {
$this->assertSame(plugin_manager::PLUGIN_STATUS_NEW, $info->get_status(), 'Invalid '.$info->component.' state');
} else if ($info->component === 'mod_xxxxxxx') {
$this->assertSame(plugin_manager::PLUGIN_STATUS_MISSING, $info->get_status(), 'Invalid '.$info->component.' state');
} else if ($info->component === 'enrol_authorize') {
$this->assertSame(plugin_manager::PLUGIN_STATUS_DELETE, $info->get_status(), 'Invalid '.$info->component.' state');
} else {
$this->assertSame(plugin_manager::PLUGIN_STATUS_UPTODATE, $info->get_status(), 'Invalid '.$info->component.' state');
}
}
}
}
}
......@@ -36,6 +36,9 @@ information provided here is intended especially for developers.
Use core_user::get_noreply_user() and core_user::get_support_user() to get noreply and support user's respectively.
Real users can be used as noreply/support users by setting $CFG->noreplyuserid and $CFG->supportuserid
* New function readfile_allow_large() in filelib.php for use when very large files may need sending to user.
* Use plugin_manager::reset_caches() when changing visibility of plugins.
* Implement new method get_enabled_plugins() method in subplugin info classes.
* Each plugin should include version information in version.php.
DEPRECATIONS:
Various previously deprecated functions have now been altered to throw DEBUG_DEVELOPER debugging notices
......
......@@ -222,21 +222,25 @@ function upgrade_main_savepoint($result, $version, $allowabort=true) {
function upgrade_mod_savepoint($result, $version, $modname, $allowabort=true) {
global $DB;
$component = 'mod_'.$modname;
if (!$result) {
throw new upgrade_exception("mod_$modname", $version);
throw new upgrade_exception($component, $version);
}
$dbversion = $DB->get_field('config_plugins', 'value', array('plugin'=>$component, 'name'=>'version'));
if (!$module = $DB->get_record('modules', array('name'=>$modname))) {
print_error('modulenotexist', 'debug', '', $modname);
}
if ($module->version >= $version) {
if ($dbversion >= $version) {
// something really wrong is going on in upgrade script
throw new downgrade_exception("mod_$modname", $module->version, $version);
throw new downgrade_exception($component, $dbversion, $version);
}
$module->version = $version;
$DB->update_record('modules', $module);
upgrade_log(UPGRADE_LOG_NORMAL, "mod_$modname", 'Upgrade savepoint reached');
set_config('version', $version, $component);
upgrade_log(UPGRADE_LOG_NORMAL, $component, 'Upgrade savepoint reached');
// reset upgrade timeout to default
upgrade_set_timeout();
......@@ -262,21 +266,25 @@ function upgrade_mod_savepoint($result, $version, $modname, $allowabort=true) {
function upgrade_block_savepoint($result, $version, $blockname, $allowabort=true) {
global $DB;
$component = 'block_'.$blockname;
if (!$result) {
throw new upgrade_exception("block_$blockname", $version);
throw new upgrade_exception($component, $version);
}
$dbversion = $DB->get_field('config_plugins', 'value', array('plugin'=>$component, 'name'=>'version'));
if (!$block = $DB->get_record('block', array('name'=>$blockname))) {
print_error('blocknotexist', 'debug', '', $blockname);
}
if ($block->version >= $version) {
if ($dbversion >= $version) {
// something really wrong is going on in upgrade script
throw new downgrade_exception("block_$blockname", $block->version, $version);
throw new downgrade_exception($component, $dbversion, $version);
}
$block->version = $version;
$DB->update_record('block', $block);
upgrade_log(UPGRADE_LOG_NORMAL, "block_$blockname", 'Upgrade savepoint reached');
set_config('version', $version, $component);
upgrade_log(UPGRADE_LOG_NORMAL, $component, 'Upgrade savepoint reached');
// reset upgrade timeout to default
upgrade_set_timeout();
......@@ -301,16 +309,19 @@ function upgrade_block_savepoint($result, $version, $blockname, $allowabort=true
* @return void
*/
function upgrade_plugin_savepoint($result, $version, $type, $plugin, $allowabort=true) {
global $DB;
$component = $type.'_'.$plugin;
if (!$result) {
throw new upgrade_exception($component, $version);
}
$installedversion = get_config($component, 'version');
if ($installedversion >= $version) {
$dbversion = $DB->get_field('config_plugins', 'value', array('plugin'=>$component, 'name'=>'version'));
if ($dbversion >= $version) {
// Something really wrong is going on in the upgrade script
throw new downgrade_exception($component, $installedversion, $version);
throw new downgrade_exception($component, $dbversion, $version);
}
set_config('version', $version, $component);
upgrade_log(UPGRADE_LOG_NORMAL, $component, 'Upgrade savepoint reached');
......@@ -338,6 +349,7 @@ function upgrade_stale_php_files_present() {
$someexamplesofremovedfiles = array(
// removed in 2.6dev
'/admin/block.php',
'/admin/oacleanup.php',
// removed in 2.5dev
'/backup/lib.php',
......@@ -402,12 +414,10 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
}
$plugin = new stdClass();
$module = new stdClass(); // Prevent some notices when plugin placed in wrong directory.
$plugin->version = null;
$module = $plugin; // Prevent some notices when plugin placed in wrong directory.
require($fullplug.'/version.php'); // defines $plugin with version etc
if (!isset($plugin->version) and isset($module->version)) {
$plugin = $module;
}
unset($module);
// if plugin tells us it's full name we may check the location
if (isset($plugin->component)) {
......@@ -425,7 +435,6 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
$plugin->name = $plug;
$plugin->fullname = $component;
if (!empty($plugin->requires)) {
if ($plugin->requires > $CFG->version) {
throw new upgrade_requires_exception($component, $plugin->version, $CFG->version, $plugin->requires);
......@@ -457,7 +466,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
}
}
$installedversion = get_config($plugin->fullname, 'version');
$installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
if (empty($installedversion)) { // new installation
$startcallback($component, true, $verbose);
......@@ -503,7 +512,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
$result = true;
}
$installedversion = get_config($plugin->fullname, 'version');
$installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
if ($installedversion < $plugin->version) {
// store version if not already there
upgrade_plugin_savepoint($result, $plugin->version, $type, $plug, false);
......@@ -516,6 +525,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
events_update_definition($component);
message_update_providers($component);
if ($type === 'message') {
// Ugly hack!
message_update_processors($plug);
}
upgrade_plugin_mnet_functions($component);
......@@ -555,36 +565,34 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
throw new plugin_defective_exception($component, 'Missing version.php');
}
$module = new stdClass();
$plugin = new stdClass(); // Prevent some notices when plugin placed in wrong directory.
require($fullmod .'/version.php'); // defines $module with version etc
if (!isset($module->version) and isset($plugin->version)) {
$module = $plugin;
}
$plugin = new stdClass();
$plugin->version = null;
$module = $plugin;
require($fullmod .'/version.php'); // Defines $module/$plugin with version etc.
$plugin = clone($module);
unset($module->version);
unset($module->component);
unset($module->dependencies);
unset($module->release);
// if plugin tells us it's full name we may check the location
if (isset($module->component)) {
if ($module->component !== $component) {
if (isset($plugin->component)) {
if ($plugin->component !== $component) {
$current = str_replace($CFG->dirroot, '$CFG->dirroot', $fullmod);
$expected = str_replace($CFG->dirroot, '$CFG->dirroot', core_component::get_component_directory($module->component));
$expected = str_replace($CFG->dirroot, '$CFG->dirroot', core_component::get_component_directory($plugin->component));
throw new plugin_misplaced_exception($component, $expected, $current);
}
}
if (empty($module->version)) {
if (isset($module->version)) {
// Version is empty but is set - it means its value is 0 or ''. Let us skip such module.
// This is intended for developers so they can work on the early stages of the module.
continue;
}
if (empty($plugin->version)) {
// Version must be always set now!
throw new plugin_defective_exception($component, 'Missing version value in version.php');
}
if (!empty($module->requires)) {
if ($module->requires > $CFG->version) {
throw new upgrade_requires_exception($component, $module->version, $CFG->version, $module->requires);
} else if ($module->requires < 2010000000) {
if (!empty($plugin->requires)) {
if ($plugin->requires > $CFG->version) {
throw new upgrade_requires_exception($component, $plugin->version, $CFG->version, $plugin->requires);
} else if ($plugin->requires < 2010000000) {
throw new plugin_defective_exception($component, 'Plugin is not compatible with Moodle 2.x or later.');
}
}
......@@ -600,7 +608,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
$module->name = $mod; // The name MUST match the directory
$currmodule = $DB->get_record('modules', array('name'=>$module->name));
$installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
if (file_exists($fullmod.'/db/install.php')) {
if (get_config($module->name, 'installrunning')) {
......@@ -622,7 +630,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
}
}
if (empty($currmodule->version)) {
if (empty($installedversion)) {
$startcallback($component, true, $verbose);
/// Execute install.xml (XMLDB) - must be present in all modules
......@@ -630,6 +638,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
/// Add record into modules table - may be needed in install.php already
$module->id = $DB->insert_record('modules', $module);
upgrade_mod_savepoint(true, $plugin->version, $module->name, false);
/// Post installation hook - optional
if (file_exists("$fullmod/db/install.php")) {
......@@ -651,22 +660,23 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
$endcallback($component, true, $verbose);
} else if ($currmodule->version < $module->version) {
} else if ($installedversion < $plugin->version) {
/// If versions say that we need to upgrade but no upgrade files are available, notify and continue
$startcallback($component, false, $verbose);
if (is_readable($fullmod.'/db/upgrade.php')) {
require_once($fullmod.'/db/upgrade.php'); // defines new upgrading function
$newupgrade_function = 'xmldb_'.$module->name.'_upgrade';
$result = $newupgrade_function($currmodule->version, $module);
$result = $newupgrade_function($installedversion, $module);
} else {
$result = true;
}
$installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
$currmodule = $DB->get_record('modules', array('name'=>$module->name));
if ($currmodule->version < $module->version) {
if ($installedversion < $plugin->version) {
// store version if not already there
upgrade_mod_savepoint($result, $module->version, $mod, false);
upgrade_mod_savepoint($result, $plugin->version, $mod, false);
}
// update cron flag if needed
......@@ -684,8 +694,8 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
$endcallback($component, false, $verbose);
} else if ($currmodule->version > $module->version) {
throw new downgrade_exception($component, $currmodule->version, $module->version);
} else if ($installedversion > $plugin->version) {
throw new downgrade_exception($component, $installedversion, $plugin->version);
}
}
}
......@@ -731,24 +741,30 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
throw new plugin_defective_exception('block/'.$blockname, 'Missing version.php file.');
}
$plugin = new stdClass();
$module = new stdClass(); // Prevent some notices when module placed in wrong directory.
$plugin->version = NULL;
$plugin->version = null;
$plugin->cron = 0;
$module = $plugin; // Prevent some notices when module placed in wrong directory.
include($fullblock.'/version.php');
if (!isset($plugin->version) and isset($module->version)) {
$plugin = $module;
}
$block = $plugin;
unset($module);
$block = clone($plugin);
unset($block->version);
unset($block->component);
unset($block->dependencies);
unset($block->release);
// if plugin tells us it's full name we may check the location
if (isset($block->component)) {
if ($block->component !== $component) {
if (isset($plugin->component)) {
if ($plugin->component !== $component) {
$current = str_replace($CFG->dirroot, '$CFG->dirroot', $fullblock);
$expected = str_replace($CFG->dirroot, '$CFG->dirroot', core_component::get_component_directory($block->component));
$expected = str_replace($CFG->dirroot, '$CFG->dirroot', core_component::get_component_directory($plugin->component));
throw new plugin_misplaced_exception($component, $expected, $current);
}
}
if (empty($plugin->version)) {
throw new plugin_defective_exception($component, 'Missing block version.');
}
if (!empty($plugin->requires)) {
if ($plugin->requires > $CFG->version) {
throw new upgrade_requires_exception($component, $plugin->version, $CFG->version, $plugin->requires);
......@@ -778,11 +794,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
$block->name = $blockname; // The name MUST match the directory
if (empty($block->version)) {
throw new plugin_defective_exception($component, 'Missing block version.');
}
$currblock = $DB->get_record('block', array('name'=>$block->name));
$installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
if (file_exists($fullblock.'/db/install.php')) {
if (get_config('block_'.$blockname, 'installrunning')) {
......@@ -804,7 +816,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
}
}
if (empty($currblock->version)) { // block not installed yet, so install it
if (empty($installedversion)) { // block not installed yet, so install it
$conflictblock = array_search($blocktitle, $blocktitles);
if ($conflictblock !== false) {
// Duplicate block titles are not allowed, they confuse people
......@@ -817,6 +829,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
$DB->get_manager()->install_from_xmldb_file($fullblock.'/db/install.xml');
}
$block->id = $DB->insert_record('block', $block);
upgrade_block_savepoint(true, $plugin->version, $block->name, false);
if (file_exists($fullblock.'/db/install.php')) {
require_once($fullblock.'/db/install.php');
......@@ -839,21 +852,22 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
$endcallback($component, true, $verbose);
} else if ($currblock->version < $block->version) {
} else if ($installedversion < $plugin->version) {
$startcallback($component, false, $verbose);
if (is_readable($fullblock.'/db/upgrade.php')) {
require_once($fullblock.'/db/upgrade.php'); // defines new upgrading function
$newupgrade_function = 'xmldb_block_'.$blockname.'_upgrade';
$result = $newupgrade_function($currblock->version, $block);
$result = $newupgrade_function($installedversion, $block);
} else {
$result = true;