Skip to content

Commit

Permalink
MDL-49329 admin: Add plugin manager method for installing remote packs
Browse files Browse the repository at this point in the history
The new method core_plugin_manager::install_remote_plugins() will serve
as a backend for all the ways of installing ZIP packages from the moodle
plugins directory, such as installing new plugins (by clicking the
Install button in the plugins directory), installing available updates
(single and bulk mode) and installing missing dependencies (single and
bulk mode).

The method should be used both for validation pre-check screen and,
after the confirmation, for actual installation. Note that we
intentionally repeat the whole procedure after confirmation. Unzipping
plugins is cheap and fast and the ZIPs themselves are already available
in the \core\update\code_manager's cache.

We will need to add support for archiving existing code to prevent
accidental data-loss.

This basically provides what mdeploy.php was doing, but better. We now
have consistent way of installing all remote ZIP packages, always
validate them and we can perform bulk operations, too.
  • Loading branch information
mudrd8mz committed Oct 9, 2015
1 parent 8acee4b commit c948b81
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 9 deletions.
45 changes: 38 additions & 7 deletions admin/renderer.php
Expand Up @@ -999,6 +999,16 @@ public function plugins_check_table(core_plugin_manager $pluginman, $version, ar
);
}

$installableupdates = $pluginman->filter_installable($pluginman->available_updates());
if ($installableupdates) {
$out .= $this->output->single_button(
new moodle_url($this->page->url, array('installupdatex' => 1)),
get_string('updateavailableinstallall', 'core_admin', count($installableupdates)),
'post',
array('class' => 'singlebutton updateavailableinstallall')
);
}

$out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 0)),
get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge'));

Expand All @@ -1015,6 +1025,33 @@ public function plugins_check_table(core_plugin_manager $pluginman, $version, ar
return $out;
}

/**
* Display the continue / cancel widgets for the plugins validation page.
*
* @param null|moodle_url $continue URL for the continue button, should it be displayed
* @param string $label explicit label for the continue button
* @param moodle_url $cancel URL for the cancel link, defaults to the current page
* @return string HTML
*/
public function install_remote_plugins_buttons(moodle_url $continue=null, $label=null, moodle_url $cancel=null) {

$out = html_writer::start_div('install-remote-plugins-buttons');

if (!empty($continue)) {
if (empty($label)) {
$label = get_string('continue');
}
$out .= $this->output->single_button($continue, $label, 'post', array('class' => 'continue'));
}

if (empty($cancel)) {
$cancel = $this->page->url;
}
$out .= html_writer::div(html_writer::link($cancel, get_string('cancel')), 'cancel');

return $out;
}

/**
* Displays the information about missing dependencies
*
Expand Down Expand Up @@ -1078,15 +1115,9 @@ protected function missing_dependencies(core_plugin_manager $pluginman) {

if ($available) {
$out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
$installable = array();
foreach ($available as $component => $remoteinfo) {
if ($pluginman->is_remote_plugin_installable($component, $remoteinfo->version->version)) {
$installable[$component] = $remoteinfo;
}
}

$out .= $this->output->container_start('plugins-check-dependencies-actions');

$installable = $pluginman->filter_installable($available);
if ($installable) {
$out .= $this->output->single_button(
new moodle_url($this->page->url, array('installdepx' => 1)),
Expand Down
4 changes: 3 additions & 1 deletion lang/en/admin.php
Expand Up @@ -1083,7 +1083,9 @@
$string['updateavailable_moreinfo'] = 'More info...';
$string['updateavailable_release'] = 'Moodle {$a}';
$string['updateavailable_version'] = 'Version {$a}';
$string['updateavailableinstall'] = 'Install this update';
$string['updateavailableinstall'] = 'Install';
$string['updateavailableinstallall'] = 'Install available updates ({$a})';
$string['updateavailableinstallallhead'] = 'Installing available updates';
$string['updateavailablenot'] = 'Your Moodle code is up-to-date!';
$string['updateavailablerecommendation'] = 'It is strongly recommended that you update your site to the latest version to obtain all recent security and bug fixes.';
$string['updatenotifications'] = 'Update notifications';
Expand Down
6 changes: 6 additions & 0 deletions lang/en/plugin.php
Expand Up @@ -66,6 +66,12 @@
$string['overviewall'] = 'All plugins';
$string['overviewext'] = 'Additional plugins';
$string['overviewupdatable'] = 'Available updates';
$string['packagesdebug'] = 'Debugging output enabled';
$string['packagesdownloading'] = 'Downloading packages';
$string['packagesextracting'] = 'Extracting packages';
$string['packagesvalidating'] = 'Validating packages';
$string['packagesvalidatingfailed'] = 'Installation aborted due to validation failure';
$string['packagesvalidatingok'] = 'Validation successful, installation can continue';
$string['plugincheckall'] = 'All plugins';
$string['plugincheckattention'] = 'Plugins requiring attention';
$string['pluginchecknone'] = 'No plugins require your attention now';
Expand Down
176 changes: 176 additions & 0 deletions lib/classes/plugin_manager.php
Expand Up @@ -987,6 +987,30 @@ public function is_remote_plugin_installable($component, $version, &$reason=null
return true;
}

/**
* Given the list of remote plugin infos, return just those installable.
*
* This is typically used on lists returned by
* {@link self::available_updates()} or {@link self::missing_dependencies()}
* to perform bulk installation of remote plugins.
*
* @param array $remoteinfos list of {@link \core\update\remote_info}
* @return array
*/
public function filter_installable($remoteinfos) {

if (empty($remoteinfos)) {
return array();
}
$installable = array();
foreach ($remoteinfos as $index => $remoteinfo) {
if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
$installable[$index] = $remoteinfo;
}
}
return $installable;
}

