Commit 35f2b674 authored by David Mudrák's avatar David Mudrák
Browse files

MDL-49329 admin: Fix the API for getting remote plugin info

The previous version of the plugin manager's method
get_remote_plugin_info() was suitable for installing missing
dependencies only. To make use of for installing new plugins and/or
available updates, it must be clear that we are requesting information
for the particular plugin version only, not "given or higher" version.
parent d6e38c2a
......@@ -1094,9 +1094,9 @@ class core_admin_renderer extends plugin_renderer_base {
foreach ($dependencies as $component => $remoteinfo) {
if ($remoteinfo === false) {
// The required version is not available. Let us check is there
// The required version is not available. Let us check if there
// is at least some version in the plugins directory.
$remoteinfoanyversion = $pluginman->get_remote_plugin_info($component, ANY_VERSION);
$remoteinfoanyversion = $pluginman->get_remote_plugin_info($component, ANY_VERSION, false);
if ($remoteinfoanyversion === false) {
$unknown[$component] = $component;
} else {
......
......@@ -71,8 +71,10 @@ class core_plugin_manager {
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 cache information about availability in the plugins directory if requesting "at least" version */
protected $remotepluginsinfoatleast = null;
/** @var array cache information about availability in the plugins directory if requesting exact version */
protected $remotepluginsinfoexact = null;
/** @var array list of installed plugins $name=>$version */
protected $installedplugins = null;
/** @var array list of all enabled plugins $name=>$name */
......@@ -83,6 +85,8 @@ class core_plugin_manager {
protected $plugintypes = null;
/** @var \core\update\code_manager code manager to use for plugins code operations */
protected $codemanager = null;
/** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */
protected $updateapiclient = null;
/**
* Direct initiation not allowed, use the factory method {@link self::instance()}
......@@ -119,10 +123,14 @@ class core_plugin_manager {
if (static::$singletoninstance) {
static::$singletoninstance->pluginsinfo = null;
static::$singletoninstance->subpluginsinfo = null;
static::$singletoninstance->remotepluginsinfoatleast = null;
static::$singletoninstance->remotepluginsinfoexact = null;
static::$singletoninstance->installedplugins = null;
static::$singletoninstance->enabledplugins = null;
static::$singletoninstance->presentplugins = null;
static::$singletoninstance->plugintypes = null;
static::$singletoninstance->codemanager = null;
static::$singletoninstance->updateapiclient = null;
}
}
$cache = cache::make('core', 'plugin_manager');
......@@ -881,7 +889,7 @@ class core_plugin_manager {
}
if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
if ($this->is_remote_plugin_available($otherpluginname, $requiredversion)) {
if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
$reqs->availability = self::REQUIREMENT_AVAILABLE;
} else {
$reqs->availability = self::REQUIREMENT_UNAVAILABLE;
......@@ -894,13 +902,17 @@ class core_plugin_manager {
/**
* Is the given plugin version available in the plugins directory?
*
* @param string $component
* @param string|int $requiredversion ANY_VERSION or the version number
* See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
* parameter is interpretted.
*
* @param string $component plugin frankenstyle name
* @param string|int $version ANY_VERSION or the version number
* @param bool $exactmatch false if "given version or higher" is requested
* @return boolean
*/
public function is_remote_plugin_available($component, $requiredversion) {
public function is_remote_plugin_available($component, $version, $exactmatch) {
$info = $this->get_remote_plugin_info($component, $requiredversion);
$info = $this->get_remote_plugin_info($component, $version, $exactmatch);
if (empty($info)) {
// There is no available plugin of that name.
......@@ -916,13 +928,13 @@ class core_plugin_manager {
}
/**
* Can the given plugin remote plugin be installed via the admin UI?
* Can the given plugin version be installed via the admin UI?
*
* @param string $component
* @param string|int $requiredversion ANY_VERSION or the version number
* @param int $version version number
* @return boolean
*/
public function is_remote_plugin_installable($component, $requiredversion) {
public function is_remote_plugin_installable($component, $version) {
global $CFG;
// Make sure the feature is not disabled.
......@@ -931,7 +943,7 @@ class core_plugin_manager {
}
// Make sure we know there is some version available.
if (!$this->is_remote_plugin_available($component, $requiredversion)) {
if (!$this->is_remote_plugin_available($component, $version, true)) {
return false;
}
......@@ -941,7 +953,7 @@ class core_plugin_manager {
return false;
}
$remoteinfo = $this->get_remote_plugin_info($component, $requiredversion);
$remoteinfo = $this->get_remote_plugin_info($component, $version, true);
$localinfo = $this->get_plugin_info($component);
if ($localinfo) {
......@@ -963,20 +975,50 @@ class core_plugin_manager {
/**
* Returns information about a plugin in the plugins directory.
*
* See {@link \core\update\api::find_plugin()} for more details.
* This is typically used when checking for available dependencies (in
* which case the $version represents minimal version we need), or
* when installing an available update or a new plugin from the plugins
* directory (in which case the $version is exact version we are
* interested in). The interpretation of the $version is controlled
* by the $exactmatch argument.
*
* @param string $component
* @param string|int $requiredversion ANY_VERSION or the version number
* If a plugin with the given component name is found, data about the
* plugin are returned as an object. The ->version property of the object
* contains the information about the particular plugin version that
* matches best the given critera. The ->version property is false if no
* suitable version of the plugin was found (yet the plugin itself is
* known).
*
* See {@link \core\update\api::validate_pluginfo_format()} for the
* returned data structure.
*
* @param string $component plugin frankenstyle name
* @param string|int $version ANY_VERSION or the version number
* @param bool $exactmatch false if "given version or higher" is requested
* @return stdClass|bool false or data object
*/
public function get_remote_plugin_info($component, $requiredversion) {
public function get_remote_plugin_info($component, $version, $exactmatch) {
if (!isset($this->remotepluginsinfo[$component][$requiredversion])) {
$client = \core\update\api::client();
$this->remotepluginsinfo[$component][$requiredversion] = $client->find_plugin($component, $requiredversion);
if ($exactmatch and $version == ANY_VERSION) {
throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
}
return $this->remotepluginsinfo[$component][$requiredversion];
$client = $this->get_update_api_client();
if ($exactmatch) {
// Use client's get_plugin_info() method.
if (!isset($this->remotepluginsinfoexact[$component][$version])) {
$this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
}
return $this->remotepluginsinfoexact[$component][$version];
} else {
// Use client's find_plugin() method.
if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
$this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
}
return $this->remotepluginsinfoatleast[$component][$version];
}
}
/**
......@@ -1032,14 +1074,15 @@ class core_plugin_manager {
}
if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
$remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver);
$remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
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 resolving requirements has led to two different versions of the same
// remote plugin, pick the higher version. This can happen in cases like one
// plugin requiring ANY_VERSION and another plugin requiring specific higher
// version with lower maturity of a remote plugin.
if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
$dependencies[$reqname] = $remoteinfo;
}
......@@ -1760,4 +1803,18 @@ class core_plugin_manager {
return $this->codemanager;
}
/**
* Returns a client for https://download.moodle.org/api/
*
* @return \core\update\api
*/
protected function get_update_api_client() {
if ($this->updateapiclient === null) {
$this->updateapiclient = \core\update\api::client();
}
return $this->updateapiclient;
}
}
......@@ -25,6 +25,8 @@
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/testable_update_api.php');
/**
* Testable variant of the core_plugin_manager
*
......@@ -36,6 +38,25 @@ class testable_core_plugin_manager extends core_plugin_manager {
/** @var testable_core_plugin_manager holds the singleton instance */
protected static $singletoninstance;
/**
* Allows us to inject items directly into the plugins info tree.
*
* Do not forget to call our reset_caches() after using this method to force a new
* singleton instance.
*/
public function inject_testable_plugininfo($type, $name, \core\plugininfo\base $plugininfo) {
$this->pluginsinfo[$type][$name] = $plugininfo;
}
/**
* Returns testable subclass of the client.
*
* @return \core\update\testable_api
*/
protected function get_update_api_client() {
return \core\update\testable_api::client();
}
/**
* Mockup implementation of loading available updates info.
*
......@@ -71,50 +92,4 @@ 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;
}
}
......@@ -92,8 +92,8 @@ class testable_api extends api {
$version2015093000info = (object)array(
'id' => '6765',
'version' => '2015093000',
'release' => '',
'maturity' => '50',
'release' => '1.0',
'maturity' => '200',
'downloadurl' => 'http://mood.le/plugins/foo_bar/2015093000.zip',
'downloadmd5' => 'd41d8cd98f00b204e9800998ecf8427e',
'vcssystem' => '',
......@@ -106,6 +106,50 @@ class testable_api extends api {
'version' => '2015041700',
'release' => '2.9'
),
(object)array(
'version' => '2015110900',
'release' => '3.0'
),
)
);
$version2015100400info = (object)array(
'id' => '6796',
'version' => '2015100400',
'release' => '1.1',
'maturity' => '200',
'downloadurl' => 'http://mood.le/plugins/foo_bar/2015100400.zip',
'downloadmd5' => 'd41d8cd98f00b204e9800998ecf8427e',
'vcssystem' => '',
'vcssystemother' => '',
'vcsrepositoryurl' => '',
'vcsbranch' => '',
'vcstag' => '',
'supportedmoodles' => array(
(object)array(
'version' => '2015110900',
'release' => '3.0'
),
)
);
$version2015100500info = (object)array(
'id' => '6799',
'version' => '2015100500',
'release' => '2.0beta',
'maturity' => '100',
'downloadurl' => 'http://mood.le/plugins/foo_bar/2015100500.zip',
'downloadmd5' => 'd41d8cd98f00b204e9800998ecf8427e',
'vcssystem' => '',
'vcssystemother' => '',
'vcsrepositoryurl' => '',
'vcsbranch' => '',
'vcstag' => '',
'supportedmoodles' => array(
(object)array(
'version' => '2015110900',
'release' => '3.0'
),
)
);
......@@ -121,6 +165,14 @@ class testable_api extends api {
$response->data->pluginfo->version = $version2015093000info;
}
if (substr($params['plugin'], -11) === '@2015100400') {
$response->data->pluginfo->version = $version2015100400info;
}
if (substr($params['plugin'], -11) === '@2015100500') {
$response->data->pluginfo->version = $version2015100500info;
}
} else if ($params['plugin'] === 'foo_bar' and isset($params['branch']) and isset($params['minversion'])) {
$response->data = $foobarinfo;
$response->info = array(
......@@ -128,8 +180,16 @@ class testable_api extends api {
);
$response->status = '200 OK';
if ($params['minversion'] <= 2015093000) {
$response->data->pluginfo->version = $version2015093000info;
if ($params['minversion'] <= 2015100400) {
// If two stable versions fullfilling the required version are
// available, the /1.3/pluginfo.php API returns the more recent one.
$response->data->pluginfo->version = $version2015100400info;
} else if ($params['minversion'] <= 2015100500) {
// The /1.3/pluginfo.php API returns versions with lower
// maturity if it is the only way how to fullfil the
// required minimal version.
$response->data->pluginfo->version = $version2015100500info;
}
} else {
......
......@@ -34,6 +34,12 @@ require_once($CFG->dirroot.'/lib/tests/fixtures/testable_plugininfo_base.php');
*/
class core_plugin_manager_testcase extends advanced_testcase {
public function tearDown() {
// The caches of the testable singleton must be reset explicitly. It is
// safer to kill the whole testable singleton at the end of every test.
testable_core_plugin_manager::reset_caches();
}
public function test_instance() {
$pluginman1 = core_plugin_manager::instance();
$this->assertInstanceOf('core_plugin_manager', $pluginman1);
......@@ -53,6 +59,25 @@ class core_plugin_manager_testcase extends advanced_testcase {
testable_core_plugin_manager::reset_caches();
}
/**
* Make sure that the tearDown() really kills the singleton after this test.
*/
public function test_teardown_works_precheck() {
$pluginman = testable_core_plugin_manager::instance();
$pluginfo = testable_plugininfo_base::fake_plugin_instance('fake', '/dev/null', 'one', '/dev/null/fake',
'testable_plugininfo_base', $pluginman);
$pluginman->inject_testable_plugininfo('fake', 'one', $pluginfo);
$this->assertInstanceOf('\core\plugininfo\base', $pluginman->get_plugin_info('fake_one'));
$this->assertNull($pluginman->get_plugin_info('fake_two'));
}
public function test_teardown_works_postcheck() {
$pluginman = testable_core_plugin_manager::instance();
$this->assertNull($pluginman->get_plugin_info('fake_one'));
$this->assertNull($pluginman->get_plugin_info('fake_two'));
}
public function test_get_plugin_types() {
// Make sure there are no warnings or errors.
$types = core_plugin_manager::instance()->get_plugin_types();
......@@ -290,17 +315,36 @@ class core_plugin_manager_testcase extends advanced_testcase {
$this->assertTrue($pluginman->some_plugins_updatable());
}
public function test_get_remote_plugin_info() {
$pluginman = testable_core_plugin_manager::instance();
$this->assertFalse($pluginman->get_remote_plugin_info('not_exists', ANY_VERSION, false));
$info = $pluginman->get_remote_plugin_info('foo_bar', 2015093000, true);
$this->assertEquals(2015093000, $info->version->version);
$info = $pluginman->get_remote_plugin_info('foo_bar', 2015093000, false);
$this->assertEquals(2015100400, $info->version->version);
}
/**
* @expectedException moodle_exception
*/
public function test_get_remote_plugin_info_exception() {
$pluginman = testable_core_plugin_manager::instance();
// The combination of ANY_VERSION + $exactmatch is illegal.
$pluginman->get_remote_plugin_info('any_thing', ANY_VERSION, true);
}
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));
$this->assertFalse($pluginman->is_remote_plugin_available('not_exists', ANY_VERSION, false));
$this->assertTrue($pluginman->is_remote_plugin_available('foo_bar', 2013131313, false));
$this->assertFalse($pluginman->is_remote_plugin_available('foo_bar', 2013131313, true));
}
public function test_resolve_requirements() {
$pluginman = testable_core_plugin_manager::instance();
// Prepare a fake pluginfo instance.
......@@ -340,30 +384,52 @@ class core_plugin_manager_testcase extends advanced_testcase {
$this->assertEquals($pluginman::REQUIREMENT_STATUS_OK, $reqs['core']->status);
// Test plugin dependencies and their availability.
// See {@link testable_core_plugin_manager::get_remote_plugin_info()}.
$pluginfo->dependencies = array(
'foo_bar' => ANY_VERSION,
'foo_baz' => 2014010100,
'foo_crazy' => ANY_VERSION,
);
// See {@link \core\update\testable_api} class.
$pluginfo->dependencies = array('foo_bar' => ANY_VERSION, 'not_exists' => ANY_VERSION);
$reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30);
$this->assertNull($reqs['foo_bar']->hasver);
$this->assertEquals(ANY_VERSION, $reqs['foo_bar']->reqver);
$this->assertEquals($pluginman::REQUIREMENT_STATUS_MISSING, $reqs['foo_bar']->status);
$this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability);
$this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_baz']->availability);
$this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['foo_crazy']->availability);
$this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['not_exists']->availability);
$pluginfo->dependencies = array('foo_bar' => 2013122400);
$reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30);
$this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability);
$pluginfo->dependencies = array(
'foo_baz' => 2015010100,
);
$pluginfo->dependencies = array('foo_bar' => 2015093000);
$reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30);
$this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_baz']->availability);
$this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability);
$pluginfo->dependencies = array('foo_bar' => 2015100500);
$reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30);
$this->assertEquals($pluginman::REQUIREMENT_AVAILABLE, $reqs['foo_bar']->availability);
$pluginfo->dependencies = array(
'foo_baz' => 2015010101,
);
$pluginfo->dependencies = array('foo_bar' => 2025010100);
$reqs = $pluginman->resolve_requirements($pluginfo, 2015110900, 30);
$this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['foo_baz']->availability);
$this->assertEquals($pluginman::REQUIREMENT_UNAVAILABLE, $reqs['foo_bar']->availability);
}
public function test_missing_dependencies() {
$pluginman = testable_core_plugin_manager::instance();
$one = testable_plugininfo_base::fake_plugin_instance('fake', '/dev/null', 'one', '/dev/null/fake',
'testable_plugininfo_base', $pluginman);
$two = testable_plugininfo_base::fake_plugin_instance('fake', '/dev/null', 'two', '/dev/null/fake',
'testable_plugininfo_base', $pluginman);
$pluginman->inject_testable_plugininfo('fake', 'one', $one);
$pluginman->inject_testable_plugininfo('fake', 'two', $two);
$this->assertEmpty($pluginman->missing_dependencies());
$one->dependencies = array('foo_bar' => ANY_VERSION);
$misdeps = $pluginman->missing_dependencies();
$this->assertEquals(2015100400, $misdeps['foo_bar']->version->version);
$two->dependencies = array('foo_bar' => 2015100500);
$misdeps = $pluginman->missing_dependencies();
$this->assertEquals(2015100500, $misdeps['foo_bar']->version->version);
}
}
......@@ -31,6 +31,11 @@ require_once(__DIR__.'/fixtures/testable_update_api.php');
/**
* Tests for \core\update\api client.
*
* Please note many of these tests heavily depend on the behaviour of the
* testable_api client. It is important to make sure that the behaviour of the
* testable_api client perfectly matches the actual behaviour of the live
* services on the given API version.
*
* @copyright 2015 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
......@@ -66,8 +71,12 @@ class core_update_api_testcase extends advanced_testcase {
$this->assertFalse($info->version);
// Both plugin and the version are available.
$info = $client->get_plugin_info('foo_bar', 2015093000);
$this->assertNotNull($info->version->downloadurl);
foreach (array(2015093000 => MATURITY_STABLE, 2015100400 => MATURITY_STABLE,
2015100500 => MATURITY_BETA) as $version => $maturity) {
$info = $client->get_plugin_info('foo_bar', $version);
$this->assertNotEmpty($info->version);
$this->assertEquals($maturity, $info->version->maturity);
}
}
/**
......@@ -81,15 +90,22 @@ class core_update_api_testcase extends advanced_testcase {
$this->assertFalse($client->find_plugin('non_existing'));
// The plugin is known but there is no sufficient version.
$info = $client->find_plugin('foo_bar', 2015093001);
$info = $client->find_plugin('foo_bar', 2016010100);
$this->assertFalse($info->version);
// Both plugin and the version are available.
// Both plugin and the version are available. Of the two available
// stable versions, the more recent one is returned.
$info = $client->find_plugin('foo_bar', 2015093000);
$this->assertNotNull($info->version->downloadurl);
$this->assertEquals(2015100400, $info->version->version);
// If any version is required, the most recent most mature one is
// returned.
$info = $client->find_plugin('foo_bar', ANY_VERSION);
$this->assertNotNull($info->version->downloadurl);
$this->assertEquals(2015100400, $info->version->version);
// Less matured versions are returned if needed.
$info = $client->find_plugin('foo_bar', 2015100500);
$this->assertEquals(2015100500, $info->version->version);
}
/**
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment