Commit 5a92cd0b authored by David Mudrák's avatar David Mudrák
Browse files

MDL-49329 admin: Display missing dependencies on plugins check screen

The patch improves the dependencies resolution in the plugin manager so
that the information about availability of the missing dependency is
included and can be displayed at the Plugins check screen during the
upgrade.
parent 48900324
......@@ -207,7 +207,6 @@ class core_admin_renderer extends plugin_renderer_base {
$output .= $this->header();
$output .= $this->box_start('generalbox');
$output .= $this->container_start('generalbox', 'notice');
$output .= html_writer::tag('p', get_string('pluginchecknotice', 'core_plugin'));
if ($checker->enabled()) {
$output .= $this->container_start('checkforupdates');
......@@ -218,8 +217,8 @@ class core_admin_renderer extends plugin_renderer_base {
}
$output .= $this->container_end();
}
$output .= $this->container_end();
$output .= $this->missing_dependencies($pluginman);
$output .= $this->plugins_check_table($pluginman, $version, array('full' => $showallplugins));
$output .= $this->box_end();
$output .= $this->upgrade_reload($reloadurl);
......@@ -1037,6 +1036,168 @@ class core_admin_renderer extends plugin_renderer_base {
return $out;
}
/**
* Displays the information about missing dependencies
*
* @param core_plugin_manager $pluginman
* @return string
*/
protected function missing_dependencies(core_plugin_manager $pluginman) {
$dependencies = $pluginman->missing_dependencies();
if (empty($dependencies)) {
return '';
}
$available = array();
$unavailable = array();
$unknown = array();
foreach ($dependencies as $component => $remoteinfo) {
if ($remoteinfo === false) {
// The required version is not available. Let us check is there
// is at least some version in the plugins directory.
$remoteinfoanyversion = $pluginman->get_remote_plugin_info($component, ANY_VERSION);
if ($remoteinfoanyversion === false) {
$unknown[$component] = $component;
} else {
$unavailable[$component] = $remoteinfoanyversion;
}
} else {
$available[$component] = $remoteinfo;
}
}
$out = $this->output->container_start('plugins-check-dependencies');
if ($unavailable or $unknown) {
$out .= $this->output->heading(get_string('misdepsunavail', 'core_plugin'));
if ($unknown) {
$out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode($unknown, ', ')));
}
if ($unavailable) {
$unavailablelist = array();
foreach ($unavailable as $component => $remoteinfoanyversion) {
$unavailablelistitem = html_writer::link('https://moodle.org/plugins/view.php?plugin='.$component,
'<strong>'.$remoteinfoanyversion->name.'</strong>');
if ($remoteinfoanyversion->version) {
$unavailablelistitem .= ' ('.$component.' &gt; '.$remoteinfoanyversion->version->version.')';
} else {
$unavailablelistitem .= ' ('.$component.')';
}
$unavailablelist[] = $unavailablelistitem;
}
$out .= $this->output->notification(get_string('misdepsunavaillist', 'core_plugin',
implode($unavailablelist, ', ')));
}
$out .= $this->output->container_start('plugins-check-dependencies-actions');
$out .= ' '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
get_string('dependencyuploadmissing', 'core_plugin'));
$out .= $this->output->container_end(); // .plugins-check-dependencies-actions
}
if ($available) {
$out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
$out .= $this->available_missing_dependencies_list($pluginman, $available);
$out .= $this->output->container_start('plugins-check-dependencies-actions');
// TODO implement the button functionality.
$out .= html_writer::link(
new moodle_url('/admin/tool/installaddon/'),
get_string('dependencyinstallmissing', 'core_plugin'),
array('class' => 'btn')
);
$out.= ' | '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
get_string('dependencyuploadmissing', 'core_plugin'));
$out .= $this->output->container_end(); // .plugins-check-dependencies-actions
}
$out .= $this->output->container_end(); // .plugins-check-dependencies
return $out;
}
/**
* Displays the list if available missing dependencies.
*
* @param core_plugin_manager $pluginman
* @param array $dependencies
* @return string
*/
protected function available_missing_dependencies_list(core_plugin_manager $pluginman, array $dependencies) {
global $CFG;
$table = new html_table();
$table->id = 'plugins-check-available-dependencies';
$table->head = array(
get_string('displayname', 'core_plugin'),
get_string('release', 'core_plugin'),
get_string('version', 'core_plugin'),
get_string('supportedmoodleversions', 'core_plugin'),
get_string('info', 'core'),
);
$table->colclasses = array('displayname', 'release', 'version', 'supportedmoodleversions', 'info');
$table->data = array();
foreach ($dependencies as $plugin) {
$supportedmoodles = array();
foreach ($plugin->version->supportedmoodles as $moodle) {
if ($CFG->branch == str_replace('.', '', $moodle->release)) {
$supportedmoodles[] = html_writer::span($moodle->release, 'label label-success');
} else {
$supportedmoodles[] = html_writer::span($moodle->release, 'label');
}
}
$requriedby = $pluginman->other_plugins_that_require($plugin->component);
if ($requriedby) {
foreach ($requriedby as $ix => $val) {
$inf = $pluginman->get_plugin_info($val);
if ($inf) {
$requriedby[$ix] = $inf->displayname.' ('.$inf->component.')';
}
}
$info = html_writer::div(
get_string('requiredby', 'core_plugin', implode(', ', $requriedby)),
'requiredby'
);
} else {
$info = '';
}
$info .= html_writer::span(
html_writer::link('https://moodle.org/plugins/view.php?plugin='.$plugin->component,
get_string('misdepinfoplugin', 'core_plugin')),
'misdepinfoplugin'
);
$info .= ' | '.html_writer::span(
html_writer::link('https://moodle.org/plugins/pluginversion.php?id='.$plugin->version->id,
get_string('misdepinfoversion', 'core_plugin')),
'misdepinfoversion'
);
$info .= ' | '.html_writer::link($plugin->version->downloadurl, get_string('download'),
array('class' => 'btn btn-small'));
// TODO Implement the button functionality.
$info .= ' | '.html_writer::link($plugin->version->downloadurl, get_string('dependencyinstall', 'core_plugin'),
array('class' => 'btn btn-small'));
$table->data[] = array(
html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component'),
$plugin->version->release,
$plugin->version->version,
implode($supportedmoodles, ' '),
$info
);
}
return html_writer::table($table);
}
/**
* Formats the information that needs to go in the 'Requires' column.
* @param \core\plugininfo\base $plugin the plugin we are rendering the row for.
......@@ -1047,6 +1208,8 @@ class core_admin_renderer extends plugin_renderer_base {
protected function required_column(\core\plugininfo\base $plugin, core_plugin_manager $pluginman, $version) {
$requires = array();
$displayuploadlink = false;
$displayupdateslink = false;
foreach ($pluginman->resolve_requirements($plugin, $version) as $reqname => $reqinfo) {
if ($reqname === 'core') {
......@@ -1069,19 +1232,37 @@ class core_admin_renderer extends plugin_renderer_base {
$class = 'requires-ok';
} else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_MISSING) {
$label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'label label-important');
$class = 'requires-failed requires-missing';
// TODO Display the install link only if really available there.
$installurl = new moodle_url('https://moodle.org/plugins/view.php', array('plugin' => $reqname));
$uploadurl = new moodle_url('/admin/tool/installaddon/');
$actions[] = html_writer::link($installurl, get_string('dependencyinstall', 'core_plugin'));
$actions[] = html_writer::link($uploadurl, get_string('dependencyupload', 'core_plugin'));
if ($reqinfo->availability == $pluginman::REQUIREMENT_AVAILABLE) {
$label = html_writer::span(get_string('dependencymissing', 'core_plugin'), 'label label-warning');
$label .= ' '.html_writer::span(get_string('dependencyavailable', 'core_plugin'), 'label label-warning');
$class = 'requires-failed requires-missing requires-available';
$actions[] = html_writer::link(
new moodle_url('https://moodle.org/plugins/view.php', array('plugin' => $reqname)),
get_string('misdepinfoplugin', 'core_plugin')
);
} else {
$label = html_writer::span(get_string('dependencymissing', 'core_plugin'), 'label label-important');
$label .= ' '.html_writer::span(get_string('dependencyunavailable', 'core_plugin'),
'label label-important');
$class = 'requires-failed requires-missing requires-unavailable';
}
$displayuploadlink = true;
} else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OUTDATED) {
$label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'label label-important');
$class = 'requires-failed requires-outdated';
$updateurl = new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1));
$actions[] = html_writer::link($updateurl, get_string('checkforupdates', 'core_plugin'));
if ($reqinfo->availability == $pluginman::REQUIREMENT_AVAILABLE) {
$label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'label label-warning');
$label .= ' '.html_writer::span(get_string('dependencyavailable', 'core_plugin'), 'label label-warning');
$class = 'requires-failed requires-outdated requires-available';
$displayupdateslink = true;
} else {
$label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'label label-important');
$label .= ' '.html_writer::span(get_string('dependencyunavailable', 'core_plugin'),
'label label-important');
$class = 'requires-failed requires-outdated requires-unavailable';
}
$displayuploadlink = true;
}
if ($reqinfo->reqver != ANY_VERSION) {
......@@ -1101,7 +1282,31 @@ class core_admin_renderer extends plugin_renderer_base {
if (!$requires) {
return '';
}
return html_writer::tag('ul', implode("\n", $requires));
$out = html_writer::tag('ul', implode("\n", $requires));
if ($displayuploadlink) {
$out .= html_writer::div(
html_writer::link(
new moodle_url('/admin/tool/installaddon/'),
get_string('dependencyuploadmissing', 'core_plugin')
),
'dependencyuploadmissing'
);
}
if ($displayupdateslink) {
$out .= html_writer::div(
html_writer::link(
new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1)),
get_string('checkforupdates', 'core_plugin')
),
'checkforupdates'
);
}
return $out;
}
/**
......
......@@ -30,9 +30,14 @@ $string['availability'] = 'Availability';
$string['checkforupdates'] = 'Check for available updates';
$string['checkforupdateslast'] = 'Last check done on {$a}';
$string['detectedmisplacedplugin'] = 'Plugin "{$a->component}" is installed in incorrect location "{$a->current}", expected location is "{$a->expected}"';
$string['dependencyavailable'] = 'Available';
$string['dependencyfails'] = 'Fails';
$string['dependencyinstall'] = 'Install';
$string['dependencyinstallmissing'] = 'Install all missing dependencies';
$string['dependencymissing'] = 'Missing';
$string['dependencyunavailable'] = 'Unavailable';
$string['dependencyupload'] = 'Upload';
$string['dependencyuploadmissing'] = 'Upload ZIP files';
$string['displayname'] = 'Plugin name';
$string['err_response_curl'] = 'Unable to fetch available updates data - unexpected cURL error.';
$string['err_response_format_version'] = 'Unexpected version of the response format. Please try to re-check for available updates.';
......@@ -42,6 +47,12 @@ $string['filtercontribonly'] = 'Show additional plugins only';
$string['filtercontribonlyactive'] = 'Showing additional plugins only';
$string['filterupdatesonly'] = 'Show updateable only';
$string['filterupdatesonlyactive'] = 'Showing updateable only';
$string['misdepinfoplugin'] = 'Plugin info';
$string['misdepinfoversion'] = 'Version info';
$string['misdepsavail'] = 'Available missing dependencies';
$string['misdepsunavail'] = 'Unavailable missing dependencies';
$string['misdepsunavaillist'] = 'No version found to fulfill the dependency requirements: {$a}.';
$string['misdepsunknownlist'] = 'Not in the Plugins directory: <strong>{$a}</strong>.';
$string['moodleversion'] = 'Moodle {$a}';
$string['nonehighlighted'] = 'No plugins require your attention now';
$string['nonehighlightedinfo'] = 'Display the list of all installed plugins anyway';
......@@ -87,6 +98,7 @@ $string['status_new'] = 'To be installed';
$string['status_nodb'] = 'No database';
$string['status_upgrade'] = 'To be upgraded';
$string['status_uptodate'] = 'Installed';
$string['supportedmoodleversions'] = 'Supported Moodle versions';
$string['systemname'] = 'Identifier';
$string['type_auth'] = 'Authentication method';
$string['type_auth_plural'] = 'Authentication methods';
......
......@@ -60,12 +60,19 @@ class core_plugin_manager {
/** the required dependency is not installed */
const REQUIREMENT_STATUS_MISSING = 'missing';
/** the required dependency is available in the plugins directory */
const REQUIREMENT_AVAILABLE = 'available';
/** the required dependency is available in the plugins directory */
const REQUIREMENT_UNAVAILABLE = 'unavailable';
/** @var core_plugin_manager holds the singleton instance */
protected static $singletoninstance;
/** @var array of raw plugins information */
protected $pluginsinfo = null;
/** @var array of raw subplugins information */
protected $subpluginsinfo = null;
/** @var array cache information about availability in the plugins directory */
protected $remotepluginsinfo = null;
/** @var array list of installed plugins $name=>$version */
protected $installedplugins = null;
/** @var array list of all enabled plugins $name=>$name */
......@@ -767,6 +774,7 @@ class core_plugin_manager {
* ->(numeric)hasver
* ->(numeric)reqver
* ->(string)status
* ->(string)availability
*
* @param \core\plugininfo\base $plugin the plugin we are checking
* @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
......@@ -807,7 +815,12 @@ class core_plugin_manager {
*/
protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
$reqs = new stdClass();
$reqs = (object)array(
'hasver' => null,
'reqver' => null,
'status' => null,
'availability' => null,
);
$reqs->hasver = $moodleversion;
......@@ -838,7 +851,13 @@ class core_plugin_manager {
protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
$requiredversion, $moodlebranch) {
$reqs = new stdClass();
$reqs = (object)array(
'hasver' => null,
'reqver' => null,
'status' => null,
'availability' => null,
);
$otherplugin = $this->get_plugin_info($otherpluginname);
if ($otherplugin !== null) {
......@@ -857,12 +876,109 @@ class core_plugin_manager {
$reqs->hasver = null;
$reqs->reqver = $requiredversion;
$reqs->status = self::REQUIREMENT_STATUS_MISSING;
// TODO: Check if the $otherpluginname is available in the plugins directory.
}
if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
if ($this->is_remote_plugin_available($otherpluginname, $requiredversion)) {
$reqs->availability = self::REQUIREMENT_AVAILABLE;
} else {
$reqs->availability = self::REQUIREMENT_UNAVAILABLE;
}
}
return $reqs;
}
/**
* Is the given plugin version available in the plugins directory?
*
* @param string $component
* @param string|int $requiredversion ANY_VERSION or the version number
* @return boolean
*/
public function is_remote_plugin_available($component, $requiredversion) {
$info = $this->get_remote_plugin_info($component, $requiredversion);
if (empty($info)) {
// There is no available plugin of that name.
return false;
}
if (empty($info->version)) {
// Plugin is known, but no suitable version was found.
return false;
}
return true;
}
/**
* Returns information about a plugin in the plugins directory.
*
* See {@link \core\update\api::find_plugin()} for more details.
*
* @param string $component
* @param string|int $requiredversion ANY_VERSION or the version number
* @return stdClass|bool false or data object
*/
public function get_remote_plugin_info($component, $requiredversion) {
if (!isset($this->remotepluginsinfo[$component][$requiredversion])) {
$client = \core\update\api::client();
$this->remotepluginsinfo[$component][$requiredversion] = $client->find_plugin($component, $requiredversion);
}
return $this->remotepluginsinfo[$component][$requiredversion];
}
/**
* Return a list of all missing dependencies.
*
* This should provide the full list of plugins that should be installed to
* fulfill the requirements of all plugins, if possible.
*
* @return array of stdClass|bool indexed by the component name
*/
public function missing_dependencies() {
$dependencies = array();
foreach ($this->get_plugins() as $plugintype => $pluginfos) {
foreach ($pluginfos as $pluginname => $pluginfo) {
foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
if ($reqname === 'core') {
continue;
}
if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
$remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver);
if (empty($dependencies[$reqname])) {
$dependencies[$reqname] = $remoteinfo;
} else {
// If two local plugins depend on the two different
// versions of the same remote plugin, pick the
// higher version.
if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
$dependencies[$reqname] = $remoteinfo;
}
}
} else {
if (!isset($dependencies[$reqname])) {
// Unable to find a plugin fulfilling the requirements.
$dependencies[$reqname] = false;
}
}
}
}
}
}
return $dependencies;
}
/**
* Is it possible to uninstall the given plugin?
*
......
......@@ -71,4 +71,50 @@ class testable_core_plugin_manager extends core_plugin_manager {
return null;
}
/**
* Mockup fetching info about a plugin available in the plugins directory.
*
* This testable method does not actually use {@link \core\update\api}.
* Instead, it provides haerd-coded list fictional plugins and their
* versions.
*
* @param string $component
* @param string|int $requiredversion ANY_VERSION or the version number
* @return stdClass|bool false or data object
*/
public function get_remote_plugin_info($component, $requiredversion) {
$info = false;
if ($component === 'foo_bar') {
$info = (object)array(
'name' => 'Foo bar',
'component' => 'foo_bar',
'version' => false,
);
if ($requiredversion === ANY_VERSION or $requiredversion <= 2015010100) {
$info->version = (object)array(
'version' => 2015010100
);
}
}
if ($component === 'foo_baz') {
$info = (object)array(
'name' => 'Foo baz',
'component' => 'foo_baz',
'version' => false,
);
if ($requiredversion === ANY_VERSION or $requiredversion <= 2015010100) {
$info->version = (object)array(
'version' => 2015010100
);
}
}
return $info;
}
}
<?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/>.
/**
* Provides testable_plugininfo_base class.
*
* @package core
* @category test
* @copyright 2015 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Testable plugininfo subclass representing a fake plugin type instance.
*
* @copyright 2015 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_plugininfo_base extends \core\plugininfo\base {
public static function fake_plugin_instance($type, $typerootdir, $name, $namerootdir, $typeclass, $pluginman) {
return self::make_plugin_instance($type, $typerootdir, $name, $namerootdir, $typeclass, $pluginman);
}
public function init_display_name() {
$this->displayname = 'Testable fake pluginfo instance';
}
public function load_disk_version() {
$this->versiondisk = null;
$this->versionrequires = null;
$this->dependencies = array();
}
public function load_db_version() {
$this->versiondb = null;
}
public function init_is_standard() {
$this->source = core_plugin_manager::PLUGIN_SOURCE_EXTENSION;
}
}
......@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/lib/tests/fixtures/testable_plugin_manager.php');
require_once($CFG->dirroot.'/lib/tests/fixtures/testable_plugininfo_base.php');
/**
* Tests of the basic API of the plugin manager.
......@@ -288,4 +289,81 @@ class core_plugin_manager_testcase extends advanced_testcase {
$pluginman = testable_core_plugin_manager::instance();
$this->assertTrue($pluginman->some_plugins_updatable());
}
public function test_is_remote_plugin_available() {
// See {@link testable_core_plugin_manager::get_remote_plugin_info()}.
$pluginman = testable_core_plugin_manager::instance();
$this->assertTrue($pluginman->is_remote_plugin_available('foo_bar', ANY_VERSION));
$this->assertTrue($pluginman->is_remote_plugin_available('foo_bar', 2015010100));
$this->assertFalse($pluginman->is_remote_plugin_available('foo_bar', 2016010100));
$this->assertFalse($pluginman->is_remote_plugin_available('bar_bar', ANY_VERSION));
}
public function test_resolve_requirements() {
$pluginman