/**
* Returns information about a plugin in the plugins directory.
*
Expand Down Expand Up @@ -1174,6 +1198,158 @@ public function can_uninstall_plugin($component) {
return true;
}

/**
* Perform the installation of plugins available in the plugins directory.
*
* The list of plugins is supposed to be processed by
* {@link self::filter_installable()} to make sure all the plugins are
* valid.
*
* @param array $plugins list of installable remote plugins
* @param bool $confirmed should the files be really deployed into the dirroot?
* @param bool $silent perform without output
* @return bool true on success
*/
public function install_remote_plugins(array $plugins, $confirmed, $silent) {
global $CFG, $OUTPUT;

if (empty($plugins)) {
return false;
}

$ok = get_string('ok', 'core');

// Let admins know they can expect more verbose output.
$silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);

// Download all ZIP packages if we do not have them yet.
$silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin'), ' ... ');
$zips = array();
foreach ($plugins as $plugin) {
$zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl, $plugin->version->downloadmd5);
$silent or $this->mtrace(PHP_EOL.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
$silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
if (!$zips[$plugin->component]) {
$silent or $this->mtrace(get_string('error'));
return false;
}
}
$silent or $this->mtrace($ok);

// Validate all downloaded packages.
$silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin'), ' ... '.PHP_EOL);
foreach ($plugins as $plugin) {
$zipfile = $zips[$plugin->component];
$silent or $this->mtrace('* '.s($plugin->name). ' ('.$plugin->component.')', ' ... ');
list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
$tmp = make_request_directory();
$zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
if (empty($zipcontents)) {
$silent or $this->mtrace(get_string('error'));
$silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
return false;
}

$validator = \core\update\validator::instance($tmp, $zipcontents);
$validator->assert_plugin_type($plugintype);
$validator->assert_moodle_version($CFG->version);
// TODO Check for missing dependencies during validation.
$result = $validator->execute();
if (!$silent) {
$result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
foreach ($validator->get_messages() as $message) {
if ($message->level === $validator::INFO) {
// Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
$level = DEBUG_NORMAL;
} else if ($message->level === $validator::DEBUG) {
// Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
$level = DEBUG_ALL;
} else {
// Display [Warning] and [Error] always.
$level = null;
}
if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
$this->mtrace(' <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
} else {
$this->mtrace(' ['.$validator->message_level_name($message->level).']', ' ', $level);
}
$this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
$info = $validator->message_code_info($message->msgcode, $message->addinfo);
if ($info) {
$this->mtrace('['.s($info).']', ' ', $level);
} else if (is_string($message->addinfo)) {
$this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
} else {
$this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
}
if ($icon = $validator->message_help_icon($message->msgcode)) {
if (CLI_SCRIPT) {
$this->mtrace(PHP_EOL.' ^^^ '.get_string('help').': '.
get_string($icon->identifier.'_help', $icon->component), '', $level);
} else {
$this->mtrace($OUTPUT->render($icon), ' ', $level);
}
}
$this->mtrace(PHP_EOL, '', $level);
}
}
if (!$result) {
$silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
return false;
}
}
$silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));

if (!$confirmed) {
return true;
}

// Extract all ZIP packs do the dirroot.
$silent or $this->mtrace(get_string('packagesextracting', 'core_plugin'), ' ... '.PHP_EOL);
foreach ($plugins as $plugin) {
$silent or $this->mtrace('* '.s($plugin->name). ' ('.$plugin->component.')', ' ... ');
$zipfile = $zips[$plugin->component];
list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
$target = $this->get_plugintype_root($plugintype);
if (file_exists($target.'/'.$pluginname)) {
$current = $this->get_plugin_info($plugin->component);
if ($current->versiondb and $current->versiondb == $current->versiondisk) {
// TODO Archive existing version so that we can revert.
}
remove_dir($target.'/'.$pluginname);
}
if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
$silent or $this->mtrace(get_string('error'));
$silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
return false;
}
$silent or $this->mtrace($ok);
}

return true;
}

/**
* Outputs the given message via {@link mtrace()}.
*
* If $debug is provided, then the message is displayed only at the given
* debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
* site has developer debugging level selected).
*
* @param string $msg message
* @param string $eol end of line
* @param null|int $debug null to display always, int only on given debug level
*/
protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
global $CFG;

if ($debug !== null and !debugging(null, $debug)) {
return;
}

mtrace($msg, $eol);
}

/**
* Returns uninstall URL if exists.
*
Expand Down
13 changes: 13 additions & 0 deletions theme/bootstrapbase/less/moodle/admin.less
Expand Up @@ -753,6 +753,19 @@ img.iconsmall {
}
}

.install-remote-plugins-buttons {
> div {
display: inline-block;
margin: 1em 1em 1em 0;
}
.continue {
padding: 0;
div, input {
margin: 0;
}
}
}

#page-admin-index .upgradepluginsinfo {
text-align: center;
}
Expand Down
2 changes: 1 addition & 1 deletion theme/bootstrapbase/style/moodle.css

Large diffs are not rendered by default.

0 comments on commit c948b81

Please sign in to comment.