Merge branch 'MDL-49329-master-multiplug' of git://github.com/mudrd8mz/moodle
authorDan Poltawski <dan@moodle.com>
Fri, 16 Oct 2015 13:53:26 +0000 (14:53 +0100)
committerDan Poltawski <dan@moodle.com>
Fri, 16 Oct 2015 13:53:26 +0000 (14:53 +0100)
87 files changed:
admin/index.php
admin/plugins.php
admin/renderer.php
admin/settings/server.php
admin/tool/installaddon/classes/installer.php
admin/tool/installaddon/classes/installfromzip_form.php
admin/tool/installaddon/classes/pluginfo_client.php [deleted file]
admin/tool/installaddon/deploy.php [deleted file]
admin/tool/installaddon/index.php
admin/tool/installaddon/lang/en/tool_installaddon.php
admin/tool/installaddon/permcheck.php
admin/tool/installaddon/renderer.php
admin/tool/installaddon/settings.php
admin/tool/installaddon/styles.css
admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php [deleted file]
admin/tool/installaddon/tests/fixtures/nolang/bah/index.php [deleted file]
admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bah.php [deleted file]
admin/tool/installaddon/tests/fixtures/nolang/bah/lib.php [deleted file]
admin/tool/installaddon/tests/fixtures/nowrapdir/index.php [deleted file]
admin/tool/installaddon/tests/fixtures/testable_installer.php [new file with mode: 0644]
admin/tool/installaddon/tests/installer_test.php
admin/tool/installaddon/validate.php [deleted file]
config-dist.php
lang/en/admin.php
lang/en/plugin.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/base.php
lib/classes/plugininfo/format.php
lib/classes/plugininfo/orphaned.php
lib/classes/update/api.php [new file with mode: 0644]
lib/classes/update/checker.php
lib/classes/update/code_manager.php [new file with mode: 0644]
lib/classes/update/deployer.php [deleted file]
lib/classes/update/remote_info.php [new file with mode: 0644]
lib/classes/update/validator.php [moved from admin/tool/installaddon/classes/validator.php with 76% similarity]
lib/db/upgrade.php
lib/phpunit/classes/util.php
lib/setup.php
lib/tests/fixtures/testable_plugin_manager.php [new file with mode: 0644]
lib/tests/fixtures/testable_plugininfo_base.php [new file with mode: 0644]
lib/tests/fixtures/testable_update_api.php [new file with mode: 0644]
lib/tests/fixtures/testable_update_checker.php [moved from lib/tests/update_deployer_test.php with 76% similarity]
lib/tests/fixtures/testable_update_code_manager.php [new file with mode: 0644]
lib/tests/fixtures/testable_update_validator.php [new file with mode: 0644]
lib/tests/fixtures/update_validator/emptydir/emptydir/README.txt [moved from admin/tool/installaddon/tests/fixtures/emptydir/emptydir/README.txt with 100% similarity]
lib/tests/fixtures/update_validator/github/moodle-repository_mahara-master/lang/en/repository_mahara.php [new file with mode: 0644]
lib/tests/fixtures/update_validator/github/moodle-repository_mahara-master/version.php [moved from admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/version.php with 98% similarity]
lib/tests/fixtures/update_validator/installed/greenbar/index.php [moved from admin/tool/installaddon/tests/fixtures/plugindir/foobar/index.php with 98% similarity]
lib/tests/fixtures/update_validator/installed/greenbar/lang/en/local_greenbar.php [moved from admin/tool/installaddon/tests/fixtures/installed/greenbar/lang/en/local_greenbar.php with 97% similarity]
lib/tests/fixtures/update_validator/installed/greenbar/version.php [moved from admin/tool/installaddon/tests/fixtures/installed/greenbar/version.php with 98% similarity]
lib/tests/fixtures/update_validator/multidir/one/version.php [moved from admin/tool/installaddon/tests/fixtures/multidir/one/version.php with 97% similarity]
lib/tests/fixtures/update_validator/multidir/two/README.txt [moved from admin/tool/installaddon/tests/fixtures/multidir/two/README.txt with 100% similarity]
lib/tests/fixtures/update_validator/nocomponent/baz/lang/en/auth_baz.php [moved from admin/tool/installaddon/tests/fixtures/nocomponent/baz/lang/en/auth_baz.php with 99% similarity]
lib/tests/fixtures/update_validator/nocomponent/baz/version.php [moved from admin/tool/installaddon/tests/fixtures/nocomponent/baz/version.php with 55% similarity]
lib/tests/fixtures/update_validator/nolang/bah/index.php [new file with mode: 0644]
lib/tests/fixtures/update_validator/nolang/bah/lang/en/bah.php [new file with mode: 0644]
lib/tests/fixtures/update_validator/nolang/bah/lang/en/bleh.php [moved from admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bleh.php with 98% similarity]
lib/tests/fixtures/update_validator/nolang/bah/lib.php [new file with mode: 0644]
lib/tests/fixtures/update_validator/nolang/bah/version.php [moved from admin/tool/installaddon/tests/fixtures/nolang/bah/version.php with 98% similarity]
lib/tests/fixtures/update_validator/nolang/bah/view.php [moved from admin/tool/installaddon/tests/fixtures/nolang/bah/view.php with 98% similarity]
lib/tests/fixtures/update_validator/noversionmod/noversion/lang/en/noversion.php [moved from admin/tool/installaddon/tests/fixtures/noversionmod/noversion/lang/en/noversion.php with 98% similarity]
lib/tests/fixtures/update_validator/noversiontheme/noversion/lang/en/theme_noversion.php [moved from admin/tool/installaddon/tests/fixtures/noversiontheme/noversion/lang/en/theme_noversion.php with 98% similarity]
lib/tests/fixtures/update_validator/nowrapdir/index.php [new file with mode: 0644]
lib/tests/fixtures/update_validator/nowrapdir/lang/en/foo.php [moved from admin/tool/installaddon/tests/fixtures/nowrapdir/lang/en/foo.php with 97% similarity]
lib/tests/fixtures/update_validator/nowrapdir/version.php [moved from admin/tool/installaddon/tests/fixtures/nowrapdir/version.php with 97% similarity]
lib/tests/fixtures/update_validator/plugindir/foobar/index.php [moved from admin/tool/installaddon/tests/fixtures/installed/greenbar/index.php with 98% similarity]
lib/tests/fixtures/update_validator/plugindir/foobar/lang/en/local_foobar.php [moved from admin/tool/installaddon/tests/fixtures/plugindir/foobar/lang/en/local_foobar.php with 97% similarity]
lib/tests/fixtures/update_validator/plugindir/foobar/version.php [moved from admin/tool/installaddon/tests/fixtures/plugindir/foobar/version.php with 75% similarity]
lib/tests/fixtures/update_validator/plugindir/legacymod/lang/en/legacymod.php [moved from admin/tool/installaddon/tests/fixtures/plugindir/legacymod/lang/en/legacymod.php with 98% similarity]
lib/tests/fixtures/update_validator/plugindir/legacymod/version.php [moved from admin/tool/installaddon/tests/fixtures/plugindir/legacymod/version.php with 98% similarity]
lib/tests/fixtures/update_validator/versionphp/version1.php [moved from admin/tool/installaddon/tests/fixtures/versionphp/version1.php with 100% similarity]
lib/tests/fixtures/update_validator/wronglang/bah/lang/en/bah.php [moved from admin/tool/installaddon/tests/fixtures/wronglang/bah/lang/en/bah.php with 98% similarity]
lib/tests/fixtures/update_validator/wronglang/bah/version.php [moved from admin/tool/installaddon/tests/fixtures/wronglang/bah/version.php with 98% similarity]
lib/tests/fixtures/update_validator/zips/bar.zip [moved from admin/tool/installaddon/tests/fixtures/zips/bar.zip with 100% similarity]
lib/tests/fixtures/update_validator/zips/invalidroot.zip [moved from admin/tool/installaddon/tests/fixtures/zips/invalidroot.zip with 100% similarity]
lib/tests/plugin_manager_test.php
lib/tests/update_api_test.php [new file with mode: 0644]
lib/tests/update_checker_test.php
lib/tests/update_code_manager_test.php [new file with mode: 0644]
lib/tests/update_validator_test.php [moved from admin/tool/installaddon/tests/validator_test.php with 67% similarity]
lib/upgradelib.php
mdeploy.php [deleted file]
mdeploytest.php [deleted file]
theme/base/style/admin.css
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/style/moodle.css
version.php

index 3e73aa4..b8de2e6 100644 (file)
@@ -98,14 +98,39 @@ core_component::get_core_subsystems();
 require_once($CFG->libdir.'/adminlib.php');    // various admin-only functions
 require_once($CFG->libdir.'/upgradelib.php');  // general upgrade/install related functions
 
-$confirmupgrade = optional_param('confirmupgrade', 0, PARAM_BOOL);
-$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);
-$newaddonreq    = optional_param('installaddonrequest', null, PARAM_RAW);
-$upgradekeyhash = optional_param('upgradekeyhash', null, PARAM_ALPHANUM);
+$confirmupgrade = optional_param('confirmupgrade', 0, PARAM_BOOL); // Core upgrade confirmed?
+$confirmrelease = optional_param('confirmrelease', 0, PARAM_BOOL); // Core release info and server checks confirmed?
+$confirmplugins = optional_param('confirmplugincheck', 0, PARAM_BOOL); // Plugins check page confirmed?
+$showallplugins = optional_param('showallplugins', 0, PARAM_BOOL); // Show all plugins on the plugins check page?
+$agreelicense = optional_param('agreelicense', 0, PARAM_BOOL); // GPL license confirmed for installation?
+$fetchupdates = optional_param('fetchupdates', 0, PARAM_BOOL); // Should check for available updates?
+$newaddonreq = optional_param('installaddonrequest', null, PARAM_RAW); // Plugin installation requested at moodle.org/plugins.
+$upgradekeyhash = optional_param('upgradekeyhash', null, PARAM_ALPHANUM); // Hash of provided upgrade key.
+$installdep = optional_param('installdep', null, PARAM_COMPONENT); // Install given missing dependency (required plugin).
+$installdepx = optional_param('installdepx', false, PARAM_BOOL); // Install all missing dependencies.
+$confirminstalldep = optional_param('confirminstalldep', false, PARAM_BOOL); // Installing dependencies confirmed.
+$abortinstall = optional_param('abortinstall', null, PARAM_COMPONENT); // Cancel installation of the given new plugin.
+$abortinstallx = optional_param('abortinstallx', null, PARAM_BOOL); // Cancel installation of all new plugins.
+$confirmabortinstall = optional_param('confirmabortinstall', false, PARAM_BOOL); // Installation cancel confirmed.
+$abortupgrade = optional_param('abortupgrade', null, PARAM_COMPONENT); // Cancel upgrade of the given existing plugin.
+$abortupgradex = optional_param('abortupgradex', null, PARAM_BOOL); // Cancel upgrade of all upgradable plugins.
+$confirmabortupgrade = optional_param('confirmabortupgrade', false, PARAM_BOOL); // Upgrade cancel confirmed.
+$installupdate = optional_param('installupdate', null, PARAM_COMPONENT); // Install given available update.
+$installupdateversion = optional_param('installupdateversion', null, PARAM_INT); // Version of the available update to install.
+$installupdatex = optional_param('installupdatex', false, PARAM_BOOL); // Install all available plugin updates.
+$confirminstallupdate = optional_param('confirminstallupdate', false, PARAM_BOOL); // Available update(s) install confirmed?
+
+if (!empty($CFG->disableupdateautodeploy)) {
+    // Invalidate all requests to install plugins via the admin UI.
+    $newaddonreq = null;
+    $installdep = null;
+    $installdepx = false;
+    $abortupgrade = null;
+    $abortupgradex = null;
+    $installupdate = null;
+    $installupdateversion = null;
+    $installupdatex = false;
+}
 
 // Set up PAGE.
 $url = new moodle_url('/admin/index.php');
@@ -117,7 +142,7 @@ $PAGE->set_url($url);
 unset($url);
 
 // Are we returning from an add-on installation request at moodle.org/plugins?
-if ($newaddonreq and !$cache and empty($CFG->disableonclickaddoninstall)) {
+if ($newaddonreq and !$cache and empty($CFG->disableupdateautodeploy)) {
     $target = new moodle_url('/admin/tool/installaddon/index.php', array(
         'installaddonrequest' => $newaddonreq,
         'confirm' => 0));
@@ -188,7 +213,6 @@ if (!core_tables_exist()) {
         $PAGE->set_heading($strinstallation);
         $PAGE->set_cacheable(false);
 
-        /** @var core_admin_renderer $output */
         $output = $PAGE->get_renderer('core', 'admin');
         echo $output->install_licence_page();
         die();
@@ -203,7 +227,6 @@ if (!core_tables_exist()) {
         $PAGE->set_heading($strinstallation . ' - Moodle ' . $CFG->target_release);
         $PAGE->set_cacheable(false);
 
-        /** @var core_admin_renderer $output */
         $output = $PAGE->get_renderer('core', 'admin');
         echo $output->install_environment_page($maturity, $envstatus, $environment_results, $release);
         die();
@@ -272,6 +295,12 @@ if ($CFG->version != $DB->get_field('config', 'value', array('name'=>'version'))
 
 if (!$cache and $version > $CFG->version) {  // upgrade
 
+    $PAGE->set_url(new moodle_url($PAGE->url, array(
+        'confirmupgrade' => $confirmupgrade,
+        'confirmrelease' => $confirmrelease,
+        'confirmplugincheck' => $confirmplugins,
+    )));
+
     check_upgrade_key($upgradekeyhash);
 
     // Warning about upgrading a test site.
@@ -289,7 +318,6 @@ if (!$cache and $version > $CFG->version) {  // upgrade
     // We then purge the regular caches.
     purge_all_caches();
 
-    /** @var core_admin_renderer $output */
     $output = $PAGE->get_renderer('core', 'admin');
 
     if (upgrade_stale_php_files_present()) {
@@ -334,40 +362,137 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         $PAGE->set_heading($strplugincheck);
         $PAGE->set_cacheable(false);
 
-        $reloadurl = new moodle_url($PAGE->url, array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
+        $pluginman = core_plugin_manager::instance();
 
+        // Check for available updates.
         if ($fetchupdates) {
             // No sesskey support guaranteed here, because sessions might not work yet.
             $updateschecker = \core\update\checker::instance();
             if ($updateschecker->enabled()) {
                 $updateschecker->fetch();
             }
-            redirect($reloadurl);
+            redirect($PAGE->url);
         }
 
-        $deployer = \core\update\deployer::instance();
-        if ($deployer->enabled()) {
-            $deployer->initialize($reloadurl, $reloadurl);
+        // Cancel all plugin installations.
+        if ($abortinstallx) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            $abortables = $pluginman->list_cancellable_installations();
+            if ($abortables) {
+                if ($confirmabortinstall) {
+                    foreach ($abortables as $plugin) {
+                        $pluginman->cancel_plugin_installation($plugin->component);
+                    }
+                    redirect($PAGE->url);
+                } else {
+                    $continue = new moodle_url($PAGE->url, array('abortinstallx' => $abortinstallx, 'confirmabortinstall' => 1));
+                    echo $output->upgrade_confirm_abort_install_page($abortables, $continue);
+                    die();
+                }
+            }
+            redirect($PAGE->url);
+        }
 
-            $deploydata = $deployer->submitted_data();
-            if (!empty($deploydata)) {
-                // No sesskey support guaranteed here, because sessions might not work yet.
-                echo $output->upgrade_plugin_confirm_deploy_page($deployer, $deploydata);
-                die();
+        // Cancel single plugin installation.
+        if ($abortinstall) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            if ($confirmabortinstall) {
+                $pluginman->cancel_plugin_installation($abortinstall);
+                redirect($PAGE->url);
+            } else {
+                $continue = new moodle_url($PAGE->url, array('abortinstall' => $abortinstall, 'confirmabortinstall' => 1));
+                $abortable = $pluginman->get_plugin_info($abortinstall);
+                if ($pluginman->can_cancel_plugin_installation($abortable)) {
+                    echo $output->upgrade_confirm_abort_install_page(array($abortable), $continue);
+                    die();
+                }
+                redirect($PAGE->url);
+            }
+        }
+
+        // Cancel all plugins upgrades (that is, restore archived versions).
+        if ($abortupgradex) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            $restorable = $pluginman->list_restorable_archives();
+            if ($restorable) {
+                upgrade_install_plugins($restorable, $confirmabortupgrade,
+                    get_string('cancelupgradehead', 'core_plugin'),
+                    new moodle_url($PAGE->url, array('abortupgradex' => 1, 'confirmabortupgrade' => 1))
+                );
+            }
+            redirect($PAGE->url);
+        }
+
+        // Cancel single plugin upgrade (that is, install the archived version).
+        if ($abortupgrade) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            $restorable = $pluginman->list_restorable_archives();
+            if (isset($restorable[$abortupgrade])) {
+                $restorable = array($restorable[$abortupgrade]);
+                upgrade_install_plugins($restorable, $confirmabortupgrade,
+                    get_string('cancelupgradehead', 'core_plugin'),
+                    new moodle_url($PAGE->url, array('abortupgrade' => $abortupgrade, 'confirmabortupgrade' => 1))
+                );
+            }
+            redirect($PAGE->url);
+        }
+
+        // Install all available missing dependencies.
+        if ($installdepx) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            $installable = $pluginman->filter_installable($pluginman->missing_dependencies(true));
+            upgrade_install_plugins($installable, $confirminstalldep,
+                get_string('dependencyinstallhead', 'core_plugin'),
+                new moodle_url($PAGE->url, array('installdepx' => 1, 'confirminstalldep' => 1))
+            );
+        }
+
+        // Install single available missing dependency.
+        if ($installdep) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            $installable = $pluginman->filter_installable($pluginman->missing_dependencies(true));
+            if (!empty($installable[$installdep])) {
+                $installable = array($installable[$installdep]);
+                upgrade_install_plugins($installable, $confirminstalldep,
+                    get_string('dependencyinstallhead', 'core_plugin'),
+                    new moodle_url($PAGE->url, array('installdep' => $installdep, 'confirminstalldep' => 1))
+                );
+            }
+        }
+
+        // Install all available updates.
+        if ($installupdatex) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            $installable = $pluginman->filter_installable($pluginman->available_updates());
+            upgrade_install_plugins($installable, $confirminstallupdate,
+                get_string('updateavailableinstallallhead', 'core_admin'),
+                new moodle_url($PAGE->url, array('installupdatex' => 1, 'confirminstallupdate' => 1))
+            );
+        }
+
+        // Install single available update.
+        if ($installupdate and $installupdateversion) {
+            // No sesskey support guaranteed here, because sessions might not work yet.
+            if ($pluginman->is_remote_plugin_installable($installupdate, $installupdateversion)) {
+                $installable = array($pluginman->get_remote_plugin_info($installupdate, $installupdateversion, true));
+                upgrade_install_plugins($installable, $confirminstallupdate,
+                    get_string('updateavailableinstallallhead', 'core_admin'),
+                    new moodle_url($PAGE->url, array('installupdate' => $installupdate,
+                        'installupdateversion' => $installupdateversion, 'confirminstallupdate' => 1)
+                    )
+                );
             }
         }
 
         echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
-                $version, $showallplugins, $reloadurl, new moodle_url($PAGE->url, array(
-                'confirmupgrade' => 1, 'confirmrelease' => 1, 'confirmplugincheck' => 1, 'cache' => 0)));
+                $version, $showallplugins, $PAGE->url, new moodle_url($PAGE->url, array('confirmplugincheck' => 1)));
         die();
 
     } else {
         // Always verify plugin dependencies!
         $failed = array();
         if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
-            $reloadurl = new moodle_url($PAGE->url, array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
-            echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
+            echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
             die();
         }
         unset($failed);
@@ -391,11 +516,16 @@ if (!$cache and $branch <> $CFG->branch) {  // Update the branch
 
 if (!$cache and moodle_needs_upgrading()) {
 
+    $PAGE->set_url(new moodle_url($PAGE->url, array('confirmplugincheck' => $confirmplugins)));
+
     check_upgrade_key($upgradekeyhash);
 
     if (!$PAGE->headerprinted) {
         // means core upgrade or installation was not already done
 
+        $pluginman = core_plugin_manager::instance();
+        $output = $PAGE->get_renderer('core', 'admin');
+
         if (!$confirmplugins) {
             $strplugincheck = get_string('plugincheck');
 
@@ -404,6 +534,7 @@ if (!$cache and moodle_needs_upgrading()) {
             $PAGE->set_heading($strplugincheck);
             $PAGE->set_cacheable(false);
 
+            // Check for available updates.
             if ($fetchupdates) {
                 require_sesskey();
                 $updateschecker = \core\update\checker::instance();
@@ -413,23 +544,119 @@ if (!$cache and moodle_needs_upgrading()) {
                 redirect($PAGE->url);
             }
 
-            /** @var core_admin_renderer $output */
-            $output = $PAGE->get_renderer('core', 'admin');
+            // Cancel all plugin installations.
+            if ($abortinstallx) {
+                require_sesskey();
+                $abortables = $pluginman->list_cancellable_installations();
+                if ($abortables) {
+                    if ($confirmabortinstall) {
+                        foreach ($abortables as $plugin) {
+                            $pluginman->cancel_plugin_installation($plugin->component);
+                        }
+                        redirect($PAGE->url);
+                    } else {
+                        $continue = new moodle_url($PAGE->url, array('abortinstallx' => $abortinstallx,
+                            'confirmabortinstall' => 1));
+                        echo $output->upgrade_confirm_abort_install_page($abortables, $continue);
+                        die();
+                    }
+                }
+                redirect($PAGE->url);
+            }
+
+            // Cancel single plugin installation.
+            if ($abortinstall) {
+                require_sesskey();
+                if ($confirmabortinstall) {
+                    $pluginman->cancel_plugin_installation($abortinstall);
+                    redirect($PAGE->url);
+                } else {
+                    $continue = new moodle_url($PAGE->url, array('abortinstall' => $abortinstall, 'confirmabortinstall' => 1));
+                    $abortable = $pluginman->get_plugin_info($abortinstall);
+                    if ($pluginman->can_cancel_plugin_installation($abortable)) {
+                        echo $output->upgrade_confirm_abort_install_page(array($abortable), $continue);
+                        die();
+                    }
+                    redirect($PAGE->url);
+                }
+            }
 
-            $deployer = \core\update\deployer::instance();
-            if ($deployer->enabled()) {
-                $deployer->initialize($PAGE->url, $PAGE->url);
+            // Cancel all plugins upgrades (that is, restore archived versions).
+            if ($abortupgradex) {
+                require_sesskey();
+                $restorable = $pluginman->list_restorable_archives();
+                if ($restorable) {
+                    upgrade_install_plugins($restorable, $confirmabortupgrade,
+                        get_string('cancelupgradehead', 'core_plugin'),
+                        new moodle_url($PAGE->url, array('abortupgradex' => 1, 'confirmabortupgrade' => 1))
+                    );
+                }
+                redirect($PAGE->url);
+            }
 
-                $deploydata = $deployer->submitted_data();
-                if (!empty($deploydata)) {
-                    require_sesskey();
-                    echo $output->upgrade_plugin_confirm_deploy_page($deployer, $deploydata);
-                    die();
+            // Cancel single plugin upgrade (that is, install the archived version).
+            if ($abortupgrade) {
+                require_sesskey();
+                $restorable = $pluginman->list_restorable_archives();
+                if (isset($restorable[$abortupgrade])) {
+                    $restorable = array($restorable[$abortupgrade]);
+                    upgrade_install_plugins($restorable, $confirmabortupgrade,
+                        get_string('cancelupgradehead', 'core_plugin'),
+                        new moodle_url($PAGE->url, array('abortupgrade' => $abortupgrade, 'confirmabortupgrade' => 1))
+                    );
+                }
+                redirect($PAGE->url);
+            }
+
+            // Install all available missing dependencies.
+            if ($installdepx) {
+                require_sesskey();
+                $installable = $pluginman->filter_installable($pluginman->missing_dependencies(true));
+                upgrade_install_plugins($installable, $confirminstalldep,
+                    get_string('dependencyinstallhead', 'core_plugin'),
+                    new moodle_url($PAGE->url, array('installdepx' => 1, 'confirminstalldep' => 1))
+                );
+            }
+
+            // Install single available missing dependency.
+            if ($installdep) {
+                require_sesskey();
+                $installable = $pluginman->filter_installable($pluginman->missing_dependencies(true));
+                if (!empty($installable[$installdep])) {
+                    $installable = array($installable[$installdep]);
+                    upgrade_install_plugins($installable, $confirminstalldep,
+                        get_string('dependencyinstallhead', 'core_plugin'),
+                        new moodle_url($PAGE->url, array('installdep' => $installdep, 'confirminstalldep' => 1))
+                    );
+                }
+            }
+
+            // Install all available updates.
+            if ($installupdatex) {
+                require_sesskey();
+                $installable = $pluginman->filter_installable($pluginman->available_updates());
+                upgrade_install_plugins($installable, $confirminstallupdate,
+                    get_string('updateavailableinstallallhead', 'core_admin'),
+                    new moodle_url($PAGE->url, array('installupdatex' => 1, 'confirminstallupdate' => 1))
+                );
+            }
+
+            // Install single available update.
+            if ($installupdate and $installupdateversion) {
+                require_sesskey();
+                if ($pluginman->is_remote_plugin_installable($installupdate, $installupdateversion)) {
+                    $installable = array($pluginman->get_remote_plugin_info($installupdate, $installupdateversion, true));
+                    upgrade_install_plugins($installable, $confirminstallupdate,
+                        get_string('updateavailableinstallallhead', 'core_admin'),
+                        new moodle_url($PAGE->url, array('installupdate' => $installupdate,
+                            'installupdateversion' => $installupdateversion, 'confirminstallupdate' => 1)
+                        )
+                    );
                 }
             }
 
             // Show plugins info.
-            echo $output->upgrade_plugin_check_page(core_plugin_manager::instance(), \core\update\checker::instance(),
+            echo $output->upgrade_plugin_check_page($pluginman, \core\update\checker::instance(),
                     $version, $showallplugins,
                     new moodle_url($PAGE->url),
                     new moodle_url($PAGE->url, array('confirmplugincheck' => 1, 'cache' => 0)));
@@ -438,11 +665,9 @@ if (!$cache and moodle_needs_upgrading()) {
 
         // Make sure plugin dependencies are always checked.
         $failed = array();
-        if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
-            /** @var core_admin_renderer $output */
+        if (!$pluginman->all_plugins_ok($version, $failed)) {
             $output = $PAGE->get_renderer('core', 'admin');
-            $reloadurl = new moodle_url($PAGE->url, array('cache' => 0));
-            echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
+            echo $output->unsatisfied_dependencies_page($version, $failed, $PAGE->url);
             die();
         }
         unset($failed);
@@ -588,7 +813,7 @@ $updateschecker = \core\update\checker::instance();
 $availableupdates = array();
 $availableupdatesfetch = null;
 
-if (empty($CFG->disableupdatenotifications)) {
+if ($updateschecker->enabled()) {
     // Only compute the update information when it is going to be displayed to the user.
     $availableupdates['core'] = $updateschecker->get_update_info('core',
         array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
@@ -597,14 +822,13 @@ if (empty($CFG->disableupdatenotifications)) {
     $pluginman = core_plugin_manager::instance();
     foreach ($pluginman->get_plugins() as $plugintype => $plugintypeinstances) {
         foreach ($plugintypeinstances as $pluginname => $plugininfo) {
-            if (!empty($plugininfo->availableupdates)) {
-                foreach ($plugininfo->availableupdates as $pluginavailableupdate) {
-                    if ($pluginavailableupdate->version > $plugininfo->versiondisk) {
-                        if (!isset($availableupdates[$plugintype.'_'.$pluginname])) {
-                            $availableupdates[$plugintype.'_'.$pluginname] = array();
-                        }
-                        $availableupdates[$plugintype.'_'.$pluginname][] = $pluginavailableupdate;
+            $pluginavailableupdates = $plugininfo->available_updates();
+            if (!empty($pluginavailableupdates)) {
+                foreach ($pluginavailableupdates as $pluginavailableupdate) {
+                    if (!isset($availableupdates[$plugintype.'_'.$pluginname])) {
+                        $availableupdates[$plugintype.'_'.$pluginname] = array();
                     }
+                    $availableupdates[$plugintype.'_'.$pluginname][] = $pluginavailableupdate;
                 }
             }
         }
@@ -622,7 +846,6 @@ $cachewarnings = cache_helper::warnings();
 
 admin_externalpage_setup('adminnotifications');
 
-/* @var core_admin_renderer $output */
 $output = $PAGE->get_renderer('core', 'admin');
 
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
index c156f9d..39510cc 100644 (file)
 /**
  * UI for general plugins management
  *
- * Supported HTTP parameters:
- *
- *  ?fetchremote=1      - check for available updates
- *  ?updatesonly=1      - display plugins with available update only
- *  ?contribonly=1      - display non-standard add-ons only
- *  ?uninstall=foo_bar  - uninstall the given plugin
- *  ?delete=foo_bar     - delete the plugin folder (it must not be installed)
- *  &confirm=1          - confirm the uninstall or delete action
- *
  * @package    core
  * @subpackage admin
  * @copyright  2011 David Mudrak <david@moodle.com>
@@ -37,13 +28,17 @@ require_once(dirname(dirname(__FILE__)) . '/config.php');
 require_once($CFG->libdir . '/adminlib.php');
 require_once($CFG->libdir . '/filelib.php');
 
-$fetchremote = optional_param('fetchremote', false, PARAM_BOOL);
-$updatesonly = optional_param('updatesonly', false, PARAM_BOOL);
-$contribonly = optional_param('contribonly', false, PARAM_BOOL);
-$uninstall   = optional_param('uninstall', '', PARAM_COMPONENT);
-$delete      = optional_param('delete', '', PARAM_COMPONENT);
-$confirmed   = optional_param('confirm', false, PARAM_BOOL);
-$return      = optional_param('return', 'overview', PARAM_ALPHA);
+$fetchupdates = optional_param('fetchupdates', false, PARAM_BOOL); // Check for available plugins updates.
+$updatesonly = optional_param('updatesonly', false, PARAM_BOOL); // Show updateable plugins only.
+$contribonly = optional_param('contribonly', false, PARAM_BOOL); // Show additional plugins only.
+$uninstall = optional_param('uninstall', '', PARAM_COMPONENT); // Uninstall the plugin.
+$delete = optional_param('delete', '', PARAM_COMPONENT); // Delete the plugin folder after it is uninstalled.
+$confirmed = optional_param('confirm', false, PARAM_BOOL); // Confirm the uninstall/delete action.
+$return = optional_param('return', 'overview', PARAM_ALPHA); // Where to return after uninstall.
+$installupdate = optional_param('installupdate', null, PARAM_COMPONENT); // Install given available update.
+$installupdateversion = optional_param('installupdateversion', null, PARAM_INT); // Version of the available update to install.
+$installupdatex = optional_param('installupdatex', false, PARAM_BOOL); // Install all available plugin updates.
+$confirminstallupdate = optional_param('confirminstallupdate', false, PARAM_BOOL); // Available update(s) install confirmed?
 
 // NOTE: do not use admin_externalpage_setup() here because it loads
 //       full admin tree which is not possible during uninstallation.
@@ -52,15 +47,19 @@ require_login();
 $syscontext = context_system::instance();
 require_capability('moodle/site:config', $syscontext);
 
+// URL params we want to maintain on redirects.
+$pageparams = array('updatesonly' => $updatesonly, 'contribonly' => $contribonly);
+$pageurl = new moodle_url('/admin/plugins.php', $pageparams);
+
 $pluginman = core_plugin_manager::instance();
 
 if ($uninstall) {
     require_sesskey();
 
     if (!$confirmed) {
-        admin_externalpage_setup('pluginsoverview');
+        admin_externalpage_setup('pluginsoverview', '', $pageparams);
     } else {
-        $PAGE->set_url('/admin/plugins.php');
+        $PAGE->set_url($pageurl);
         $PAGE->set_context($syscontext);
         $PAGE->set_pagelayout('maintenance');
         $PAGE->set_popup_notification_allowed(false);
@@ -122,7 +121,7 @@ if ($uninstall) {
 if ($delete and $confirmed) {
     require_sesskey();
 
-    $PAGE->set_url('/admin/plugins.php');
+    $PAGE->set_url($pageurl);
     $PAGE->set_context($syscontext);
     $PAGE->set_pagelayout('maintenance');
     $PAGE->set_popup_notification_allowed(false);
@@ -149,13 +148,6 @@ if ($delete and $confirmed) {
             'core_plugin_manager::get_plugin_info() returned not-null versiondb for the plugin to be deleted');
     }
 
-    // Make sure the folder is removable.
-    if (!$pluginman->is_plugin_folder_removable($pluginfo->component)) {
-        throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
-            array('plugin' => $pluginfo->component, 'rootdir' => $pluginfo->rootdir),
-            'plugin root folder is not removable as expected');
-    }
-
     // Make sure the folder is within Moodle installation tree.
     if (strpos($pluginfo->rootdir, $CFG->dirroot) !== 0) {
         throw new moodle_exception('err_unexpected_plugin_rootdir', 'core_plugin', '',
@@ -164,44 +156,61 @@ if ($delete and $confirmed) {
     }
 
     // So long, and thanks for all the bugs.
-    fulldelete($pluginfo->rootdir);
-    // Reset op code caches.
-    if (function_exists('opcache_reset')) {
-        opcache_reset();
-    }
+    $pluginman->remove_plugin_folder($pluginfo);
+
     // We need to execute upgrade to make sure everything including caches is up to date.
     redirect(new moodle_url('/admin/index.php'));
 }
 
-admin_externalpage_setup('pluginsoverview');
+// Install all avilable updates.
+if ($installupdatex) {
+    require_once($CFG->libdir.'/upgradelib.php');
+    require_sesskey();
+
+    $PAGE->set_url($pageurl);
+    $PAGE->set_context($syscontext);
+    $PAGE->set_pagelayout('maintenance');
+    $PAGE->set_popup_notification_allowed(false);
+
+    $installable = $pluginman->filter_installable($pluginman->available_updates());
+    upgrade_install_plugins($installable, $confirminstallupdate,
+        get_string('updateavailableinstallallhead', 'core_admin'),
+        new moodle_url($PAGE->url, array('installupdatex' => 1, 'confirminstallupdate' => 1))
+    );
+}
+
+// Install single available update.
+if ($installupdate and $installupdateversion) {
+    require_once($CFG->libdir.'/upgradelib.php');
+    require_sesskey();
+
+    $PAGE->set_url($pageurl);
+    $PAGE->set_context($syscontext);
+    $PAGE->set_pagelayout('maintenance');
+    $PAGE->set_popup_notification_allowed(false);
+
+    if ($pluginman->is_remote_plugin_installable($installupdate, $installupdateversion)) {
+        $installable = array($pluginman->get_remote_plugin_info($installupdate, $installupdateversion, true));
+        upgrade_install_plugins($installable, $confirminstallupdate,
+            get_string('updateavailableinstallallhead', 'core_admin'),
+            new moodle_url($PAGE->url, array('installupdate' => $installupdate,
+                'installupdateversion' => $installupdateversion, 'confirminstallupdate' => 1)
+            )
+        );
+    }
+}
+
+admin_externalpage_setup('pluginsoverview', '', $pageparams);
 
 /** @var core_admin_renderer $output */
 $output = $PAGE->get_renderer('core', 'admin');
 
 $checker = \core\update\checker::instance();
 
-// Filtering options.
-$options = array(
-    'updatesonly' => $updatesonly,
-    'contribonly' => $contribonly,
-);
-
-if ($fetchremote) {
+if ($fetchupdates) {
     require_sesskey();
     $checker->fetch();
-    redirect(new moodle_url($PAGE->url, $options));
-}
-
-$deployer = \core\update\deployer::instance();
-if ($deployer->enabled()) {
-    $myurl = new moodle_url($PAGE->url, array('updatesonly' => $updatesonly, 'contribonly' => $contribonly));
-    $deployer->initialize($myurl, new moodle_url('/admin'));
-
-    $deploydata = $deployer->submitted_data();
-    if (!empty($deploydata)) {
-        echo $output->upgrade_plugin_confirm_deploy_page($deployer, $deploydata);
-        die();
-    }
+    redirect($PAGE->url);
 }
 
-echo $output->plugin_management_page($pluginman, $checker, $options);
+echo $output->plugin_management_page($pluginman, $checker, $pageparams);
index 40036da..12d9dc3 100644 (file)
@@ -202,25 +202,14 @@ class core_admin_renderer extends plugin_renderer_base {
      */
     public function upgrade_plugin_check_page(core_plugin_manager $pluginman, \core\update\checker $checker,
             $version, $showallplugins, $reloadurl, $continueurl) {
-        global $CFG;
 
         $output = '';
 
         $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 (empty($CFG->disableupdatenotifications)) {
-            $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->box_start('generalbox', 'plugins-check-page');
+        $output .= html_writer::tag('p', get_string('pluginchecknotice', 'core_plugin'), array('class' => 'page-description'));
+        $output .= $this->check_for_updates_button($checker, $reloadurl);
+        $output .= $this->missing_dependencies($pluginman);
         $output .= $this->plugins_check_table($pluginman, $version, array('full' => $showallplugins));
         $output .= $this->box_end();
         $output .= $this->upgrade_reload($reloadurl);
@@ -240,59 +229,38 @@ class core_admin_renderer extends plugin_renderer_base {
     }
 
     /**
-     * Prints a page with a summary of plugin deployment to be confirmed.
+     * Display a page to confirm plugin installation cancelation.
      *
-     * @param \core\update\deployer $deployer
-     * @param array $data deployer's data package as returned by {@link \core\update\deployer::submitted_data()}
+     * @param array $abortable list of \core\update\plugininfo
+     * @param moodle_url $continue
      * @return string
      */
-    public function upgrade_plugin_confirm_deploy_page(\core\update\deployer $deployer, array $data) {
-
-        if (!$deployer->initialized()) {
-            throw new coding_exception('Unable to render a page for non-initialized deployer.');
-        }
-
-        if (empty($data['updateinfo'])) {
-            throw new coding_exception('Missing required data component.');
-        }
+    public function upgrade_confirm_abort_install_page(array $abortable, moodle_url $continue) {
 
-        $updateinfo = $data['updateinfo'];
-
-        $output  = '';
-        $output .= $this->header();
-        $output .= $this->container_start('generalbox updateplugin', 'notice');
+        $pluginman = core_plugin_manager::instance();
 
-        $a = new stdClass();
-        if (get_string_manager()->string_exists('pluginname', $updateinfo->component)) {
-            $a->name = get_string('pluginname', $updateinfo->component);
-        } else {
-            $a->name = $updateinfo->component;
+        if (empty($abortable)) {
+            // The UI should not allow this.
+            throw new moodle_exception('err_no_plugin_install_abortable', 'core_plugin');
         }
 
-        if (isset($updateinfo->release)) {
-            $a->version = $updateinfo->release . ' (' . $updateinfo->version . ')';
-        } else {
-            $a->version = $updateinfo->version;
-        }
-        $a->url = $updateinfo->download;
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('cancelinstallhead', 'core_plugin'), 3);
+        $out .= $this->output->container(get_string('cancelinstallinfo', 'core_plugin'), 'cancelinstallinfo');
 
-        $output .= $this->output->heading(get_string('updatepluginconfirm', 'core_plugin'));
-        $output .= $this->output->container(format_text(get_string('updatepluginconfirminfo', 'core_plugin', $a)), 'updatepluginconfirminfo');
-        $output .= $this->output->container(get_string('updatepluginconfirmwarning', 'core_plugin', 'updatepluginconfirmwarning'));
-
-        if ($repotype = $deployer->plugin_external_source($data['updateinfo'])) {
-            $output .= $this->output->container(get_string('updatepluginconfirmexternal', 'core_plugin', $repotype), 'updatepluginconfirmexternal');
+        foreach ($abortable as $pluginfo) {
+            $out .= $this->output->heading($pluginfo->displayname.' ('.$pluginfo->component.')', 4);
+            $out .= $this->output->container(get_string('cancelinstallinfodir', 'core_plugin', $pluginfo->rootdir));
+            if ($repotype = $pluginman->plugin_external_source($pluginfo->component)) {
+                $out .= $this->output->container(get_string('uninstalldeleteconfirmexternal', 'core_plugin', $repotype),
+                    'uninstalldeleteconfirmexternal');
+            }
         }
 
-        $widget = $deployer->make_execution_widget($data['updateinfo'], $data['returnurl']);
-        $output .= $this->output->render($widget);
-
-        $output .= $this->output->single_button($data['callerurl'], get_string('cancel', 'core'), 'get');
+        $out .= $this->plugins_management_confirm_buttons($continue, $this->page->url);
+        $out .= $this->output->footer();
 
-        $output .= $this->container_end();
-        $output .= $this->footer();
-
-        return $output;
+        return $out;
     }
 
     /**
@@ -351,30 +319,43 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML to output.
      */
     public function plugin_management_page(core_plugin_manager $pluginman, \core\update\checker $checker, array $options = array()) {
-        global $CFG;
 
         $output = '';
 
         $output .= $this->header();
         $output .= $this->heading(get_string('pluginsoverview', 'core_admin'));
+        $output .= $this->check_for_updates_button($checker, $this->page->url);
         $output .= $this->plugins_overview_panel($pluginman, $options);
+        $output .= $this->plugins_control_panel($pluginman, $options);
+        $output .= $this->footer();
+
+        return $output;
+    }
+
+    /**
+     * Renders a button to fetch for available updates.
+     *
+     * @param \core\update\checker $checker
+     * @param moodle_url $reloadurl
+     * @return string HTML
+     */
+    public function check_for_updates_button(\core\update\checker $checker, $reloadurl) {
+
+        $output = '';
 
-        if (empty($CFG->disableupdatenotifications)) {
+        if ($checker->enabled()) {
             $output .= $this->container_start('checkforupdates');
             $output .= $this->single_button(
-                new moodle_url($this->page->url, array_merge($options, array('fetchremote' => 1))),
+                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'))));
+                $timefetched = userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'));
+                $output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched), 'lasttimefetched');
             }
             $output .= $this->container_end();
         }
 
-        $output .= $this->box($this->plugins_control_panel($pluginman, $options), 'generalbox');
-        $output .= $this->footer();
-
         return $output;
     }
 
@@ -856,7 +837,6 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function plugins_check_table(core_plugin_manager $pluginman, $version, array $options = array()) {
-        global $CFG;
 
         $plugininfo = $pluginman->get_plugins();
 
@@ -870,20 +850,27 @@ class core_admin_renderer extends plugin_renderer_base {
         $table = new html_table();
         $table->id = 'plugins-check';
         $table->head = array(
-            get_string('displayname', 'core_plugin'),
-            get_string('rootdir', 'core_plugin'),
-            get_string('source', 'core_plugin'),
+            get_string('displayname', 'core_plugin').' / '.get_string('rootdir', 'core_plugin'),
             get_string('versiondb', 'core_plugin'),
             get_string('versiondisk', 'core_plugin'),
             get_string('requires', 'core_plugin'),
-            get_string('status', 'core_plugin'),
+            get_string('source', 'core_plugin').' / '.get_string('status', 'core_plugin'),
         );
         $table->colclasses = array(
-            'displayname', 'rootdir', 'source', 'versiondb', 'versiondisk', 'requires', 'status',
+            'displayname', 'versiondb', 'versiondisk', 'requires', 'status',
         );
         $table->data = array();
 
-        $numofhighlighted = array();    // number of highlighted rows per this subsection
+        // Number of displayed plugins per type.
+        $numdisplayed = array();
+        // Number of plugins known to the plugin manager.
+        $sumtotal = 0;
+        // Number of plugins requiring attention.
+        $sumattention = 0;
+        // List of all components we can cancel installation of.
+        $installabortable = $pluginman->list_cancellable_installations();
+        // List of all components we can cancel upgrade of.
+        $upgradeabortable = $pluginman->list_restorable_archives();
 
         foreach ($plugininfo as $type => $plugins) {
 
@@ -893,7 +880,7 @@ class core_admin_renderer extends plugin_renderer_base {
             $header = new html_table_row(array($header));
             $header->attributes['class'] = 'plugintypeheader type-' . $type;
 
-            $numofhighlighted[$type] = 0;
+            $numdisplayed[$type] = 0;
 
             if (empty($plugins) and $options['full']) {
                 $msg = new html_table_cell(get_string('noneinstalled', 'core_plugin'));
@@ -908,52 +895,91 @@ class core_admin_renderer extends plugin_renderer_base {
             $plugintyperows = array();
 
             foreach ($plugins as $name => $plugin) {
+                $sumtotal++;
                 $row = new html_table_row();
                 $row->attributes['class'] = 'type-' . $plugin->type . ' name-' . $plugin->type . '_' . $plugin->name;
 
                 if ($this->page->theme->resolve_image_location('icon', $plugin->type . '_' . $plugin->name, null)) {
                     $icon = $this->output->pix_icon('icon', '', $plugin->type . '_' . $plugin->name, array('class' => 'smallicon pluginicon'));
                 } else {
-                    $icon = $this->output->pix_icon('spacer', '', 'moodle', array('class' => 'smallicon pluginicon noicon'));
+                    $icon = '';
                 }
-                $displayname  = $icon . ' ' . $plugin->displayname;
-                $displayname = new html_table_cell($displayname);
 
-                $rootdir = new html_table_cell($plugin->get_dir());
+                $displayname = new html_table_cell(
+                    $icon.
+                    html_writer::span($plugin->displayname, 'pluginname').
+                    html_writer::div($plugin->get_dir(), 'plugindir')
+                );
+
+                $versiondb = new html_table_cell($plugin->versiondb);
+                $versiondisk = new html_table_cell($plugin->versiondisk);
 
                 if ($isstandard = $plugin->is_standard()) {
                     $row->attributes['class'] .= ' standard';
-                    $source = new html_table_cell(get_string('sourcestd', 'core_plugin'));
+                    $sourcelabel = html_writer::span(get_string('sourcestd', 'core_plugin'), 'sourcetext label');
                 } else {
                     $row->attributes['class'] .= ' extension';
-                    $source = new html_table_cell(get_string('sourceext', 'core_plugin'));
+                    $sourcelabel = html_writer::span(get_string('sourceext', 'core_plugin'), 'sourcetext label label-info');
                 }
 
-                $versiondb = new html_table_cell($plugin->versiondb);
-                $versiondisk = new html_table_cell($plugin->versiondisk);
+                $coredependency = $plugin->is_core_dependency_satisfied($version);
+                $otherpluginsdependencies = $pluginman->are_dependencies_satisfied($plugin->get_other_required_plugins());
+                $dependenciesok = $coredependency && $otherpluginsdependencies;
 
                 $statuscode = $plugin->get_status();
                 $row->attributes['class'] .= ' status-' . $statuscode;
-                $status = get_string('status_' . $statuscode, 'core_plugin');
+                $statusclass = 'statustext label ';
+                switch ($statuscode) {
+                    case core_plugin_manager::PLUGIN_STATUS_NEW:
+                        $statusclass .= $dependenciesok ? 'label-success' : 'label-warning';
+                        break;
+                    case core_plugin_manager::PLUGIN_STATUS_UPGRADE:
+                        $statusclass .= $dependenciesok ? 'label-info' : 'label-warning';
+                        break;
+                    case core_plugin_manager::PLUGIN_STATUS_MISSING:
+                    case core_plugin_manager::PLUGIN_STATUS_DOWNGRADE:
+                    case core_plugin_manager::PLUGIN_STATUS_DELETE:
+                        $statusclass .= 'label-important';
+                        break;
+                    case core_plugin_manager::PLUGIN_STATUS_NODB:
+                    case core_plugin_manager::PLUGIN_STATUS_UPTODATE:
+                        $statusclass .= $dependenciesok ? '' : 'label-warning';
+                        break;
+                }
+                $status = html_writer::span(get_string('status_' . $statuscode, 'core_plugin'), $statusclass);
+
+                if (!empty($installabortable[$plugin->component])) {
+                    $status .= $this->output->single_button(
+                        new moodle_url($this->page->url, array('abortinstall' => $plugin->component)),
+                        get_string('cancelinstallone', 'core_plugin'),
+                        'post',
+                        array('class' => 'actionbutton cancelinstallone')
+                    );
+                }
+
+                if (!empty($upgradeabortable[$plugin->component])) {
+                    $status .= $this->output->single_button(
+                        new moodle_url($this->page->url, array('abortupgrade' => $plugin->component)),
+                        get_string('cancelupgradeone', 'core_plugin'),
+                        'post',
+                        array('class' => 'actionbutton cancelupgradeone')
+                    );
+                }
 
                 $availableupdates = $plugin->available_updates();
-                if (!empty($availableupdates) and empty($CFG->disableupdatenotifications)) {
+                if (!empty($availableupdates)) {
                     foreach ($availableupdates as $availableupdate) {
-                        $status .= $this->plugin_available_update_info($availableupdate);
+                        $status .= $this->plugin_available_update_info($pluginman, $availableupdate);
                     }
                 }
 
-                $status = new html_table_cell($status);
+                $status = new html_table_cell($sourcelabel.' '.$status);
 
                 $requires = new html_table_cell($this->required_column($plugin, $pluginman, $version));
 
                 $statusisboring = in_array($statuscode, array(
                         core_plugin_manager::PLUGIN_STATUS_NODB, core_plugin_manager::PLUGIN_STATUS_UPTODATE));
 
-                $coredependency = $plugin->is_core_dependency_satisfied($version);
-                $otherpluginsdependencies = $pluginman->are_dependencies_satisfied($plugin->get_other_required_plugins());
-                $dependenciesok = $coredependency && $otherpluginsdependencies;
-
                 if ($options['xdep']) {
                     // we want to see only plugins with failed dependencies
                     if ($dependenciesok) {
@@ -966,17 +992,18 @@ class core_admin_renderer extends plugin_renderer_base {
                     if (empty($options['full'])) {
                         continue;
                     }
-                }
 
-                // ok, the plugin should be displayed
-                $numofhighlighted[$type]++;
+                } else {
+                    $sumattention++;
+                }
 
-                $row->cells = array($displayname, $rootdir, $source,
-                    $versiondb, $versiondisk, $requires, $status);
+                // The plugin should be displayed.
+                $numdisplayed[$type]++;
+                $row->cells = array($displayname, $versiondb, $versiondisk, $requires, $status);
                 $plugintyperows[] = $row;
             }
 
-            if (empty($numofhighlighted[$type]) and empty($options['full'])) {
+            if (empty($numdisplayed[$type]) and empty($options['full'])) {
                 continue;
             }
 
@@ -984,45 +1011,296 @@ class core_admin_renderer extends plugin_renderer_base {
             $table->data = array_merge($table->data, $plugintyperows);
         }
 
-        $sumofhighlighted = array_sum($numofhighlighted);
+        // Total number of displayed plugins.
+        $sumdisplayed = array_sum($numdisplayed);
 
         if ($options['xdep']) {
-            // we do not want to display no heading and links in this mode
-            $out = '';
+            // At the plugins dependencies check page, display the table only.
+            return html_writer::table($table);
+        }
 
-        } else if ($sumofhighlighted == 0) {
-            $out  = $this->output->container_start('nonehighlighted', 'plugins-check-info');
-            $out .= $this->output->heading(get_string('nonehighlighted', 'core_plugin'));
-            if (empty($options['full'])) {
-                $out .= html_writer::link(new moodle_url($this->page->url,
-                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
-                    get_string('nonehighlightedinfo', 'core_plugin'));
-            }
-            $out .= $this->output->container_end();
+        $out = $this->output->container_start('', 'plugins-check-info');
+
+        if ($sumdisplayed == 0) {
+            $out .= $this->output->heading(get_string('pluginchecknone', 'core_plugin'));
 
         } else {
-            $out  = $this->output->container_start('somehighlighted', 'plugins-check-info');
             if (empty($options['full'])) {
-                $out .= $this->output->heading(get_string('somehighlighted', 'core_plugin', $sumofhighlighted));
-                $out .= html_writer::link(new moodle_url($this->page->url,
-                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 1, 'cache' => 0)),
-                    get_string('somehighlightedinfo', 'core_plugin'));
+                $out .= $this->output->heading(get_string('plugincheckattention', 'core_plugin'));
             } else {
-                $out .= $this->output->heading(get_string('somehighlightedall', 'core_plugin', $sumofhighlighted));
-                $out .= html_writer::link(new moodle_url($this->page->url,
-                    array('confirmupgrade' => 1, 'confirmrelease' => 1, 'showallplugins' => 0, 'cache' => 0)),
-                    get_string('somehighlightedonly', 'core_plugin'));
+                $out .= $this->output->heading(get_string('plugincheckall', 'core_plugin'));
             }
-            $out .= $this->output->container_end();
         }
 
-        if ($sumofhighlighted > 0 or $options['full']) {
+        $out .= $this->output->container_start('actions');
+
+        $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')
+            );
+        }
+
+        if ($installabortable) {
+            $out .= $this->output->single_button(
+                new moodle_url($this->page->url, array('abortinstallx' => 1)),
+                get_string('cancelinstallall', 'core_plugin', count($installabortable)),
+                'post',
+                array('class' => 'singlebutton cancelinstallall')
+            );
+        }
+
+        if ($upgradeabortable) {
+            $out .= $this->output->single_button(
+                new moodle_url($this->page->url, array('abortupgradex' => 1)),
+                get_string('cancelupgradeall', 'core_plugin', count($upgradeabortable)),
+                'post',
+                array('class' => 'singlebutton cancelupgradeall')
+            );
+        }
+
+        $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'));
+
+        $out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 1)),
+            get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge'));
+
+        $out .= $this->output->container_end(); // End of .actions container.
+        $out .= $this->output->container_end(); // End of #plugins-check-info container.
+
+        if ($sumdisplayed > 0 or $options['full']) {
             $out .= html_writer::table($table);
         }
 
         return $out;
     }
 
+    /**
+     * Display the continue / cancel widgets for the plugins management pages.
+     *
+     * @param null|moodle_url $continue URL for the continue button, should it be displayed
+     * @param null|moodle_url $cancel URL for the cancel link, defaults to the current page
+     * @return string HTML
+     */
+    public function plugins_management_confirm_buttons(moodle_url $continue=null, moodle_url $cancel=null) {
+
+        $out = html_writer::start_div('plugins-management-confirm-buttons');
+
+        if (!empty($continue)) {
+            $out .= $this->output->single_button($continue, get_string('continue'), '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
+     *
+     * @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 if there
+                // is at least some version in the plugins directory.
+                $remoteinfoanyversion = $pluginman->get_remote_plugin_info($component, ANY_VERSION, false);
+                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(); // End of .plugins-check-dependencies-actions container.
+        }
+
+        if ($available) {
+            $out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
+            $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)),
+                    get_string('dependencyinstallmissing', 'core_plugin', count($installable)),
+                    'post',
+                    array('class' => 'singlebutton dependencyinstallmissing')
+                );
+            }
+
+            $out .= html_writer::div(html_writer::link(new moodle_url('/admin/tool/installaddon/'),
+                get_string('dependencyuploadmissing', 'core_plugin')), 'dependencyuploadmissing');
+
+            $out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
+
+            $out .= $this->available_missing_dependencies_list($pluginman, $available);
+        }
+
+        $out .= $this->output->container_end(); // End of .plugins-check-dependencies container.
+
+        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 .= $this->output->container_start('actions');
+
+            $info .= html_writer::div(
+                html_writer::link('https://moodle.org/plugins/view.php?plugin='.$plugin->component,
+                    get_string('misdepinfoplugin', 'core_plugin')),
+                'misdepinfoplugin'
+            );
+
+            $info .= html_writer::div(
+                html_writer::link('https://moodle.org/plugins/pluginversion.php?id='.$plugin->version->id,
+                    get_string('misdepinfoversion', 'core_plugin')),
+                'misdepinfoversion'
+            );
+
+            $info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')), 'misdepdownload');
+
+            if ($pluginman->is_remote_plugin_installable($plugin->component, $plugin->version->version, $reason)) {
+                $info .= $this->output->single_button(
+                    new moodle_url($this->page->url, array('installdep' => $plugin->component)),
+                    get_string('dependencyinstall', 'core_plugin'),
+                    'post',
+                    array('class' => 'singlebutton dependencyinstall')
+                );
+            } else {
+                $reasonhelp = $this->info_remote_plugin_not_installable($reason);
+                if ($reasonhelp) {
+                    $info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall');
+                }
+            }
+
+            $info .= $this->output->container_end(); // End of .actions container.
+
+            $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);
+    }
+
+    /**
+     * Explain why {@link core_plugin_manager::is_remote_plugin_installable()} returned false.
+     *
+     * @param string $reason the reason code as returned by the plugin manager
+     * @return string
+     */
+    protected function info_remote_plugin_not_installable($reason) {
+
+        if ($reason === 'notwritableplugintype' or $reason === 'notwritableplugin') {
+            return $this->output->help_icon('notwritable', 'core_plugin', get_string('notwritable', 'core_plugin'));
+        }
+
+        if ($reason === 'remoteunavailable') {
+            return $this->output->help_icon('notdownloadable', 'core_plugin', get_string('notdownloadable', 'core_plugin'));
+        }
+
+        return false;
+    }
+
     /**
      * Formats the information that needs to go in the 'Requires' column.
      * @param \core\plugininfo\base $plugin the plugin we are rendering the row for.
@@ -1031,61 +1309,107 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     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') {
+                if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
+                    $class = 'requires-ok';
+                    $label = '';
+                } else {
+                    $class = 'requires-failed';
+                    $label = html_writer::span(get_string('dependencyfails', 'core_plugin'), 'label label-important');
+                }
+                $requires[] = html_writer::tag('li',
+                    html_writer::span(get_string('moodleversion', 'core_plugin', $plugin->versionrequires), 'dep dep-core').
+                    ' '.$label, array('class' => $class));
 
-        if (!empty($plugin->versionrequires)) {
-            if ($plugin->versionrequires <= $version) {
-                $class = 'requires-ok';
             } else {
-                $class = 'requires-failed';
-            }
-            $requires[] = html_writer::tag('li',
-                get_string('moodleversion', 'core_plugin', $plugin->versionrequires),
-                array('class' => $class));
-        }
-
-        foreach ($plugin->get_other_required_plugins() as $component => $requiredversion) {
-            $otherplugin = $pluginman->get_plugin_info($component);
-            $actions = array();
-
-            if (is_null($otherplugin)) {
-                // The required plugin is not installed.
-                $class = 'requires-failed requires-missing';
-                $installurl = new moodle_url('https://moodle.org/plugins/view.php', array('plugin' => $component));
-                $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'));
-
-            } else if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
-                // The required plugin is installed but needs to be updated.
-                $class = 'requires-failed requires-outdated';
-                if (!$otherplugin->is_standard()) {
-                    $updateurl = new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1));
-                    $actions[] = html_writer::link($updateurl, get_string('checkforupdates', 'core_plugin'));
+                $actions = array();
+
+                if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
+                    $label = '';
+                    $class = 'requires-ok';
+
+                } else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_MISSING) {
+                    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) {
+                    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;
                 }
 
-            } else {
-                // Already installed plugin with sufficient version.
-                $class = 'requires-ok';
-            }
+                if ($reqinfo->reqver != ANY_VERSION) {
+                    $str = 'otherpluginversion';
+                } else {
+                    $str = 'otherplugin';
+                }
 
-            if ($requiredversion != ANY_VERSION) {
-                $str = 'otherpluginversion';
-            } else {
-                $str = 'otherplugin';
+                $requires[] = html_writer::tag('li', html_writer::span(
+                    get_string($str, 'core_plugin', array('component' => $reqname, 'version' => $reqinfo->reqver)),
+                    'dep dep-plugin').' '.$label.' '.html_writer::span(implode(' | ', $actions), 'actions'),
+                    array('class' => $class)
+                );
             }
-
-            $requires[] = html_writer::tag('li',
-                    html_writer::div(get_string($str, 'core_plugin',
-                            array('component' => $component, 'version' => $requiredversion)), 'component').
-                    html_writer::div(implode(' | ', $actions), 'actions'),
-                    array('class' => $class));
         }
 
         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;
+
     }
 
     /**
@@ -1096,74 +1420,76 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string as usually
      */
     public function plugins_overview_panel(core_plugin_manager $pluginman, array $options = array()) {
-        global $CFG;
 
         $plugininfo = $pluginman->get_plugins();
 
-        $numtotal = $numdisabled = $numextension = $numupdatable = 0;
+        $numtotal = $numextension = $numupdatable = 0;
 
         foreach ($plugininfo as $type => $plugins) {
             foreach ($plugins as $name => $plugin) {
+                if ($plugin->available_updates()) {
+                    $numupdatable++;
+                }
                 if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) {
                     continue;
                 }
                 $numtotal++;
-                if ($plugin->is_enabled() === false) {
-                    $numdisabled++;
-                }
                 if (!$plugin->is_standard()) {
                     $numextension++;
                 }
-                if (empty($CFG->disableupdatenotifications) and $plugin->available_updates()) {
-                    $numupdatable++;
-                }
             }
         }
 
-        $info = array();
-        $filter = array();
-        $somefilteractive = false;
-        $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 ($numextension > 0) {
-            if (empty($options['contribonly'])) {
-                $filter[] = html_writer::link(
-                    new moodle_url($this->page->url, array('contribonly' => 1)),
-                    get_string('filtercontribonly', 'core_plugin'),
-                    array('class' => 'filter-item show-contribonly')
-                );
-            } else {
-                $filter[] = html_writer::tag('span', get_string('filtercontribonlyactive', 'core_plugin'),
-                    array('class' => 'filter-item active show-contribonly'));
-                $somefilteractive = true;
-            }
+        $infoall = html_writer::link(
+            new moodle_url($this->page->url, array('contribonly' => 0, 'updatesonly' => 0)),
+            get_string('overviewall', 'core_plugin'),
+            array('title' => get_string('filterall', 'core_plugin'))
+        ).' '.html_writer::span($numtotal, 'badge number number-all');
+
+        $infoext = html_writer::link(
+            new moodle_url($this->page->url, array('contribonly' => 1, 'updatesonly' => 0)),
+            get_string('overviewext', 'core_plugin'),
+            array('title' => get_string('filtercontribonly', 'core_plugin'))
+        ).' '.html_writer::span($numextension, 'badge number number-additional');
+
+        if ($numupdatable) {
+            $infoupdatable = html_writer::link(
+                new moodle_url($this->page->url, array('contribonly' => 0, 'updatesonly' => 1)),
+                get_string('overviewupdatable', 'core_plugin'),
+                array('title' => get_string('filterupdatesonly', 'core_plugin'))
+            ).' '.html_writer::span($numupdatable, 'badge badge-info number number-updatable');
+        } else {
+            // No updates, or the notifications disabled.
+            $infoupdatable = '';
         }
-        if ($numupdatable > 0) {
-            $info[] = html_writer::tag('span', get_string('numupdatable', 'core_plugin', $numupdatable), array('class' => 'info updatable'));
-            if (empty($options['updatesonly'])) {
-                $filter[] = html_writer::link(
-                    new moodle_url($this->page->url, array('updatesonly' => 1)),
-                    get_string('filterupdatesonly', 'core_plugin'),
-                    array('class' => 'filter-item show-updatesonly')
+
+        $out = html_writer::start_div('', array('id' => 'plugins-overview-panel'));
+
+        if (!empty($options['updatesonly'])) {
+            $out .= $this->output->heading(get_string('overviewupdatable', 'core_plugin'), 3);
+        } else if (!empty($options['contribonly'])) {
+            $out .= $this->output->heading(get_string('overviewext', 'core_plugin'), 3);
+        }
+
+        if ($numupdatable) {
+            $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')
                 );
-            } else {
-                $filter[] = html_writer::tag('span', get_string('filterupdatesonlyactive', 'core_plugin'),
-                    array('class' => 'filter-item active show-updatesonly'));
-                $somefilteractive = true;
             }
         }
-        if ($somefilteractive) {
-            $filter[] = html_writer::link($this->page->url, get_string('filterall', 'core_plugin'), array('class' => 'filter-item show-all'));
-        }
 
-        $output  = $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '', 'plugins-overview-panel');
+        $out .= html_writer::div($infoall, 'info info-all').
+            html_writer::div($infoext, 'info info-ext').
+            html_writer::div($infoupdatable, 'info info-updatable');
 
-        if (!empty($filter)) {
-            $output .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $filter), '', 'plugins-overview-filter');
-        }
+        $out .= html_writer::end_div(); // End of #plugins-overview-panel block.
 
-        return $output;
+        return $out;
     }
 
     /**
@@ -1176,7 +1502,6 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function plugins_control_panel(core_plugin_manager $pluginman, array $options = array()) {
-        global $CFG;
 
         $plugininfo = $pluginman->get_plugins();
 
@@ -1185,11 +1510,10 @@ class core_admin_renderer extends plugin_renderer_base {
             $updateable = array();
             foreach ($plugininfo as $plugintype => $pluginnames) {
                 foreach ($pluginnames as $pluginname => $pluginfo) {
-                    if (!empty($pluginfo->availableupdates)) {
-                        foreach ($pluginfo->availableupdates as $pluginavailableupdate) {
-                            if ($pluginavailableupdate->version > $pluginfo->versiondisk) {
-                                $updateable[$plugintype][$pluginname] = $pluginfo;
-                            }
+                    $pluginavailableupdates = $pluginfo->available_updates();
+                    if (!empty($pluginavailableupdates)) {
+                        foreach ($pluginavailableupdates as $pluginavailableupdate) {
+                            $updateable[$plugintype][$pluginname] = $pluginfo;
                         }
                     }
                 }
@@ -1217,23 +1541,22 @@ class core_admin_renderer extends plugin_renderer_base {
         $table->id = 'plugins-control-panel';
         $table->head = array(
             get_string('displayname', 'core_plugin'),
-            get_string('source', 'core_plugin'),
             get_string('version', 'core_plugin'),
-            get_string('release', 'core_plugin'),
             get_string('availability', 'core_plugin'),
             get_string('actions', 'core_plugin'),
             get_string('notes','core_plugin'),
         );
-        $table->headspan = array(1, 1, 1, 1, 1, 2, 1);
+        $table->headspan = array(1, 1, 1, 2, 1);
         $table->colclasses = array(
-            'pluginname', 'source', 'version', 'release', 'availability', 'settings', 'uninstall', 'notes'
+            'pluginname', 'version', 'availability', 'settings', 'uninstall', 'notes'
         );
 
         foreach ($plugininfo as $type => $plugins) {
             $heading = $pluginman->plugintype_name_plural($type);
             $pluginclass = core_plugin_manager::resolve_plugininfo_class($type);
             if ($manageurl = $pluginclass::get_manage_url()) {
-                $heading = html_writer::link($manageurl, $heading);
+                $heading .= $this->output->action_icon($manageurl, new pix_icon('i/settings',
+                    get_string('settings', 'core_plugin')));
             }
             $header = new html_table_cell(html_writer::tag('span', $heading, array('id'=>'plugin_type_cell_'.$type)));
             $header->header = true;
@@ -1262,27 +1585,15 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
                 $status = $plugin->get_status();
                 $row->attributes['class'] .= ' status-'.$status;
-                if ($status === core_plugin_manager::PLUGIN_STATUS_MISSING) {
-                    $msg = html_writer::tag('span', get_string('status_missing', 'core_plugin'), array('class' => 'statusmsg'));
-                } else if ($status === core_plugin_manager::PLUGIN_STATUS_NEW) {
-                    $msg = html_writer::tag('span', get_string('status_new', 'core_plugin'), array('class' => 'statusmsg'));
-                } else {
-                    $msg = '';
-                }
-                $pluginname  = html_writer::tag('div', $icon . '' . $plugin->displayname . ' ' . $msg, array('class' => 'displayname')).
+                $pluginname  = html_writer::tag('div', $icon.$plugin->displayname, 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';
-                    $source = new html_table_cell(get_string('sourcestd', 'core_plugin'));
-                } else {
-                    $row->attributes['class'] .= ' extension';
-                    $source = new html_table_cell(get_string('sourceext', 'core_plugin'));
+                $version = html_writer::div($plugin->versiondb, 'versionnumber');
+                if ((string)$plugin->release !== '') {
+                    $version = html_writer::div($plugin->release, 'release').$version;
                 }
-
-                $version = new html_table_cell($plugin->versiondb);
-                $release = new html_table_cell($plugin->release);
+                $version = new html_table_cell($version);
 
                 $isenabled = $plugin->is_enabled();
                 if (is_null($isenabled)) {
@@ -1310,6 +1621,22 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
                 $uninstall = new html_table_cell($uninstall);
 
+                if ($plugin->is_standard()) {
+                    $row->attributes['class'] .= ' standard';
+                    $source = '';
+                } else {
+                    $row->attributes['class'] .= ' extension';
+                    $source = html_writer::div(get_string('sourceext', 'core_plugin'), 'source label label-info');
+                }
+
+                if ($status === core_plugin_manager::PLUGIN_STATUS_MISSING) {
+                    $msg = html_writer::div(get_string('status_missing', 'core_plugin'), 'statusmsg label label-important');
+                } else if ($status === core_plugin_manager::PLUGIN_STATUS_NEW) {
+                    $msg = html_writer::div(get_string('status_new', 'core_plugin'), 'statusmsg label label-success');
+                } else {
+                    $msg = '';
+                }
+
                 $requriedby = $pluginman->other_plugins_that_require($plugin->component);
                 if ($requriedby) {
                     $requiredby = html_writer::tag('div', get_string('requiredby', 'core_plugin', implode(', ', $requriedby)),
@@ -1319,16 +1646,16 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
 
                 $updateinfo = '';
-                if (empty($CFG->disableupdatenotifications) and is_array($plugin->available_updates())) {
+                if (is_array($plugin->available_updates())) {
                     foreach ($plugin->available_updates() as $availableupdate) {
-                        $updateinfo .= $this->plugin_available_update_info($availableupdate);
+                        $updateinfo .= $this->plugin_available_update_info($pluginman, $availableupdate);
                     }
                 }
 
-                $notes = new html_table_cell($requiredby.$updateinfo);
+                $notes = new html_table_cell($source.$msg.$requiredby.$updateinfo);
 
                 $row->cells = array(
-                    $pluginname, $source, $version, $release, $availability, $settings, $uninstall, $notes
+                    $pluginname, $version, $availability, $settings, $uninstall, $notes
                 );
                 $table->data[] = $row;
             }
@@ -1340,57 +1667,68 @@ class core_admin_renderer extends plugin_renderer_base {
     /**
      * 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.
-     *
+     * @param core_plugin_manager $pluginman plugin manager instance
      * @param \core\update\info $updateinfo information about the available update for the plugin
      */
-    protected function plugin_available_update_info(\core\update\info $updateinfo) {
+    protected function plugin_available_update_info(core_plugin_manager $pluginman, \core\update\info $updateinfo) {
 
         $boxclasses = 'pluginupdateinfo';
         $info = array();
 
         if (isset($updateinfo->release)) {
-            $info[] = html_writer::tag('span', get_string('updateavailable_release', 'core_plugin', $updateinfo->release),
-                array('class' => 'info release'));
+            $info[] = html_writer::div(
+                get_string('updateavailable_release', 'core_plugin', $updateinfo->release),
+                'info release'
+            );
         }
 
         if (isset($updateinfo->maturity)) {
-            $info[] = html_writer::tag('span', get_string('maturity'.$updateinfo->maturity, 'core_admin'),
-                array('class' => 'info maturity'));
+            $info[] = html_writer::div(
+                get_string('maturity'.$updateinfo->maturity, 'core_admin'),
+                'info maturity'
+            );
             $boxclasses .= ' maturity'.$updateinfo->maturity;
         }
 
         if (isset($updateinfo->download)) {
-            $info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download'));
+            $info[] = html_writer::div(
+                html_writer::link($updateinfo->download, get_string('download')),
+                'info download'
+            );
         }
 
         if (isset($updateinfo->url)) {
-            $info[] = html_writer::link($updateinfo->url, get_string('updateavailable_moreinfo', 'core_plugin'),
-                array('class' => 'info more'));
+            $info[] = html_writer::div(
+                html_writer::link($updateinfo->url, get_string('updateavailable_moreinfo', 'core_plugin')),
+                'info more'
+            );
         }
 
-        $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 = html_writer::start_div($boxclasses);
+        $box .= html_writer::div(
+            get_string('updateavailable', 'core_plugin', $updateinfo->version),
+            'version'
+        );
+        $box .= html_writer::div(
+            implode(html_writer::span(' ', 'separator'), $info),
+            'infos'
+        );
 
-        $deployer = \core\update\deployer::instance();
-        if ($deployer->initialized()) {
-            $impediments = $deployer->deployment_impediments($updateinfo);
-            if (empty($impediments)) {
-                $widget = $deployer->make_confirm_widget($updateinfo);
-                $box .= $this->output->render($widget);
-            } else {
-                if (isset($impediments['notwritable'])) {
-                    $box .= $this->output->help_icon('notwritable', 'core_plugin', get_string('notwritable', 'core_plugin'));
-                }
-                if (isset($impediments['notdownloadable'])) {
-                    $box .= $this->output->help_icon('notdownloadable', 'core_plugin', get_string('notdownloadable', 'core_plugin'));
-                }
+        if ($pluginman->is_remote_plugin_installable($updateinfo->component, $updateinfo->version, $reason)) {
+            $box .= $this->output->single_button(
+                new moodle_url($this->page->url, array('installupdate' => $updateinfo->component,
+                    'installupdateversion' => $updateinfo->version)),
+                get_string('updateavailableinstall', 'core_admin'),
+                'post',
+                array('class' => 'singlebutton updateavailableinstall')
+            );
+        } else {
+            $reasonhelp = $this->info_remote_plugin_not_installable($reason);
+            if ($reasonhelp) {
+                $box .= html_writer::div($reasonhelp, 'reasonhelp updateavailableinstall');
             }
         }
-
-        $box .= $this->output->box_end();
+        $box .= html_writer::end_div();
 
         return $box;
     }
index 4b3b11e..cc43e70 100644 (file)
@@ -215,10 +215,6 @@ if (empty($CFG->disableupdatenotifications)) {
     $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));
-    if (empty($CFG->disableupdateautodeploy)) {
-        $temp->add(new admin_setting_configcheckbox('updateautodeploy', new lang_string('updateautodeploy', 'core_admin'),
-                                                    new lang_string('updateautodeploy_desc', 'core_admin'), 0));
-    }
     $temp->add(new admin_setting_configselect('updateminmaturity', new lang_string('updateminmaturity', 'core_admin'),
                                               new lang_string('updateminmaturity_desc', 'core_admin'), MATURITY_STABLE,
                                               array(
index d9807f1..07e1fe4 100644 (file)
@@ -16,7 +16,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Provides tool_installaddon_installer related classes
+ * Provides tool_installaddon_installer class.
  *
  * @package     tool_installaddon
  * @subpackage  classes
@@ -103,69 +103,16 @@ class tool_installaddon_installer {
     }
 
     /**
-     * Saves the ZIP file from the {@link tool_installaddon_installfromzip_form} form
+     * Makes a unique writable storage for uploaded ZIP packages.
      *
-     * The file is saved into the given temporary location for inspection and eventual
-     * deployment. The form is expected to be submitted and validated.
+     * We need the saved ZIP to survive across multiple requests so that it can
+     * be used by the plugin manager after the installation is confirmed. In
+     * other words, we cannot use make_request_directory() here.
      *
-     * @param tool_installaddon_installfromzip_form $form
-     * @param string $targetdir full path to the directory where the ZIP should be stored to
-     * @return string filename of the saved file relative to the given target
+     * @return string full path to the directory
      */
-    public function save_installfromzip_file(tool_installaddon_installfromzip_form $form, $targetdir) {
-
-        $filename = clean_param($form->get_new_filename('zipfile'), PARAM_FILE);
-        $form->save_file('zipfile', $targetdir.'/'.$filename);
-
-        return $filename;
-    }
-
-    /**
-     * Extracts the saved file previously saved by {self::save_installfromzip_file()}
-     *
-     * The list of files found in the ZIP is returned via $zipcontentfiles parameter
-     * by reference. The format of that list is array of (string)filerelpath => (bool|string)
-     * where the array value is either true or a string describing the problematic file.
-     *
-     * @see zip_packer::extract_to_pathname()
-     * @param string $zipfilepath full path to the saved ZIP file
-     * @param string $targetdir full path to the directory to extract the ZIP file to
-     * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
-     * @param array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
-     */
-    public function extract_installfromzip_file($zipfilepath, $targetdir, $rootdir = '') {
-        global $CFG;
-        require_once($CFG->libdir.'/filelib.php');
-
-        $fp = get_file_packer('application/zip');
-        $files = $fp->extract_to_pathname($zipfilepath, $targetdir);
-
-        if (!$files) {
-            return array();
-        }
-
-        if (!empty($rootdir)) {
-            $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
-        }
-
-        // Sometimes zip may not contain all parent directories, add them to make it consistent.
-        foreach ($files as $path => $status) {
-            if ($status !== true) {
-                continue;
-            }
-            $parts = explode('/', trim($path, '/'));
-            while (array_pop($parts)) {
-                if (empty($parts)) {
-                    break;
-                }
-                $dir = implode('/', $parts).'/';
-                if (!isset($files[$dir])) {
-                    $files[$dir] = true;
-                }
-            }
-        }
-
-        return $files;
+    public function make_installfromzip_storage() {
+        return make_unique_writable_directory(make_temp_directory('tool_installaddon'));
     }
 
     /**
@@ -186,57 +133,6 @@ class tool_installaddon_installer {
         return $menu;
     }
 
-    /**
-     * Returns the full path of the root of the given plugin type
-     *
-     * Null is returned if the plugin type is not known. False is returned if the plugin type
-     * root is expected but not found. Otherwise, string is returned.
-     *
-     * @param string $plugintype
-     * @return string|bool|null
-     */
-    public function get_plugintype_root($plugintype) {
-
-        $plugintypepath = null;
-        foreach (core_component::get_plugin_types() as $type => $fullpath) {
-            if ($type === $plugintype) {
-                $plugintypepath = $fullpath;
-                break;
-            }
-        }
-        if (is_null($plugintypepath)) {
-            return null;
-        }
-
-        if (!is_dir($plugintypepath)) {
-            return false;
-        }
-
-        return $plugintypepath;
-    }
-
-    /**
-     * Is it possible to create a new plugin directory for the given plugin type?
-     *
-     * @throws coding_exception for invalid plugin types or non-existing plugin type locations
-     * @param string $plugintype
-     * @return boolean
-     */
-    public function is_plugintype_writable($plugintype) {
-
-        $plugintypepath = $this->get_plugintype_root($plugintype);
-
-        if (is_null($plugintypepath)) {
-            throw new coding_exception('Unknown plugin type!');
-        }
-
-        if ($plugintypepath === false) {
-            throw new coding_exception('Plugin type location does not exist!');
-        }
-
-        return is_writable($plugintypepath);
-    }
-
     /**
      * Hook method to handle the remote request to install an add-on
      *
@@ -245,15 +141,12 @@ class tool_installaddon_installer {
      * it.
      *
      * This hook is called early from admin/tool/installaddon/index.php page so that
-     * it has opportunity to take over the UI.
+     * it has opportunity to take over the UI and display the first confirmation screen.
      *
      * @param tool_installaddon_renderer $output
      * @param string|null $request
-     * @param bool $confirmed
      */
-    public function handle_remote_request(tool_installaddon_renderer $output, $request, $confirmed = false) {
-        global $CFG;
-        require_once(dirname(__FILE__).'/pluginfo_client.php');
+    public function handle_remote_request(tool_installaddon_renderer $output, $request) {
 
         if (is_null($request)) {
             return;
@@ -267,196 +160,34 @@ class tool_installaddon_installer {
         }
 
         list($plugintype, $pluginname) = core_component::normalize_component($data->component);
+        $pluginman = core_plugin_manager::instance();
 
-        $plugintypepath = $this->get_plugintype_root($plugintype);
+        $plugintypepath = $pluginman->get_plugintype_root($plugintype);
 
         if (file_exists($plugintypepath.'/'.$pluginname)) {
             echo $output->remote_request_alreadyinstalled_page($data, $this->index_url());
             exit();
         }
 
-        if (!$this->is_plugintype_writable($plugintype)) {
+        if (!$pluginman->is_plugintype_writable($plugintype)) {
             $continueurl = $this->index_url(array('installaddonrequest' => $request));
             echo $output->remote_request_permcheck_page($data, $plugintypepath, $continueurl, $this->index_url());
             exit();
         }
 
-        $continueurl = $this->index_url(array(
-            'installaddonrequest' => $request,
-            'confirm' => 1,
-            'sesskey' => sesskey()));
-
-        if (!$confirmed) {
-            echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
+        if (!$pluginman->is_remote_plugin_installable($data->component, $data->version, $reason)) {
+            $data->reason = $reason;
+            echo $output->remote_request_non_installable_page($data, $this->index_url());
             exit();
         }
 
-        // The admin has confirmed their intention to install the add-on.
-        require_sesskey();
-
-        // Fetch the plugin info. The essential information is the URL to download the ZIP
-        // and the MD5 hash of the ZIP, obtained via HTTPS.
-        $client = tool_installaddon_pluginfo_client::instance();
-
-        try {
-            $pluginfo = $client->get_pluginfo($data->component, $data->version);
-
-        } catch (tool_installaddon_pluginfo_exception $e) {
-            if (debugging()) {
-                throw $e;
-            } else {
-                echo $output->remote_request_pluginfo_exception($data, $e, $this->index_url());
-                exit();
-            }
-        }
-
-        // Fetch the ZIP with the plugin version
-        $jobid = md5(rand().uniqid('', true));
-        $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
-        $zipfilename = 'downloaded.zip';
-
-        try {
-            $this->download_file($pluginfo->downloadurl, $sourcedir.'/'.$zipfilename);
-
-        } catch (tool_installaddon_installer_exception $e) {
-            if (debugging()) {
-                throw $e;
-            } else {
-                echo $output->installer_exception($e, $this->index_url());
-                exit();
-            }
-        }
-
-        // Check the MD5 checksum
-        $md5expected = $pluginfo->downloadmd5;
-        $md5actual = md5_file($sourcedir.'/'.$zipfilename);
-        if ($md5expected !== $md5actual) {
-            $e = new tool_installaddon_installer_exception('err_zip_md5', array('expected' => $md5expected, 'actual' => $md5actual));
-            if (debugging()) {
-                throw $e;
-            } else {
-                echo $output->installer_exception($e, $this->index_url());
-                exit();
-            }
-        }
-
-        // Redirect to the validation page.
-        $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array(
-            'sesskey' => sesskey(),
-            'jobid' => $jobid,
-            'zip' => $zipfilename,
-            'type' => $plugintype));
-        redirect($nexturl);
-    }
-
-    /**
-     * Download the given file into the given destination.
-     *
-     * This is basically a simplified version of {@link download_file_content()} from
-     * Moodle itself, tuned for fetching files from moodle.org servers. Same code is used
-     * in mdeploy.php for fetching available updates.
-     *
-     * @param string $source file url starting with http(s)://
-     * @param string $target store the downloaded content to this file (full path)
-     * @throws tool_installaddon_installer_exception
-     */
-    public function download_file($source, $target) {
-        global $CFG;
-        require_once($CFG->libdir.'/filelib.php');
-
-        $targetfile = fopen($target, 'w');
-
-        if (!$targetfile) {
-            throw new tool_installaddon_installer_exception('err_download_write_file', $target);
-        }
-
-        $options = array(
-            'file' => $targetfile,
-            'timeout' => 300,
-            'followlocation' => true,
-            'maxredirs' => 3,
-            'ssl_verifypeer' => true,
-            'ssl_verifyhost' => 2,
-        );
-
-        $curl = new curl(array('proxy' => true));
-
-        $result = $curl->download_one($source, null, $options);
-
-        $curlinfo = $curl->get_info();
-
-        fclose($targetfile);
-
-        if ($result !== true) {
-            throw new tool_installaddon_installer_exception('err_curl_exec', array(
-                'url' => $source, 'errorno' => $curl->get_errno(), 'error' => $result));
-
-        } else if (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200) {
-            throw new tool_installaddon_installer_exception('err_curl_http_code', array(
-                'url' => $source, 'http_code' => $curlinfo['http_code']));
-
-        } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) {
-            throw new tool_installaddon_installer_exception('err_curl_ssl_verify', array(
-                'url' => $source, 'ssl_verify_result' => $curlinfo['ssl_verify_result']));
-        }
-    }
-
-    /**
-     * Moves the given source into a new location recursively
-     *
-     * This is cross-device safe implementation to be used instead of the native rename() function.
-     * See https://bugs.php.net/bug.php?id=54097 for more details.
-     *
-     * @param string $source full path to the existing directory
-     * @param string $target full path to the new location of the directory
-     * @param int $dirpermissions
-     * @param int $filepermissions
-     */
-    public function move_directory($source, $target, $dirpermissions, $filepermissions) {
-
-        if (file_exists($target)) {
-            throw new tool_installaddon_installer_exception('err_folder_already_exists', array('path' => $target));
-        }
-
-        if (is_dir($source)) {
-            $handle = opendir($source);
-        } else {
-            throw new tool_installaddon_installer_exception('err_no_such_folder', array('path' => $source));
-        }
-
-        if (!file_exists($target)) {
-            // Do not use make_writable_directory() here - it is intended for dataroot only.
-            mkdir($target, true);
-            @chmod($target, $dirpermissions);
-        }
-
-        if (!is_writable($target)) {
-            closedir($handle);
-            throw new tool_installaddon_installer_exception('err_folder_not_writable', array('path' => $target));
-        }
-
-        while ($filename = readdir($handle)) {
-            $sourcepath = $source.'/'.$filename;
-            $targetpath = $target.'/'.$filename;
-
-            if ($filename === '.' or $filename === '..') {
-                continue;
-            }
-
-            if (is_dir($sourcepath)) {
-                $this->move_directory($sourcepath, $targetpath, $dirpermissions, $filepermissions);
-
-            } else {
-                rename($sourcepath, $targetpath);
-                @chmod($targetpath, $filepermissions);
-            }
-        }
-
-        closedir($handle);
-
-        rmdir($source);
+        $continueurl = $this->index_url(array(
+            'installremote' => $data->component,
+            'installremoteversion' => $data->version
+        ));
 
-        clearstatcache();
+        echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
+        exit();
     }
 
     /**
@@ -466,11 +197,11 @@ class tool_installaddon_installer {
      * are supported.
      *
      * @param string $zipfilepath full path to the saved ZIP file
-     * @param string $workdir full path to the directory we can use for extracting required bits from the archive
      * @return string|bool declared component name or false if unable to detect
      */
-    public function detect_plugin_component($zipfilepath, $workdir) {
+    public function detect_plugin_component($zipfilepath) {
 
+        $workdir = make_request_directory();
         $versionphp = $this->extract_versionphp_file($zipfilepath, $workdir);
 
         if (empty($versionphp)) {
@@ -539,58 +270,6 @@ class tool_installaddon_installer {
         return true;
     }
 
-    /**
-     * Renames the root directory of the extracted ZIP package.
-     *
-     * This method does not validate the presence of the single root directory
-     * (the validator does it later). It just searches for the first directory
-     * under the given location and renames it.
-     *
-     * The method will not rename the root if the requested location already
-     * exists.
-     *
-     * @param string $dirname the location of the extracted ZIP package
-     * @param string $rootdir the requested name of the root directory
-     * @param array $files list of extracted files
-     * @return array eventually amended list of extracted files
-     */
-    protected function rename_extracted_rootdir($dirname, $rootdir, array $files) {
-
-        if (!is_dir($dirname)) {
-            debugging('Unable to rename rootdir of non-existing content', DEBUG_DEVELOPER);
-            return $files;
-        }
-
-        if (file_exists($dirname.'/'.$rootdir)) {
-            debugging('Unable to rename rootdir to already existing folder', DEBUG_DEVELOPER);
-            return $files;
-        }
-
-        $found = null; // The name of the first subdirectory under the $dirname.
-        foreach (scandir($dirname) as $item) {
-            if (substr($item, 0, 1) === '.') {
-                continue;
-            }
-            if (is_dir($dirname.'/'.$item)) {
-                $found = $item;
-                break;
-            }
-        }
-
-        if (!is_null($found)) {
-            if (rename($dirname.'/'.$found, $dirname.'/'.$rootdir)) {
-                $newfiles = array();
-                foreach ($files as $filepath => $status) {
-                    $newpath = preg_replace('~^'.preg_quote($found.'/').'~', preg_quote($rootdir.'/'), $filepath);
-                    $newfiles[$newpath] = $status;
-                }
-                return $newfiles;
-            }
-        }
-
-        return $files;
-    }
-
     /**
      * Decode the request from the Moodle Plugins directory
      *
@@ -728,21 +407,3 @@ class tool_installaddon_installer {
         return false;
     }
 }
-
-
-/**
- * General exception thrown by {@link tool_installaddon_installer} class
- *
- * @copyright 2013 David Mudrak <david@moodle.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class tool_installaddon_installer_exception extends moodle_exception {
-
-    /**
-     * @param string $errorcode exception description identifier
-     * @param mixed $debuginfo debugging data to display
-     */
-    public function __construct($errorcode, $a=null, $debuginfo=null) {
-        parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true));
-    }
-}
index fc7c840..7aca466 100644 (file)
@@ -86,6 +86,21 @@ class tool_installaddon_installfromzip_form extends moodleform {
         $mform->insertElementBefore($typedetectionfailed, 'permcheck');
     }
 
+    /**
+     * Warn that the selected plugin type does not match the detected one.
+     *
+     * @param string $detected detected plugin type
+     */
+    public function selected_plugintype_mismatch($detected) {
+
+        $mform = $this->_form;
+        $mform->addRule('plugintype', get_string('required'), 'required', null, 'client');
+        $mform->setAdvanced('plugintype', false);
+        $mform->setAdvanced('permcheck', false);
+        $mform->insertElementBefore($mform->createElement('static', 'selectedplugintypemismatch', '',
+            html_writer::span(get_string('typedetectionmismatch', 'tool_installaddon', $detected), 'error')), 'permcheck');
+    }
+
     /**
      * Validate the form fields
      *
@@ -95,12 +110,12 @@ class tool_installaddon_installfromzip_form extends moodleform {
      */
     public function validation($data, $files) {
 
-        $installer = $this->_customdata['installer'];
+        $pluginman = core_plugin_manager::instance();
         $errors = parent::validation($data, $files);
 
         if (!empty($data['plugintype'])) {
-            if (!$installer->is_plugintype_writable($data['plugintype'])) {
-                $path = $installer->get_plugintype_root($data['plugintype']);
+            if (!$pluginman->is_plugintype_writable($data['plugintype'])) {
+                $path = $pluginman->get_plugintype_root($data['plugintype']);
                 $errors['plugintype'] = get_string('permcheckresultno', 'tool_installaddon', array('path' => $path));
             }
         }
diff --git a/admin/tool/installaddon/classes/pluginfo_client.php b/admin/tool/installaddon/classes/pluginfo_client.php
deleted file mode 100644 (file)
index b6693ba..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-<?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 tool_installaddon_pluginfo_client and related classes
- *
- * @package     tool_installaddon
- * @subpackage  classes
- * @copyright   2013 David Mudrak <david@moodle.com>
- * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Implements a client for https://download.moodle.org/api/x.y/pluginfo.php service
- *
- * @copyright 2013 David Mudrak <david@moodle.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class tool_installaddon_pluginfo_client {
-
-    /**
-     * Factory method returning an instance of this class.
-     *
-     * @return tool_installaddon_pluginfo_client
-     */
-    public static function instance() {
-        return new static();
-    }
-
-    /**
-     * Return the information about the plugin
-     *
-     * @throws tool_installaddon_pluginfo_exception
-     * @param string $component
-     * @param string $version
-     * @return stdClass the pluginfo structure
-     */
-    public function get_pluginfo($component, $version) {
-
-        $response = $this->call_service($component, $version);
-        $response = $this->decode_response($response);
-        $this->validate_response($response);
-
-        return $response->pluginfo;
-    }
-
-    // End of external API /////////////////////////////////////////////////
-
-    /**
-     * @see self::instance()
-     */
-    protected function __construct() {
-    }
-
-    /**
-     * Calls the pluginfo.php service and returns the raw response
-     *
-     * @param string $component
-     * @param string $version
-     * @return string
-     */
-    protected function call_service($component, $version) {
-        global $CFG;
-        require_once($CFG->libdir.'/filelib.php');
-
-        $curl = new curl(array('proxy' => true));
-
-        $response = $curl->get(
-            $this->service_request_url(),
-            $this->service_request_params($component, $version),
-            $this->service_request_options());
-
-        $curlerrno = $curl->get_errno();
-        $curlinfo = $curl->get_info();
-
-        if (!empty($curlerrno)) {
-            throw new tool_installaddon_pluginfo_exception('err_curl_exec', array(
-                'url' => $curlinfo['url'], 'errno' => $curlerrno, 'error' => $curl->error));
-
-        } else if ($curlinfo['http_code'] != 200) {
-            throw new tool_installaddon_pluginfo_exception('err_curl_http_code', array(
-                'url' => $curlinfo['url'], 'http_code' => $curlinfo['http_code']));
-
-        } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) {
-            throw new tool_installaddon_pluginfo_exception('err_curl_ssl_verify', array(
-                'url' => $curlinfo['url'], 'ssl_verify_result' => $curlinfo['ssl_verify_result']));
-        }
-
-        return $response;
-    }
-
-    /**
-     * Return URL to the pluginfo.php service
-     *
-     * @return moodle_url
-     */
-    protected function service_request_url() {
-        global $CFG;
-
-        if (!empty($CFG->config_php_settings['alternativepluginfoserviceurl'])) {
-            $url = $CFG->config_php_settings['alternativepluginfoserviceurl'];
-        } else {
-            $url = 'https://download.moodle.org/api/1.2/pluginfo.php';
-        }
-
-        return new moodle_url($url);
-    }
-
-    /**
-     * Return list of pluginfo service parameters
-     *
-     * @param string $component
-     * @param string $version
-     * @return array
-     */
-    protected function service_request_params($component, $version) {
-
-        $params = array();
-        $params['format'] = 'json';
-        $params['plugin'] = $component.'@'.$version;
-
-        return $params;
-    }
-
-    /**
-     * Return cURL options for the service request
-     *
-     * @return array of (string)param => (string)value
-     */
-    protected function service_request_options() {
-        global $CFG;
-
-        $options = array(
-            'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
-            'CURLOPT_SSL_VERIFYPEER' => true,
-        );
-
-        return $options;
-    }
-
-    /**
-     * Decode the raw service response
-     *
-     * @param string $raw
-     * @return stdClass
-     */
-    protected function decode_response($raw) {
-        return json_decode($raw);
-    }
-
-    /**
-     * Validate decoded service response
-     *
-     * @param stdClass $response
-     */
-    protected function validate_response($response) {
-
-        if (empty($response)) {
-            throw new tool_installaddon_pluginfo_exception('err_response_empty');
-        }
-
-        if (empty($response->status) or $response->status !== 'OK') {
-            throw new tool_installaddon_pluginfo_exception('err_response_status', $response->status);
-        }
-
-        if (empty($response->apiver) or $response->apiver !== '1.2') {
-            throw new tool_installaddon_pluginfo_exception('err_response_api_version', $response->apiver);
-        }
-
-        if (empty($response->pluginfo->component) or empty($response->pluginfo->downloadurl)
-                or empty($response->pluginfo->downloadmd5)) {
-            throw new tool_installaddon_pluginfo_exception('err_response_pluginfo');
-        }
-    }
-}
-
-
-/**
- * General exception thrown by {@link tool_installaddon_pluginfo_client} class
- *
- * @copyright 2013 David Mudrak <david@moodle.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class tool_installaddon_pluginfo_exception extends moodle_exception {
-
-    /**
-     * @param string $errorcode exception description identifier
-     * @param mixed $debuginfo debugging data to display
-     */
-    public function __construct($errorcode, $a=null, $debuginfo=null) {
-        parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true));
-    }
-}
diff --git a/admin/tool/installaddon/deploy.php b/admin/tool/installaddon/deploy.php
deleted file mode 100644 (file)
index b80af83..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?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/>.
-
-/**
- * Deploy the validated contents of the ZIP package to the $CFG->dirroot
- *
- * @package     tool_installaddon
- * @copyright   2013 David Mudrak <david@moodle.com>
- * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-require(dirname(__FILE__) . '/../../../config.php');
-require_once($CFG->libdir.'/filelib.php');
-
-require_login();
-require_capability('moodle/site:config', context_system::instance());
-
-if (!empty($CFG->disableonclickaddoninstall)) {
-    notice(get_string('featuredisabled', 'tool_installaddon'));
-}
-
-require_sesskey();
-
-$jobid = required_param('jobid', PARAM_ALPHANUM);
-$plugintype = required_param('type', PARAM_ALPHANUMEXT);
-$pluginname = required_param('name', PARAM_PLUGIN);
-
-$zipcontentpath = $CFG->tempdir.'/tool_installaddon/'.$jobid.'/contents';
-
-if (!is_dir($zipcontentpath)) {
-    debugging('Invalid location of the extracted ZIP package: '.s($zipcontentpath), DEBUG_DEVELOPER);
-    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
-        get_string('invaliddata', 'core_error'));
-}
-
-if (!is_dir($zipcontentpath.'/'.$pluginname)) {
-    debugging('Invalid location of the plugin root directory: '.$zipcontentpath.'/'.$pluginname, DEBUG_DEVELOPER);
-    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
-        get_string('invaliddata', 'core_error'));
-}
-
-$installer = tool_installaddon_installer::instance();
-
-if (!$installer->is_plugintype_writable($plugintype)) {
-    debugging('Plugin type location not writable', DEBUG_DEVELOPER);
-    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
-        get_string('invaliddata', 'core_error'));
-}
-
-$plugintypepath = $installer->get_plugintype_root($plugintype);
-
-if (file_exists($plugintypepath.'/'.$pluginname)) {
-    debugging('Target location already exists', DEBUG_DEVELOPER);
-    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
-        get_string('invaliddata', 'core_error'));
-}
-
-// Copy permissions form the plugin type directory.
-$dirpermissions = fileperms($plugintypepath);
-$filepermissions = ($dirpermissions & 0666); // Strip execute flags.
-
-$installer->move_directory($zipcontentpath.'/'.$pluginname, $plugintypepath.'/'.$pluginname, $dirpermissions, $filepermissions);
-fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid);
-redirect(new moodle_url('/admin'));
index e493296..cd6e7f4 100644 (file)
@@ -28,10 +28,11 @@ require_once($CFG->libdir.'/adminlib.php');
 
 admin_externalpage_setup('tool_installaddon_index');
 
-if (!empty($CFG->disableonclickaddoninstall)) {
+if (!empty($CFG->disableupdateautodeploy)) {
     notice(get_string('featuredisabled', 'tool_installaddon'));
 }
 
+$pluginman = core_plugin_manager::instance();
 $installer = tool_installaddon_installer::instance();
 
 $output = $PAGE->get_renderer('tool_installaddon');
@@ -39,8 +40,55 @@ $output->set_installer_instance($installer);
 
 // Handle the eventual request for installing from remote repository.
 $remoterequest = optional_param('installaddonrequest', null, PARAM_RAW);
-$confirmed = optional_param('confirm', false, PARAM_BOOL);
-$installer->handle_remote_request($output, $remoterequest, $confirmed);
+$installer->handle_remote_request($output, $remoterequest);
+
+// Handle the confirmed installation request.
+$installremote = optional_param('installremote', null, PARAM_COMPONENT);
+$installremoteversion = optional_param('installremoteversion', null, PARAM_INT);
+$installremoteconfirm = optional_param('installremoteconfirm', false, PARAM_BOOL);
+
+if ($installremote and $installremoteversion) {
+    require_sesskey();
+    require_once($CFG->libdir.'/upgradelib.php');
+
+    $PAGE->set_pagelayout('maintenance');
+    $PAGE->set_popup_notification_allowed(false);
+
+    if ($pluginman->is_remote_plugin_installable($installremote, $installremoteversion)) {
+        $installable = array($pluginman->get_remote_plugin_info($installremote, $installremoteversion, true));
+        upgrade_install_plugins($installable, $installremoteconfirm,
+            get_string('installfromrepo', 'tool_installaddon'),
+            new moodle_url($PAGE->url, array('installremote' => $installremote,
+                'installremoteversion' => $installremoteversion, 'installremoteconfirm' => 1)
+            )
+        );
+    }
+    // We should never get here.
+    throw new moodle_exception('installing_non_installable_component', 'tool_installaddon');
+}
+
+// Handle installation of a plugin from the ZIP file.
+$installzipcomponent = optional_param('installzipcomponent', null, PARAM_COMPONENT);
+$installzipstorage = optional_param('installzipstorage', null, PARAM_FILE);
+$installzipconfirm = optional_param('installzipconfirm', false, PARAM_BOOL);
+
+if ($installzipcomponent and $installzipstorage) {
+    require_sesskey();
+    require_once($CFG->libdir.'/upgradelib.php');
+
+    $PAGE->set_pagelayout('maintenance');
+    $PAGE->set_popup_notification_allowed(false);
+
+    $installable = array((object)array(
+        'component' => $installzipcomponent,
+        'zipfilepath' => make_temp_directory('tool_installaddon').'/'.$installzipstorage.'/plugin.zip',
+    ));
+    upgrade_install_plugins($installable, $installzipconfirm, get_string('installfromzip', 'tool_installaddon'),
+        new moodle_url($installer->index_url(), array('installzipcomponent' => $installzipcomponent,
+            'installzipstorage' => $installzipstorage, 'installzipconfirm' => 1)
+        )
+    );
+}
 
 $form = $installer->get_installfromzip_form();
 
@@ -48,35 +96,47 @@ if ($form->is_cancelled()) {
     redirect($PAGE->url);
 
 } else if ($data = $form->get_data()) {
-    // Save the ZIP file into a temporary location.
-    $jobid = md5(rand().uniqid('', true));
-    $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
-    $zipfilename = $installer->save_installfromzip_file($form, $sourcedir);
-    if (empty($data->plugintype)) {
-        $versiondir = make_temp_directory('tool_installaddon/'.$jobid.'/version');
-        $detected = $installer->detect_plugin_component($sourcedir.'/'.$zipfilename, $versiondir);
-        if (empty($detected)) {
+    $storage = $installer->make_installfromzip_storage();
+    $form->save_file('zipfile', $storage.'/plugin.zip');
+
+    $ziprootdir = $pluginman->get_plugin_zip_root_dir($storage.'/plugin.zip');
+    if (empty($ziprootdir)) {
+        echo $output->zip_not_valid_plugin_package_page($installer->index_url());
+        die();
+    }
+
+    $component = $installer->detect_plugin_component($storage.'/plugin.zip');
+    if (!empty($component) and !empty($data->plugintype)) {
+        // If the plugin type was explicitly set, make sure it matches the detected one.
+        list($detectedtype, $detectedname) = core_component::normalize_component($component);
+        if ($detectedtype !== $data->plugintype) {
+            $form->selected_plugintype_mismatch($detectedtype);
+            echo $output->index_page();
+            die();
+        }
+    }
+    if (empty($component)) {
+        // This should not happen as all plugins are supposed to declare their
+        // component. Still, let admins upload legacy packages if they want/need.
+        if (empty($data->plugintype)) {
             $form->require_explicit_plugintype();
+            echo $output->index_page();
+            die();
+        }
+        if (!empty($data->rootdir)) {
+            $usepluginname = $data->rootdir;
         } else {
-            list($detectedtype, $detectedname) = core_component::normalize_component($detected);
-            if ($detectedtype and $detectedname and $detectedtype !== 'core') {
-                $data->plugintype = $detectedtype;
-            } else {
-                $form->require_explicit_plugintype();
-            }
+            $usepluginname = $ziprootdir;
         }
+        $component = $data->plugintype.'_'.$usepluginname;
     }
-    // Redirect to the validation page.
-    if (!empty($data->plugintype)) {
-        $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array(
-            'sesskey' => sesskey(),
-            'jobid' => $jobid,
-            'zip' => $zipfilename,
-            'type' => $data->plugintype,
-            'rootdir' => $data->rootdir));
-        redirect($nexturl);
-    }
+
+    redirect($installer->index_url(array(
+        'installzipcomponent' => $component,
+        'installzipstorage' => basename($storage),
+        'sesskey' => sesskey(),
+    )));
 }
 
-// Output starts here.
+// Display the tool main page.
 echo $output->index_page();
index 4f869ca..da1aee9 100644 (file)
@@ -31,13 +31,13 @@ $string['acknowledgementtext'] = 'I understand that it is my responsibility to h
 $string['featuredisabled'] = 'The plugin installer is disabled on this site.';
 $string['installaddon'] = 'Install plugin!';
 $string['installaddons'] = 'Install plugins';
-$string['installexception'] = 'Oops... An error occurred while trying to install the plugin. Turn debugging mode on to see details of the error.';
 $string['installfromrepo'] = 'Install plugins from the Moodle plugins directory';
 $string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install a plugin. Note that your site full name, URL and Moodle version will be sent as well, to make the installation process easier for you.';
 $string['installfromzip'] = 'Install plugin from ZIP file';
 $string['installfromzip_help'] = 'An alternative to installing a plugin directly from the Moodle plugins directory is to upload a ZIP package of the plugin. The ZIP package should have the same structure as a package downloaded from the Moodle plugins directory.';
 $string['installfromzipfile'] = 'ZIP package';
-$string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory, named to match the plugin. The ZIP will be extracted into an appropriate location for the plugin type. If the package has been downloaded from the Moodle plugins directory then it will have this structure.';
+$string['installfromzipfile_help'] = 'The plugin ZIP package must contain just one directory, named to match the plugin name. The ZIP will be extracted into an appropriate location for the plugin type. If the package has been downloaded from the Moodle plugins directory then it will have this structure.';
+$string['installfromzipinvalid'] = 'The plugin ZIP package must contain just one directory, named to match the plugin name. Provided file is not a valid plugin ZIP package.';
 $string['installfromziprootdir'] = 'Rename the root directory';
 $string['installfromziprootdir_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. If so, the correct name may be entered here.';
 $string['installfromzipsubmit'] = 'Install plugin from the ZIP file';
@@ -56,54 +56,6 @@ $string['remoterequestconfirm'] = 'There is a request to install plugin <strong>
 $string['remoterequestinvalid'] = 'There is a request to install a plugin from the Moodle plugins directory on this site. Unfortunately the request is not valid and so the plugin cannot be installed.';
 $string['remoterequestpermcheck'] = 'There is a request to install plugin {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, the location <strong>{$a->typepath}</strong> is <strong>not writable</strong>. You need to give write access for the web server user to the location, then press the continue button to repeat the check.';
 $string['remoterequestpluginfoexception'] = 'Oops... An error occurred while trying to obtain information about the plugin {$a->name} ({$a->component}) version {$a->version}. The plugin cannot be installed. Turn debugging mode on to see details of the error.';
+$string['remoterequestnoninstallable'] = 'There is a request to install plugin {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory on this site. However, the plugin installation pre-check failed (reason code: {$a->reason}).';
 $string['typedetectionfailed'] = 'Unable to detect the plugin type. Please choose the plugin type manually.';
-$string['validation'] = 'Plugin package validation';
-$string['validationmsg_componentmatch'] = 'Full component name';
-$string['validationmsg_componentmismatchname'] = 'Plugin name mismatch';
-$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the declared plugin name.';
-$string['validationmsg_componentmismatchname_info'] = 'The plugin declares its name is \'{$a}\' but that does not match the name of the root directory.';
-$string['validationmsg_componentmismatchtype'] = 'Plugin type mismatch';
-$string['validationmsg_componentmismatchtype_info'] = 'Expected type \'{$a->expected}\' but the plugin declares its type is \'{$a->found}\'.';
-$string['validationmsg_filenotexists'] = 'Extracted file not found';
-$string['validationmsg_filesnumber'] = 'Not enough files found in the package';
-$string['validationmsg_filestatus'] = 'Unable to extract all files';
-$string['validationmsg_filestatus_info'] = 'Attempting to extract file {$a->file} resulted in error \'{$a->status}\'.';
-$string['validationmsg_foundlangfile'] = 'Found language file';
-$string['validationmsg_maturity'] = 'Declared maturity level';
-$string['validationmsg_maturity_help'] = 'The plugin can declare its maturity level. If the maintainer considers the plugin stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.';
-$string['validationmsg_missingcomponent'] = 'Plugin does not declare its component name';
-$string['validationmsg_missingcomponent_help'] = 'All plugins must provide their full component name via the `$plugin->component` declaration in the version.php file.';
-$string['validationmsg_missingcomponent_link'] = 'Development:version.php';
-$string['validationmsg_missingexpectedlangenfile'] = 'English language file name mismatch';
-$string['validationmsg_missingexpectedlangenfile_info'] = 'The given plugin type is missing the expected English language file {$a}.';
-$string['validationmsg_missinglangenfile'] = 'No English language file found';
-$string['validationmsg_missinglangenfolder'] = 'Missing English language folder';
-$string['validationmsg_missingversion'] = 'Plugin does not declare its version';
-$string['validationmsg_missingversionphp'] = 'File version.php not found';
-$string['validationmsg_multiplelangenfiles'] = 'Multiple English language files found';
-$string['validationmsg_onedir'] = 'Invalid structure of the ZIP package.';
-$string['validationmsg_onedir_help'] = 'The ZIP package must contain just one root directory that holds the plugin code. The name of that root directory must match the name of the plugin.';
-$string['validationmsg_pathwritable'] = 'Write access check';
-$string['validationmsg_pluginversion'] = 'Plugin version';
-$string['validationmsg_release'] = 'Plugin release';
-$string['validationmsg_requiresmoodle'] = 'Required Moodle version';
-$string['validationmsg_rootdir'] = 'Name of the plugin to be installed';
-$string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the plugin to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the plugin.';
-$string['validationmsg_rootdirinvalid'] = 'Invalid plugin name';
-$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the plugin name.';
-$string['validationmsg_targetexists'] = 'Target location already exists';
-$string['validationmsg_targetexists_help'] = 'The directory that the plugin is to be installed to must not yet exist.';
-$string['validationmsg_unknowntype'] = 'Unknown plugin type';
-$string['validationmsg_versionphpsyntax'] = 'Unsupported syntax detected in version.php file';
-$string['validationmsglevel_debug'] = 'Debug';
-$string['validationmsglevel_error'] = 'Error';
-$string['validationmsglevel_info'] = 'OK';
-$string['validationmsglevel_warning'] = 'Warning';
-$string['validationresult0'] = 'Validation failed!';
-$string['validationresult0_help'] = 'A serious problem was detected and so it is not safe to install the plugin. See the validation log messages for details.';
-$string['validationresult1'] = 'Validation passed!';
-$string['validationresult2_help'] = 'No serious problems were detected. You can continue with the plugin installation. See the validation log messages for more details and eventual warnings.';
-$string['validationresult1_help'] = 'The plugin package has been validated and no serious problems were detected.';
-$string['validationresultinfo'] = 'Info';
-$string['validationresultmsg'] = 'Message';
-$string['validationresultstatus'] = 'Status';
+$string['typedetectionmismatch'] = 'The selected plugin type does not match the one declared by the plugin: {$a}';
index 9bd433f..8df27d0 100644 (file)
@@ -36,7 +36,7 @@ if (!has_capability('moodle/site:config', context_system::instance())) {
     die();
 }
 
-if (!empty($CFG->disableonclickaddoninstall)) {
+if (!empty($CFG->disableupdateautodeploy)) {
     header('HTTP/1.1 403 Forbidden');
     die();
 }
@@ -52,9 +52,9 @@ if (is_null($plugintype)) {
     die();
 }
 
-$installer = tool_installaddon_installer::instance();
+$pluginman = core_plugin_manager::instance();
 
-$plugintypepath = $installer->get_plugintype_root($plugintype);
+$plugintypepath = $pluginman->get_plugintype_root($plugintype);
 
 if (empty($plugintypepath)) {
     header('HTTP/1.1 400 Bad Request');
@@ -63,7 +63,7 @@ if (empty($plugintypepath)) {
 
 $response = array('path' => $plugintypepath);
 
-if ($installer->is_plugintype_writable($plugintype)) {
+if ($pluginman->is_plugintype_writable($plugintype)) {
     $response['writable'] = 1;
 } else {
     $response['writable'] = 0;
index d15c804..e69aa67 100644 (file)
@@ -37,9 +37,6 @@ class tool_installaddon_renderer extends plugin_renderer_base {
     /** @var tool_installaddon_installer */
     protected $installer = null;
 
-    /** @var tool_installaddon_validator */
-    protected $validator = null;
-
     /**
      * Sets the tool_installaddon_installer instance being used.
      *
@@ -54,20 +51,6 @@ class tool_installaddon_renderer extends plugin_renderer_base {
         }
     }
 
-    /**
-     * Sets the tool_installaddon_validator instance being used.
-     *
-     * @throws coding_exception if the validator has been already set
-     * @param tool_installaddon_validator $validator
-     */
-    public function set_validator_instance(tool_installaddon_validator $validator) {
-        if (is_null($this->validator)) {
-            $this->validator = $validator;
-        } else {
-            throw new coding_exception('Attempting to reset the validator instance.');
-        }
-    }
-
     /**
      * Defines the index page layout
      *
@@ -96,24 +79,17 @@ class tool_installaddon_renderer extends plugin_renderer_base {
     }
 
     /**
-     * Defines the validation results page layout
+     * Inform the user that the ZIP is not a valid plugin package file.
      *
+     * @param moodle_url $continueurl
      * @return string
      */
-    public function validation_page() {
-
-        if (is_null($this->installer)) {
-            throw new coding_exception('Installer instance has not been set.');
-        }
-
-        if (is_null($this->validator)) {
-            throw new coding_exception('Validator instance has not been set.');
-        }
+    public function zip_not_valid_plugin_package_page(moodle_url $continueurl) {
 
         $out = $this->output->header();
-        $out .= $this->validation_page_heading();
-        $out .= $this->validation_page_messages();
-        $out .= $this->validation_page_continue();
+        $out .= $this->output->heading(get_string('installfromzip', 'tool_installaddon'));
+        $out .= $this->output->box(get_string('installfromzipinvalid', 'tool_installaddon'), 'generalbox', 'notice');
+        $out .= $this->output->continue_button($continueurl, 'get');
         $out .= $this->output->footer();
 
         return $out;
@@ -194,44 +170,17 @@ class tool_installaddon_renderer extends plugin_renderer_base {
     }
 
     /**
-     * Inform the user about pluginfo service call exception
-     *
-     * This implementation does not actually use the passed exception. Custom renderers might want to
-     * display additional data obtained via {@link get_exception_info()}. Also note, this method is called
-     * in non-debugging mode only. If debugging is allowed at the site, default exception handler is triggered.
-     *
-     * @param stdClass $data decoded request data
-     * @param tool_installaddon_pluginfo_exception $e thrown exception
-     * @param moodle_url $continueurl
-     * @return string
-     */
-    public function remote_request_pluginfo_exception(stdClass $data, tool_installaddon_pluginfo_exception $e, moodle_url $continueurl) {
-
-        $out = $this->output->header();
-        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
-        $out .= $this->output->box(get_string('remoterequestpluginfoexception', 'tool_installaddon', $data), 'generalbox', 'notice');
-        $out .= $this->output->continue_button($continueurl, 'get');
-        $out .= $this->output->footer();
-
-        return $out;
-    }
-
-    /**
-     * Inform the user about the installer exception
+     * Inform the user that the requested remote plugin is not installable.
      *
-     * This implementation does not actually use the passed exception. Custom renderers might want to
-     * display additional data obtained via {@link get_exception_info()}. Also note, this method is called
-     * in non-debugging mode only. If debugging is allowed at the site, default exception handler is triggered.
-     *
-     * @param tool_installaddon_installer_exception $e thrown exception
+     * @param stdClass $data decoded request data with ->reason property added
      * @param moodle_url $continueurl
      * @return string
      */
-    public function installer_exception(tool_installaddon_installer_exception $e, moodle_url $continueurl) {
+    public function remote_request_non_installable_page(stdClass $data, moodle_url $continueurl) {
 
         $out = $this->output->header();
         $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
-        $out .= $this->output->box(get_string('installexception', 'tool_installaddon'), 'generalbox', 'notice');
+        $out .= $this->output->box(get_string('remoterequestnoninstallable', 'tool_installaddon', $data), 'generalbox', 'notice');
         $out .= $this->output->continue_button($continueurl, 'get');
         $out .= $this->output->footer();
 
@@ -284,123 +233,4 @@ class tool_installaddon_renderer extends plugin_renderer_base {
 
         return $out;
     }
-
-    /**
-     * Renders the page title and the overall validation verdict
-     *
-     * @return string
-     */
-    protected function validation_page_heading() {
-
-        $heading = $this->output->heading(get_string('validation', 'tool_installaddon'));
-
-        if ($this->validator->get_result()) {
-            $status = $this->output->container(
-                html_writer::span(get_string('validationresult1', 'tool_installaddon'), 'verdict').
-                    $this->output->help_icon('validationresult1', 'tool_installaddon'),
-                array('validationresult', 'success')
-            );
-        } else {
-            $status = $this->output->container(
-                html_writer::span(get_string('validationresult0', 'tool_installaddon'), 'verdict').
-                    $this->output->help_icon('validationresult0', 'tool_installaddon'),
-                array('validationresult', 'failure')
-            );
-        }
-
-        return $heading . $status;
-    }
-
-    /**
-     * Renders validation log messages.
-     *
-     * @return string
-     */
-    protected function validation_page_messages() {
-
-        $validator = $this->validator; // We need this to be able to use their constants.
-        $messages = $validator->get_messages();
-
-        if (empty($messages)) {
-            return '';
-        }
-
-        $table = new html_table();
-        $table->attributes['class'] = 'validationmessages generaltable';
-        $table->head = array(
-            get_string('validationresultstatus', 'tool_installaddon'),
-            get_string('validationresultmsg', 'tool_installaddon'),
-            get_string('validationresultinfo', 'tool_installaddon')
-        );
-        $table->colclasses = array('msgstatus', 'msgtext', 'msginfo');
-
-        $stringman = get_string_manager();
-
-        foreach ($messages as $message) {
-
-            if ($message->level === $validator::DEBUG and !debugging()) {
-                continue;
-            }
-
-            $msgstatus = get_string('validationmsglevel_'.$message->level, 'tool_installaddon');
-            $msgtext = $msgtext = s($message->msgcode);
-            if (is_null($message->addinfo)) {
-                $msginfo = '';
-            } else {
-                $msginfo = html_writer::tag('pre', s(print_r($message->addinfo, true)));
-            }
-            $msghelp = '';
-
-            // Replace the message code with the string if it is defined.
-            if ($stringman->string_exists('validationmsg_'.$message->msgcode, 'tool_installaddon')) {
-                $msgtext = get_string('validationmsg_'.$message->msgcode, 'tool_installaddon');
-                // And check for the eventual help, too.
-                if ($stringman->string_exists('validationmsg_'.$message->msgcode.'_help', 'tool_installaddon')) {
-                    $msghelp = $this->output->help_icon('validationmsg_'.$message->msgcode, 'tool_installaddon');
-                }
-            }
-
-            // Re-format the message info using a string if it is define.
-            if (!is_null($message->addinfo) and $stringman->string_exists('validationmsg_'.$message->msgcode.'_info', 'tool_installaddon')) {
-                $msginfo = get_string('validationmsg_'.$message->msgcode.'_info', 'tool_installaddon', $message->addinfo);
-            }
-
-            $row = new html_table_row(array($msgstatus, $msgtext.$msghelp, $msginfo));
-            $row->attributes['class'] = 'level-'.$message->level.' '.$message->msgcode;
-
-            $table->data[] = $row;
-        }
-
-        return html_writer::table($table);
-    }
-
-    /**
-     * Renders widgets to continue from the validation results page
-     *
-     * @return string
-     */
-    protected function validation_page_continue() {
-
-        $output = '';
-        $conturl = $this->validator->get_continue_url();
-
-        if (is_null($conturl)) {
-            $contbutton = '';
-
-        } else {
-            $contbutton = $this->output->single_button(
-                $conturl, get_string('installaddon', 'tool_installaddon'), 'post',
-                array('class' => 'singlebutton continuebutton'));
-            $output .= $this->output->heading(get_string('acknowledgement', 'tool_installaddon'), 3);
-            $output .= $this->output->container(get_string('acknowledgementtext', 'tool_installaddon'));
-        }
-
-        $cancelbutton = $this->output->single_button(
-            new moodle_url('/admin/tool/installaddon/index.php'), get_string('cancel', 'core'), 'get',
-            array('class' => 'singlebutton cancelbutton'));
-
-        $output .= $this->output->container($cancelbutton.$contbutton, 'postvalidationbuttons');
-
-        return $output;
-    }
 }
index b533360..17f0d51 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-if ($hassiteconfig and empty($CFG->disableonclickaddoninstall)) {
+if ($hassiteconfig and empty($CFG->disableupdateautodeploy)) {
 
     $ADMIN->add('modules', new admin_externalpage('tool_installaddon_index',
         get_string('installaddons', 'tool_installaddon'),
         "$CFG->wwwroot/$CFG->admin/tool/installaddon/index.php"), 'modsettings');
-
-    $ADMIN->add('modules', new admin_externalpage('tool_installaddon_validate',
-        get_string('validation', 'tool_installaddon'),
-        "$CFG->wwwroot/$CFG->admin/tool/installaddon/validate.php",
-        'moodle/site:config',
-        true), 'modsettings');
 }
index 05155ca..3de50f3 100644 (file)
 #page-admin-tool-installaddon-index #installfromrepobox .singlebutton input[type=submit] {
     padding: 1em;
 }
-
-#page-admin-tool-installaddon-validate .validationresult {
-    margin: 2em auto;
-    text-align: center;
-}
-
-#page-admin-tool-installaddon-validate .validationresult .verdict {
-    margin: 0em 0.5em;
-    padding: 0.5em;
-    border: 2px solid;
-    -webkit-border-radius: 5px;
-    -moz-border-radius: 5px;
-    border-radius: 5px;
-    font-weight: bold;
-}
-
-#page-admin-tool-installaddon-validate .validationresult.success .verdict {
-    background-color: #e7f1c3;
-    border-color: #aaeeaa;
-}
-
-#page-admin-tool-installaddon-validate .validationresult.failure .verdict {
-    background-color: #ffd3d9;
-    border-color: #eeaaaa;
-}
-
-#page-admin-tool-installaddon-validate .validationmessages {
-    margin: 0px auto;
-}
-
-#page-admin-tool-installaddon-validate .validationmessages .level-error .msgstatus {
-    background-color: #ffd3d9;
-}
-
-#page-admin-tool-installaddon-validate .validationmessages .level-warning .msgstatus {
-    background-color: #f3f2aa;
-}
-
-#page-admin-tool-installaddon-validate .validationmessages .level-info .msgstatus {
-    background-color: #e7f1c3;
-}
-
-#page-admin-tool-installaddon-validate .validationmessages .level-debug .msgstatus {
-    background-color: #d2ebff;
-}
-
-#page-admin-tool-installaddon-validate .postvalidationbuttons {
-    text-align: center;
-    margin: 1em auto;
-}
-
-#page-admin-tool-installaddon-validate .postvalidationbuttons .singlebutton {
-    display: inline-block;
-    margin: 1em 1em;
-}
diff --git a/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php b/admin/tool/installaddon/tests/fixtures/github/moodle-repository_mahara-master/lang/en/repository_mahara.php
deleted file mode 100644 (file)
index b3d9bbc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/index.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/index.php
deleted file mode 100644 (file)
index b3d9bbc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bah.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/lang/en/bah.php
deleted file mode 100644 (file)
index b3d9bbc..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<?php
diff --git a/admin/tool/installaddon/tests/fixtures/nolang/bah/lib.php b/admin/tool/installaddon/tests/fixtures/nolang/bah/lib.php
deleted file mode 100644 (file)
index 50cce95..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-
diff --git a/admin/tool/installaddon/tests/fixtures/nowrapdir/index.php b/admin/tool/installaddon/tests/fixtures/nowrapdir/index.php
deleted file mode 100644 (file)
index a37d47e..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-// index.php
diff --git a/admin/tool/installaddon/tests/fixtures/testable_installer.php b/admin/tool/installaddon/tests/fixtures/testable_installer.php
new file mode 100644 (file)
index 0000000..c8ebd82
--- /dev/null
@@ -0,0 +1,60 @@
+<?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 {@link testable_tool_installaddon_installer} class.
+ *
+ * @package     tool_installaddon
+ * @subpackage  fixtures
+ * @category    test
+ * @copyright   2013, 2015 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Testable subclass of the tested class
+ *
+ * @copyright 2013 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_tool_installaddon_installer extends tool_installaddon_installer {
+
+    public function get_site_fullname() {
+        return strip_tags('<h1 onmouseover="alert(\'Hello Moodle.org!\');">Nasty site</h1>');
+    }
+
+    public function get_site_url() {
+        return 'file:///etc/passwd';
+    }
+
+    public function get_site_major_version() {
+        return "2.5'; DROP TABLE mdl_user; --";
+    }
+
+    public function testable_decode_remote_request($request) {
+        return parent::decode_remote_request($request);
+    }
+
+    protected function should_send_site_info() {
+        return true;
+    }
+
+    public function testable_detect_plugin_component_from_versionphp($code) {
+        return parent::detect_plugin_component_from_versionphp($code);
+    }
+}
index 7280eb9..55a0608 100644 (file)
@@ -26,6 +26,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+global $CFG;
+require_once(__DIR__.'/fixtures/testable_installer.php');
 
 /**
  * Unit tests for the {@link tool_installaddon_installer} class
@@ -49,29 +51,6 @@ class tool_installaddon_installer_testcase extends advanced_testcase {
         $this->assertSame("2.5'; DROP TABLE mdl_user; --", $site['majorversion']);
     }
 
-    public function test_extract_installfromzip_file() {
-        $jobid = md5(rand().uniqid('test_', true));
-        $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
-        $contentsdir = make_temp_directory('tool_installaddon/'.$jobid.'/contents');
-        copy(dirname(__FILE__).'/fixtures/zips/invalidroot.zip', $sourcedir.'/testinvalidroot.zip');
-
-        $installer = tool_installaddon_installer::instance();
-        $files = $installer->extract_installfromzip_file($sourcedir.'/testinvalidroot.zip', $contentsdir, 'fixed_root');
-        $this->assertInternalType('array', $files);
-        $this->assertCount(4, $files);
-        $this->assertSame(true, $files['fixed_root/']);
-        $this->assertSame(true, $files['fixed_root/lang/']);
-        $this->assertSame(true, $files['fixed_root/lang/en/']);
-        $this->assertSame(true, $files['fixed_root/lang/en/fixed_root.php']);
-        foreach ($files as $file => $status) {
-            if (substr($file, -1) === '/') {
-                $this->assertTrue(is_dir($contentsdir.'/'.$file));
-            } else {
-                $this->assertTrue(is_file($contentsdir.'/'.$file));
-            }
-        }
-    }
-
     public function test_decode_remote_request() {
         $installer = testable_tool_installaddon_installer::instance();
 
@@ -130,66 +109,51 @@ class tool_installaddon_installer_testcase extends advanced_testcase {
         $this->assertSame(false, $installer->testable_decode_remote_request($request));
     }
 
-    public function test_move_directory() {
-        $jobid = md5(rand().uniqid('test_', true));
-        $jobroot = make_temp_directory('tool_installaddon/'.$jobid);
-        $contentsdir = make_temp_directory('tool_installaddon/'.$jobid.'/contents/sub/folder');
-        file_put_contents($contentsdir.'/readme.txt', 'Hello world!');
+    public function test_detect_plugin_component() {
+        global $CFG;
 
         $installer = tool_installaddon_installer::instance();
-        $installer->move_directory($jobroot.'/contents', $jobroot.'/moved', 0777, 0666);
 
-        $this->assertFalse(is_dir($jobroot.'/contents'));
-        $this->assertTrue(is_file($jobroot.'/moved/sub/folder/readme.txt'));
-        $this->assertSame('Hello world!', file_get_contents($jobroot.'/moved/sub/folder/readme.txt'));
-    }
+        $zipfile = $CFG->libdir.'/tests/fixtures/update_validator/zips/bar.zip';
+        $this->assertEquals('foo_bar', $installer->detect_plugin_component($zipfile));
 
-    public function test_detect_plugin_component() {
-        $jobid = md5(rand().uniqid('test_', true));
-        $workdir = make_temp_directory('tool_installaddon/'.$jobid.'/version');
-        $zipfile = __DIR__.'/fixtures/zips/bar.zip';
-        $installer = tool_installaddon_installer::instance();
-        $this->assertEquals('foo_bar', $installer->detect_plugin_component($zipfile, $workdir));
+        $zipfile = $CFG->libdir.'/tests/fixtures/update_validator/zips/invalidroot.zip';
+        $this->assertFalse($installer->detect_plugin_component($zipfile));
     }
 
     public function test_detect_plugin_component_from_versionphp() {
+        global $CFG;
+
         $installer = testable_tool_installaddon_installer::instance();
-        $this->assertEquals('bar_bar_conan', $installer->detect_plugin_component_from_versionphp('
+        $fixtures = $CFG->libdir.'/tests/fixtures/update_validator/';
+
+        $this->assertEquals('bar_bar_conan', $installer->testable_detect_plugin_component_from_versionphp('
 $plugin->version  = 2014121300;
   $plugin->component=   "bar_bar_conan"  ; // Go Arnie go!'));
-    }
-}
 
+        $versionphp = file_get_contents($fixtures.'/github/moodle-repository_mahara-master/version.php');
+        $this->assertEquals('repository_mahara', $installer->testable_detect_plugin_component_from_versionphp($versionphp));
 
-/**
- * Testable subclass of the tested class
- *
- * @copyright 2013 David Mudrak <david@moodle.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class testable_tool_installaddon_installer extends tool_installaddon_installer {
-
-    public function get_site_fullname() {
-        return strip_tags('<h1 onmouseover="alert(\'Hello Moodle.org!\');">Nasty site</h1>');
+        $versionphp = file_get_contents($fixtures.'/nocomponent/baz/version.php');
+        $this->assertFalse($installer->testable_detect_plugin_component_from_versionphp($versionphp));
     }
 
-    public function get_site_url() {
-        return 'file:///etc/passwd';
-    }
-
-    public function get_site_major_version() {
-        return "2.5'; DROP TABLE mdl_user; --";
-    }
+    public function test_make_installfromzip_storage() {
+        $installer = testable_tool_installaddon_installer::instance();
 
-    public function testable_decode_remote_request($request) {
-        return parent::decode_remote_request($request);
-    }
+        // Check we get writable directory.
+        $storage1 = $installer->make_installfromzip_storage();
+        $this->assertTrue(is_dir($storage1));
+        $this->assertTrue(is_writable($storage1));
+        file_put_contents($storage1.'/hello.txt', 'Find me if you can!');
 
-    protected function should_send_site_info() {
-        return true;
-    }
+        // Check we get unique directory on each call.
+        $storage2 = $installer->make_installfromzip_storage();
+        $this->assertTrue(is_dir($storage2));
+        $this->assertTrue(is_writable($storage2));
+        $this->assertFalse(file_exists($storage2.'/hello.txt'));
 
-    public function detect_plugin_component_from_versionphp($code) {
-        return parent::detect_plugin_component_from_versionphp($code);
+        // Check both are in the same parent directory.
+        $this->assertEquals(dirname($storage1), dirname($storage2));
     }
 }
diff --git a/admin/tool/installaddon/validate.php b/admin/tool/installaddon/validate.php
deleted file mode 100644 (file)
index ef7454d..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?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/>.
-
-/**
- * The ZIP package validation.
- *
- * @package     tool_installaddon
- * @copyright   2013 David Mudrak <david@moodle.com>
- * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-require(dirname(__FILE__) . '/../../../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->libdir.'/filelib.php');
-
-navigation_node::override_active_url(new moodle_url('/admin/tool/installaddon/index.php'));
-admin_externalpage_setup('tool_installaddon_validate');
-
-if (!empty($CFG->disableonclickaddoninstall)) {
-    notice(get_string('featuredisabled', 'tool_installaddon'));
-}
-
-require_sesskey();
-
-$jobid = required_param('jobid', PARAM_ALPHANUM);
-$zipfilename = required_param('zip', PARAM_FILE);
-$plugintype = required_param('type', PARAM_ALPHANUMEXT);
-$rootdir = optional_param('rootdir', '', PARAM_PLUGIN);
-
-$zipfilepath = $CFG->tempdir.'/tool_installaddon/'.$jobid.'/source/'.$zipfilename;
-if (!file_exists($zipfilepath)) {
-    redirect(new moodle_url('/admin/tool/installaddon/index.php'),
-        get_string('invaliddata', 'core_error'));
-}
-
-$installer = tool_installaddon_installer::instance();
-
-// Extract the ZIP contents.
-fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid.'/contents');
-$zipcontentpath = make_temp_directory('tool_installaddon/'.$jobid.'/contents');
-$zipcontentfiles = $installer->extract_installfromzip_file($zipfilepath, $zipcontentpath, $rootdir);
-
-// Validate the contents of the plugin ZIP file.
-$validator = tool_installaddon_validator::instance($zipcontentpath, $zipcontentfiles);
-$validator->assert_plugin_type($plugintype);
-$validator->assert_moodle_version($CFG->version);
-$result = $validator->execute();
-
-if ($result) {
-    $validator->set_continue_url(new moodle_url('/admin/tool/installaddon/deploy.php', array(
-        'sesskey' => sesskey(),
-        'jobid' => $jobid,
-        'type' => $plugintype,
-        'name' => $validator->get_rootdir())));
-
-} else {
-    fulldelete($CFG->tempdir.'/tool_installaddon/'.$jobid);
-}
-
-// Display the validation results.
-$output = $PAGE->get_renderer('tool_installaddon');
-$output->set_installer_instance($installer);
-$output->set_validator_instance($validator);
-echo $output->validation_page();
index a9b59fc..beb9ac3 100644 (file)
@@ -447,16 +447,13 @@ $CFG->admin = 'admin';
 //
 //      $CFG->disableupdatenotifications = true;
 //
-// Use the following flag to completely disable the Automatic updates deployment
-// feature and hide it from the server administration UI.
+// Use the following flag to completely disable the installation of plugins
+// (new plugins, available updates and missing dependencies) and related
+// features (such as cancelling the plugin installation or upgrade) via the
+// server administration web interface.
 //
 //      $CFG->disableupdateautodeploy = true;
 //
-// Use the following flag to completely disable the On-click add-on installation
-// feature and hide it from the server administration UI.
-//
-//      $CFG->disableonclickaddoninstall = true;
-//
 // Use the following flag to disable modifications to scheduled tasks
 // whilst still showing the state of tasks.
 //
index e792044..390320b 100644 (file)
@@ -1084,6 +1084,8 @@ $string['updateavailable_moreinfo'] = 'More info...';
 $string['updateavailable_release'] = 'Moodle {$a}';
 $string['updateavailable_version'] = 'Version {$a}';
 $string['updateavailableinstall'] = 'Install this update';
+$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';
@@ -1091,8 +1093,6 @@ $string['updatenotificationfooter'] = 'Your Moodle site {$a->siteurl} is configu
 $string['updatenotificationsubject'] = 'Moodle updates are available ({$a->siteurl})';
 $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 additional plugins. If there is a new update available, a notification will be sent to site admins.';
-$string['updateautodeploy'] = 'Enable updates deployment';
-$string['updateautodeploy_desc'] = 'If enabled, you will be able to download and install available updates directly from Moodle administration pages. Note that your web server process has to have write access into folders with Moodle installation to make this work. That can be seen as a potential security risk.';
 $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';
index 95db37f..fc55100 100644 (file)
@@ -27,41 +27,62 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['actions'] = 'Actions';
 $string['availability'] = 'Availability';
+$string['cancelinstallall'] = 'Cancel new installations ({$a})';
+$string['cancelinstallone'] = 'Cancel this installation';
+$string['cancelinstallhead'] = 'Cancelling installation of plugins';
+$string['cancelinstallinfo'] = 'Following plugins are not fully installed yet and their installation can be cancelled. To do so, the plugin folder must be removed from your server now. Make sure that is really what you want to prevent accidental data loss (such as your own code modifications).';
+$string['cancelinstallinfodir'] = 'Folder to be deleted: {$a}';
+$string['cancelupgradeall'] = 'Cancel upgrades ({$a})';
+$string['cancelupgradehead'] = 'Restoring previous version of plugins';
+$string['cancelupgradeone'] = 'Cancel this upgrade';
 $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['dependencyinstallhead'] = 'Installing missing dependencies';
+$string['dependencyinstallmissing'] = 'Install missing dependencies ({$a})';
+$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.';
 $string['err_response_http_code'] = 'Unable to fetch available updates data - unexpected HTTP response code.';
 $string['filterall'] = 'Show all';
 $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';
 $string['noneinstalled'] = 'No plugins of this type are installed';
 $string['notes'] = 'Notes';
 $string['notdownloadable'] = 'Can not download the package';
 $string['notdownloadable_help'] = 'ZIP package with the update can not be downloaded automatically. Please refer to the documentation page for more help.';
 $string['notdownloadable_link'] = 'admin/mdeploy/notdownloadable';
 $string['notwritable'] = 'Plugin files not writable';
-$string['notwritable_help'] = 'You have enabled automatic updates deployment and there is an available update for this plugin. However, the plugin files are not writable by the web server so the update cannot be installed automatically.
-
-You need to make the plugin folder and all its contents writable to be able to install the available update automatically.';
-$string['notwritable_link'] = 'admin/mdeploy/notwritable';
-$string['numtotal'] = 'Installed: {$a}';
-$string['numdisabled'] = 'Disabled: {$a}';
-$string['numextension'] = 'Additional: {$a}';
-$string['numupdatable'] = 'Updates available: {$a}';
+$string['notwritable_help'] = 'Plugin files are not writable by the web server. The web server process has to have write access to the plugin folder and all its contents. Write access to the root folder of the given plugin type may be required, too.';
 $string['otherplugin'] = '{$a->component}';
 $string['otherpluginversion'] = '{$a->component} ({$a->version})';
-$string['showall'] = 'Reload and show all plugins';
-$string['pluginchecknotice'] = 'This page displays plugins that may require your attention during the upgrade. Highlighted items include new plugins that are about to be installed, updated plugins that are about to be upgraded and any missing plugins. Additional plugins are highlighted if there is an available update for them. It is recommended that you check whether there are more recent versions of plugins available and update their source code before continuing with this Moodle upgrade.';
+$string['overviewall'] = 'All plugins';
+$string['overviewext'] = 'Additional plugins';
+$string['overviewupdatable'] = 'Available updates';
+$string['packagesdebug'] = 'Debugging output enabled';
+$string['packagesdownloading'] = 'Downloading {$a}';
+$string['packagesextracting'] = 'Extracting {$a}';
+$string['packagesvalidating'] = 'Validating {$a}';
+$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';
+$string['pluginchecknotice'] = 'This page displays plugins that may require your attention during the upgrade, such as new plugins to be installed, plugins to be upgraded, missing plugins etc. Additional plugins are displayed if there is an available update for them. It is recommended that you check whether there are more recent versions of plugins available and update their source code before continuing with this Moodle upgrade.';
 $string['plugindisable'] = 'Disable';
 $string['plugindisabled'] = 'Disabled';
 $string['pluginenable'] = 'Enable';
@@ -71,10 +92,6 @@ $string['requiredby'] = 'Required by: {$a}';
 $string['requires'] = 'Requires';
 $string['rootdir'] = 'Directory';
 $string['settings'] = 'Settings';
-$string['somehighlighted'] = 'Number of plugins requiring your attention: {$a}';
-$string['somehighlightedall'] = 'Number of installed plugins: {$a}';
-$string['somehighlightedinfo'] = 'Display the full list of installed plugins';
-$string['somehighlightedonly'] = 'Display only plugins requiring your attention';
 $string['source'] = 'Source';
 $string['sourceext'] = 'Additional';
 $string['sourcestd'] = 'Standard';
@@ -86,6 +103,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';
@@ -163,6 +181,48 @@ $string['uninstallextraconfirmblock'] = 'There are {$a->instances}¬†instances of
 $string['uninstallextraconfirmenrol'] = 'There are {$a->enrolments} user enrolments.';
 $string['uninstallextraconfirmmod'] = 'There are {$a->instances}¬†instances of this module in {$a->courses} courses.';
 $string['uninstalling'] = 'Uninstalling {$a->name}';
+$string['validationmsg_componentmatch'] = 'Full component name';
+$string['validationmsg_componentmismatchname'] = 'Plugin name mismatch';
+$string['validationmsg_componentmismatchname_help'] = 'Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the declared plugin name.';
+$string['validationmsg_componentmismatchname_info'] = 'The plugin declares its name is \'{$a}\' but that does not match the name of the root directory.';
+$string['validationmsg_componentmismatchtype'] = 'Plugin type mismatch';
+$string['validationmsg_componentmismatchtype_info'] = 'Expected type \'{$a->expected}\' but the plugin declares its type is \'{$a->found}\'.';
+$string['validationmsg_filenotexists'] = 'Extracted file not found';
+$string['validationmsg_filesnumber'] = 'Not enough files found in the package';
+$string['validationmsg_filestatus'] = 'Unable to extract all files';
+$string['validationmsg_filestatus_info'] = 'Attempting to extract file {$a->file} resulted in error \'{$a->status}\'.';
+$string['validationmsg_foundlangfile'] = 'Found language file';
+$string['validationmsg_maturity'] = 'Declared maturity level';
+$string['validationmsg_maturity_help'] = 'The plugin can declare its maturity level. If the maintainer considers the plugin stable, the declared maturity level will read MATURITY_STABLE. All other maturity levels (such as alpha or beta) should be considered unstable and a warning is raised.';
+$string['validationmsg_missingcomponent'] = 'Plugin does not declare its component name';
+$string['validationmsg_missingcomponent_help'] = 'All plugins must provide their full component name via the `$plugin->component` declaration in the version.php file.';
+$string['validationmsg_missingcomponent_link'] = 'Development:version.php';
+$string['validationmsg_missingexpectedlangenfile'] = 'English language file name mismatch';
+$string['validationmsg_missingexpectedlangenfile_info'] = 'The given plugin type is missing the expected English language file {$a}.';
+$string['validationmsg_missinglangenfile'] = 'No English language file found';
+$string['validationmsg_missinglangenfolder'] = 'Missing English language folder';
+$string['validationmsg_missingversion'] = 'Plugin does not declare its version';
+$string['validationmsg_missingversionphp'] = 'File version.php not found';
+$string['validationmsg_multiplelangenfiles'] = 'Multiple English language files found';
+$string['validationmsg_onedir'] = 'Invalid structure of the ZIP package.';
+$string['validationmsg_onedir_help'] = 'The ZIP package must contain just one root directory that holds the plugin code. The name of that root directory must match the name of the plugin.';
+$string['validationmsg_pathwritable'] = 'Write access check';
+$string['validationmsg_pluginversion'] = 'Plugin version';
+$string['validationmsg_release'] = 'Plugin release';
+$string['validationmsg_requiresmoodle'] = 'Required Moodle version';
+$string['validationmsg_rootdir'] = 'Name of the plugin to be installed';
+$string['validationmsg_rootdir_help'] = 'The name of the root directory in the ZIP package forms the name of the plugin to be installed. If the name is not correct, you may wish to rename the root directory in the ZIP prior to installing the plugin.';
+$string['validationmsg_rootdirinvalid'] = 'Invalid plugin name';
+$string['validationmsg_rootdirinvalid_help'] = 'The name of the root directory in the ZIP package violates formal syntax requirements. Some ZIP packages, such as those generated by Github, may contain an incorrect root directory name. You need to fix the name of the root directory to match the plugin name.';
+$string['validationmsg_targetexists'] = 'Target location already exists and will be removed';
+$string['validationmsg_targetexists_help'] = 'The plugin directory already exists and will be replaced by the plugin package contents.';
+$string['validationmsg_targetnotdir'] = 'Target location occupied by a file';
+$string['validationmsg_unknowntype'] = 'Unknown plugin type';
+$string['validationmsg_versionphpsyntax'] = 'Unsupported syntax detected in version.php file';
+$string['validationmsglevel_debug'] = 'Debug';
+$string['validationmsglevel_error'] = 'Error';
+$string['validationmsglevel_info'] = 'OK';
+$string['validationmsglevel_warning'] = 'Warning';
 $string['version'] = 'Version';
 $string['versiondb'] = 'Current version';
 $string['versiondisk'] = 'New version';
index 328133e..6b1f3fc 100644 (file)
@@ -53,12 +53,28 @@ class core_plugin_manager {
     /** the plugin is installed but missing from disk */
     const PLUGIN_STATUS_MISSING     = 'missing';
 
+    /** the given requirement/dependency is fulfilled */
+    const REQUIREMENT_STATUS_OK = 'ok';
+    /** the plugin requires higher core/other plugin version than is currently installed */
+    const REQUIREMENT_STATUS_OUTDATED = 'outdated';
+    /** 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 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 */
@@ -67,6 +83,10 @@ class core_plugin_manager {
     protected $presentplugins = null;
     /** @var array reordered list of plugin types */
     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()}
@@ -86,10 +106,10 @@ class core_plugin_manager {
      * @return core_plugin_manager the singleton instance
      */
     public static function instance() {
-        if (is_null(self::$singletoninstance)) {
-            self::$singletoninstance = new self();
+        if (is_null(static::$singletoninstance)) {
+            static::$singletoninstance = new static();
         }
-        return self::$singletoninstance;
+        return static::$singletoninstance;
     }
 
     /**
@@ -98,15 +118,19 @@ class core_plugin_manager {
      */
     public static function reset_caches($phpunitreset = false) {
         if ($phpunitreset) {
-            self::$singletoninstance = null;
+            static::$singletoninstance = null;
         } else {
-            if (self::$singletoninstance) {
-                self::$singletoninstance->pluginsinfo = null;
-                self::$singletoninstance->subpluginsinfo = null;
-                self::$singletoninstance->installedplugins = null;
-                self::$singletoninstance->enabledplugins = null;
-                self::$singletoninstance->presentplugins = null;
-                self::$singletoninstance->plugintypes = null;
+            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');
@@ -238,7 +262,7 @@ class core_plugin_manager {
 
         $plugintypes = core_component::get_plugin_types();
         foreach ($plugintypes as $plugintype => $fulldir) {
-            $plugininfoclass = self::resolve_plugininfo_class($plugintype);
+            $plugininfoclass = static::resolve_plugininfo_class($plugintype);
             if (class_exists($plugininfoclass)) {
                 $enabled = $plugininfoclass::get_enabled_plugins();
                 if (!is_array($enabled)) {
@@ -374,24 +398,16 @@ class core_plugin_manager {
 
         if (!isset($types[$type])) {
             // Orphaned subplugins!
-            $plugintypeclass = self::resolve_plugininfo_class($type);
-            $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass);
+            $plugintypeclass = static::resolve_plugininfo_class($type);
+            $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
             return $this->pluginsinfo[$type];
         }
 
         /** @var \core\plugininfo\base $plugintypeclass */
-        $plugintypeclass = self::resolve_plugininfo_class($type);
-        $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass);
+        $plugintypeclass = static::resolve_plugininfo_class($type);
+        $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
         $this->pluginsinfo[$type] = $plugins;
 
-        if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
-            // Append the information about available updates provided by {@link \core\update\checker()}.
-            $provider = \core\update\checker::instance();
-            foreach ($plugins as $plugininfoholder) {
-                $plugininfoholder->check_available_updates($provider);
-            }
-        }
-
         return $this->pluginsinfo[$type];
     }
 
@@ -651,7 +667,6 @@ class core_plugin_manager {
     /**
      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
      *
-     * @see \core\update\deployer::plugin_external_source()
      * @param string $component frankenstyle component name
      * @return false|string
      */
@@ -758,6 +773,405 @@ class core_plugin_manager {
         return $return;
     }
 
+    /**
+     * Resolve requirements and dependencies of a plugin.
+     *
+     * Returns an array of objects describing the requirement/dependency,
+     * indexed by the frankenstyle name of the component. The returned array
+     * can be empty. The objects in the array have following properties:
+     *
+     *  ->(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
+     * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
+     * @return array of objects
+     */
+    public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
+        global $CFG;
+
+        if ($plugin->versiondisk === null) {
+            // Missing from disk, we have no version.php to read from.
+            return array();
+        }
+
+        if ($moodleversion === null) {
+            $moodleversion = $CFG->version;
+        }
+
+        if ($moodlebranch === null) {
+            $moodlebranch = $CFG->branch;
+        }
+
+        $reqs = array();
+        $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
+
+        if (!empty($reqcore)) {
+            $reqs['core'] = $reqcore;
+        }
+
+        foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
+            $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
+        }
+
+        return $reqs;
+    }
+
+    /**
+     * Helper method to resolve plugin's requirements on the moodle core.
+     *
+     * @param \core\plugininfo\base $plugin the plugin we are checking
+     * @param string|int|double $moodleversion moodle core branch to check against
+     * @return stdObject
+     */
+    protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
+
+        $reqs = (object)array(
+            'hasver' => null,
+            'reqver' => null,
+            'status' => null,
+            'availability' => null,
+        );
+
+        $reqs->hasver = $moodleversion;
+
+        if (empty($plugin->versionrequires)) {
+            $reqs->reqver = ANY_VERSION;
+        } else {
+            $reqs->reqver = $plugin->versionrequires;
+        }
+
+        if ($plugin->is_core_dependency_satisfied($moodleversion)) {
+            $reqs->status = self::REQUIREMENT_STATUS_OK;
+        } else {
+            $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
+        }
+
+        return $reqs;
+    }
+
+    /**
+     * Helper method to resolve plugin's dependecies on other plugins.
+     *
+     * @param \core\plugininfo\base $plugin the plugin we are checking
+     * @param string $otherpluginname
+     * @param string|int $requiredversion
+     * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
+     * @return stdClass
+     */
+    protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
+            $requiredversion, $moodlebranch) {
+
+        $reqs = (object)array(
+            'hasver' => null,
+            'reqver' => null,
+            'status' => null,
+            'availability' => null,
+        );
+
+        $otherplugin = $this->get_plugin_info($otherpluginname);
+
+        if ($otherplugin !== null) {
+            // The required plugin is installed.
+            $reqs->hasver = $otherplugin->versiondisk;
+            $reqs->reqver = $requiredversion;
+            // Check it has sufficient version.
+            if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
+                $reqs->status = self::REQUIREMENT_STATUS_OK;
+            } else {
+                $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
+            }
+
+        } else {
+            // The required plugin is not installed.
+            $reqs->hasver = null;
+            $reqs->reqver = $requiredversion;
+            $reqs->status = self::REQUIREMENT_STATUS_MISSING;
+        }
+
+        if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
+            if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
+                $reqs->availability = self::REQUIREMENT_AVAILABLE;
+            } else {
+                $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
+            }
+        }
+
+        return $reqs;
+    }
+
+    /**
+     * Is the given plugin version available in the plugins directory?
+     *
+     * 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, $version, $exactmatch) {
+
+        $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
+
+        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;
+    }
+
+    /**
+     * Can the given plugin version be installed via the admin UI?
+     *
+     * This check should be used whenever attempting to install a plugin from
+     * the plugins directory (new install, available update, missing dependency).
+     *
+     * @param string $component
+     * @param int $version version number
+     * @param string $reason returned code of the reason why it is not
+     * @return boolean
+     */
+    public function is_remote_plugin_installable($component, $version, &$reason=null) {
+        global $CFG;
+
+        // Make sure the feature is not disabled.
+        if (!empty($CFG->disableupdateautodeploy)) {
+            $reason = 'disabled';
+            return false;
+        }
+
+        // Make sure the version is available.
+        if (!$this->is_remote_plugin_available($component, $version, true)) {
+            $reason = 'remoteunavailable';
+            return false;
+        }
+
+        // Make sure the plugin type root directory is writable.
+        list($plugintype, $pluginname) = core_component::normalize_component($component);
+        if (!$this->is_plugintype_writable($plugintype)) {
+            $reason = 'notwritableplugintype';
+            return false;
+        }
+
+        $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
+        $localinfo = $this->get_plugin_info($component);
+
+        if ($localinfo) {
+            // If the plugin is already present, prevent downgrade.
+            if ($localinfo->versiondb > $remoteinfo->version->version) {
+                $reason = 'cannotdowngrade';
+                return false;
+            }
+
+            // Make sure we have write access to all the existing code.
+            if (is_dir($localinfo->rootdir)) {
+                if (!$this->is_plugin_folder_removable($component)) {
+                    $reason = 'notwritableplugin';
+                    return false;
+                }
+            }
+        }
+
+        // Looks like it could work.
+        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) {
+        global $CFG;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            return array();
+        }
+        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.
+     *
+     * 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.
+     *
+     * 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 \core\update\remote_info|bool
+     */
+    public function get_remote_plugin_info($component, $version, $exactmatch) {
+
+        if ($exactmatch and $version == ANY_VERSION) {
+            throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
+        }
+
+        $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];
+        }
+    }
+
+    /**
+     * Obtain the plugin ZIP file from the given URL
+     *
+     * The caller is supposed to know both downloads URL and the MD5 hash of
+     * the ZIP contents in advance, typically by using the API requests against
+     * the plugins directory.
+     *
+     * @param string $url
+     * @param string $md5
+     * @return string|bool full path to the file, false on error
+     */
+    public function get_remote_plugin_zip($url, $md5) {
+        global $CFG;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            return false;
+        }
+        return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
+    }
+
+    /**
+     * Extracts the saved plugin ZIP file.
+     *
+     * Returns the list of files found in the ZIP. The format of that list is
+     * array of (string)filerelpath => (bool|string) where the array value is
+     * either true or a string describing the problematic file.
+     *
+     * @see zip_packer::extract_to_pathname()
+     * @param string $zipfilepath full path to the saved ZIP file
+     * @param string $targetdir full path to the directory to extract the ZIP file to
+     * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
+     * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
+     */
+    public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
+        return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
+    }
+
+    /**
+     * Detects the plugin's name from its ZIP file.
+     *
+     * Plugin ZIP packages are expected to contain a single directory and the
+     * directory name would become the plugin name once extracted to the Moodle
+     * dirroot.
+     *
+     * @param string $zipfilepath full path to the ZIP files
+     * @return string|bool false on error
+     */
+    public function get_plugin_zip_root_dir($zipfilepath) {
+        return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
+    }
+
+    /**
+     * Return a list of missing dependencies.
+     *
+     * This should provide the full list of plugins that should be installed to
+     * fulfill the requirements of all plugins, if possible.
+     *
+     * @param bool $availableonly return only available missing dependencies
+     * @return array of \core\update\remote_info|bool indexed by the component name
+     */
+    public function missing_dependencies($availableonly=false) {
+
+        $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, false);
+
+                            if (empty($dependencies[$reqname])) {
+                                $dependencies[$reqname] = $remoteinfo;
+                            } else {
+                                // 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;
+                                }
+                            }
+
+                        } else {
+                            if (!isset($dependencies[$reqname])) {
+                                // Unable to find a plugin fulfilling the requirements.
+                                $dependencies[$reqname] = false;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if ($availableonly) {
+            foreach ($dependencies as $component => $info) {
+                if (empty($info) or empty($info->version)) {
+                    unset($dependencies[$component]);
+                }
+            }
+        }
+
+        return $dependencies;
+    }
+
     /**
      * Is it possible to uninstall the given plugin?
      *
@@ -807,6 +1221,179 @@ class core_plugin_manager {
         return true;
     }
 
+    /**
+     * Perform the installation of plugins.
+     *
+     * If used for installation of remote plugins from the Moodle Plugins
+     * directory, the $plugins must be list of {@link \core\update\remote_info}
+     * object that represent installable remote plugins. The caller can use
+     * {@link self::filter_installable()} to prepare the list.
+     *
+     * If used for installation of plugins from locally available ZIP files,
+     * the $plugins should be list of objects with properties ->component and
+     * ->zipfilepath.
+     *
+     * The method uses {@link mtrace()} to produce direct output and can be
+     * used in both web and cli interfaces.
+     *
+     * @param array $plugins list of 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_plugins(array $plugins, $confirmed, $silent) {
+        global $CFG, $OUTPUT;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            return false;
+        }
+
+        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.
+        $zips = array();
+        foreach ($plugins as $plugin) {
+            if ($plugin instanceof \core\update\remote_info) {
+                $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
+                    $plugin->version->downloadmd5);
+                $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
+                $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);
+            } else {
+                if (empty($plugin->zipfilepath)) {
+                    throw new coding_exception('Unexpected data structure provided');
+                }
+                $zips[$plugin->component] = $plugin->zipfilepath;
+                $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
+            }
+        }
+
+        // Validate all downloaded packages.
+        foreach ($plugins as $plugin) {
+            $zipfile = $zips[$plugin->component];
+            $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $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.
+        foreach ($plugins as $plugin) {
+            $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $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)) {
+                $this->remove_plugin_folder($this->get_plugin_info($plugin->component));
+            }
+            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);
+                if (function_exists('opcache_reset')) {
+                    opcache_reset();
+                }
+                return false;
+            }
+            $silent or $this->mtrace($ok);
+        }
+        if (function_exists('opcache_reset')) {
+            opcache_reset();
+        }
+
+        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.
      *
@@ -886,6 +1473,101 @@ class core_plugin_manager {
         return false;
     }
 
+    /**
+     * Returns list of available updates for the given component.
+     *
+     * This method should be considered as internal API and is supposed to be
+     * called by {@link \core\plugininfo\base::available_updates()} only
+     * to lazy load the data once they are first requested.
+     *
+     * @param string $component frankenstyle name of the plugin
+     * @return null|array array of \core\update\info objects or null
+     */
+    public function load_available_updates_for_plugin($component) {
+        global $CFG;
+
+        $provider = \core\update\checker::instance();
+
+        if (!$provider->enabled() or during_initial_install()) {
+            return null;
+        }
+
+        if (isset($CFG->updateminmaturity)) {
+            $minmaturity = $CFG->updateminmaturity;
+        } else {
+            // This can happen during the very first upgrade to 2.3.
+            $minmaturity = MATURITY_STABLE;
+        }
+
+        return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
+    }
+
+    /**
+     * Returns a list of all available updates to be installed.
+     *
+     * This is used when "update all plugins" action is performed at the
+     * administration UI screen.
+     *
+     * Returns array of remote info objects indexed by the plugin
+     * component. If there are multiple updates available (typically a mix of
+     * stable and non-stable ones), we pick the most mature most recent one.
+     *
+     * Plugins without explicit maturity are considered more mature than
+     * release candidates but less mature than explicit stable (this should be
+     * pretty rare case).
+     *
+     * @return array (string)component => (\core\update\remote_info)remoteinfo
+     */
+    public function available_updates() {
+
+        $updates = array();
+
+        foreach ($this->get_plugins() as $type => $plugins) {
+            foreach ($plugins as $plugin) {
+                $availableupdates = $plugin->available_updates();
+                if (empty($availableupdates)) {
+                    continue;
+                }
+                foreach ($availableupdates as $update) {
+                    if (empty($updates[$plugin->component])) {
+                        $updates[$plugin->component] = $update;
+                        continue;
+                    }
+                    $maturitycurrent = $updates[$plugin->component]->maturity;
+                    if (empty($maturitycurrent)) {
+                        $maturitycurrent = MATURITY_STABLE - 25;
+                    }
+                    $maturityremote = $update->maturity;
+                    if (empty($maturityremote)) {
+                        $maturityremote = MATURITY_STABLE - 25;
+                    }
+                    if ($maturityremote < $maturitycurrent) {
+                        continue;
+                    }
+                    if ($maturityremote > $maturitycurrent) {
+                        $updates[$plugin->component] = $update;
+                        continue;
+                    }
+                    if ($update->version > $updates[$plugin->component]->version) {
+                        $updates[$plugin->component] = $update;
+                        continue;
+                    }
+                }
+            }
+        }
+
+        foreach ($updates as $component => $update) {
+            $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
+            if (empty($remoteinfo) or empty($remoteinfo->version)) {
+                unset($updates[$component]);
+            } else {
+                $updates[$component] = $remoteinfo;
+            }
+        }
+
+        return $updates;
+    }
+
     /**
      * Check to see if the given plugin folder can be removed by the web server process.
      *
@@ -909,6 +1591,57 @@ class core_plugin_manager {
         return $this->is_directory_removable($pluginfo->rootdir);
     }
 
+    /**
+     * Is it possible to create a new plugin directory for the given plugin type?
+     *
+     * @throws coding_exception for invalid plugin types or non-existing plugin type locations
+     * @param string $plugintype
+     * @return boolean
+     */
+    public function is_plugintype_writable($plugintype) {
+
+        $plugintypepath = $this->get_plugintype_root($plugintype);
+
+        if (is_null($plugintypepath)) {
+            throw new coding_exception('Unknown plugin type: '.$plugintype);
+        }
+
+        if ($plugintypepath === false) {
+            throw new coding_exception('Plugin type location does not exist: '.$plugintype);
+        }
+
+        return is_writable($plugintypepath);
+    }
+
+    /**
+     * Returns the full path of the root of the given plugin type
+     *
+     * Null is returned if the plugin type is not known. False is returned if
+     * the plugin type root is expected but not found. Otherwise, string is
+     * returned.
+     *
+     * @param string $plugintype
+     * @return string|bool|null
+     */
+    public function get_plugintype_root($plugintype) {
+
+        $plugintypepath = null;
+        foreach (core_component::get_plugin_types() as $type => $fullpath) {
+            if ($type === $plugintype) {
+                $plugintypepath = $fullpath;
+                break;
+            }
+        }
+        if (is_null($plugintypepath)) {
+            return null;
+        }
+        if (!is_dir($plugintypepath)) {
+            return false;
+        }
+
+        return $plugintypepath;
+    }
+
     /**
      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
      * but are not anymore and are deleted during upgrades.
@@ -1187,6 +1920,186 @@ class core_plugin_manager {
         }
     }
 
+    /**
+     * Remove the current plugin code from the dirroot.
+     *
+     * If removing the currently installed version (which happens during
+     * updates), we archive the code so that the upgrade can be cancelled.
+     *
+     * To prevent accidental data-loss, we also archive the existing plugin
+     * code if cancelling installation of it, so that the developer does not
+     * loose the only version of their work-in-progress.
+     *
+     * @param \core\plugininfo\base $plugin
+     */
+    public function remove_plugin_folder(\core\plugininfo\base $plugin) {
+
+        if (!$this->is_plugin_folder_removable($plugin->component)) {
+            throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
+                array('plugin' => $pluginfo->component, 'rootdir' => $pluginfo->rootdir),
+                'plugin root folder is not removable as expected');
+        }
+
+        if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
+            $this->archive_plugin_version($plugin);
+        }
+
+        remove_dir($plugin->rootdir);
+        clearstatcache();
+        if (function_exists('opcache_reset')) {
+            opcache_reset();
+        }
+    }
+
+    /**
+     * Can the installation of the new plugin be cancelled?
+     *
+     * Subplugins can be cancelled only via their parent plugin, not separately
+     * (they are considered as implicit requirements if distributed together
+     * with the main package).
+     *
+     * @param \core\plugininfo\base $plugin
+     * @return bool
+     */
+    public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
+        global $CFG;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            return false;
+        }
+
+        if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
+                or !$this->is_plugin_folder_removable($plugin->component)) {
+            return false;
+        }
+
+        if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Can the upgrade of the existing plugin be cancelled?
+     *
+     * Subplugins can be cancelled only via their parent plugin, not separately
+     * (they are considered as implicit requirements if distributed together
+     * with the main package).
+     *
+     * @param \core\plugininfo\base $plugin
+     * @return bool
+     */
+    public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
+        global $CFG;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            // Cancelling the plugin upgrade is actually installation of the
+            // previously archived version.
+            return false;
+        }
+
+        if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
+                or !$this->is_plugin_folder_removable($plugin->component)) {
+            return false;
+        }
+
+        if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
+            if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Removes the plugin code directory if it is not installed yet.
+     *
+     * This is intended for the plugins check screen to give the admin a chance
+     * to cancel the installation of just unzipped plugin before the database
+     * upgrade happens.
+     *
+     * @param string $component
+     */
+    public function cancel_plugin_installation($component) {
+        global $CFG;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            return false;
+        }
+
+        $plugin = $this->get_plugin_info($component);
+
+        if ($this->can_cancel_plugin_installation($plugin)) {
+            $this->remove_plugin_folder($plugin);
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns plugins, the installation of which can be cancelled.
+     *
+     * @return array [(string)component] => (\core\plugininfo\base)plugin
+     */
+    public function list_cancellable_installations() {
+        global $CFG;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            return array();
+        }
+
+        $cancellable = array();
+        foreach ($this->get_plugins() as $type => $plugins) {
+            foreach ($plugins as $plugin) {
+                if ($this->can_cancel_plugin_installation($plugin)) {
+                    $cancellable[$plugin->component] = $plugin;
+                }
+            }
+        }
+
+        return $cancellable;
+    }
+
+    /**
+     * Archive the current on-disk plugin code.
+     *
+     * @param \core\plugiinfo\base $plugin
+     * @return bool
+     */
+    public function archive_plugin_version(\core\plugininfo\base $plugin) {
+        return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
+    }
+
+    /**
+     * Returns list of all archives that can be installed to cancel the plugin upgrade.
+     *
+     * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
+     */
+    public function list_restorable_archives() {
+        global $CFG;
+
+        if (!empty($CFG->disableupdateautodeploy)) {
+            return false;
+        }
+
+        $codeman = $this->get_code_manager();
+        $restorable = array();
+        foreach ($this->get_plugins() as $type => $plugins) {
+            foreach ($plugins as $plugin) {
+                if ($this->can_cancel_plugin_upgrade($plugin)) {
+                    $restorable[$plugin->component] = (object)array(
+                        'component' => $plugin->component,
+                        'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
+                    );
+                }
+            }
+        }
+
+        return $restorable;
+    }
+
     /**
      * Reorders plugin types into a sequence to be displayed
      *
@@ -1257,7 +2170,7 @@ class core_plugin_manager {
      * @param string $fullpath
      * @return boolean
      */
-    protected function is_directory_removable($fullpath) {
+    public function is_directory_removable($fullpath) {
 
         if (!is_writable($fullpath)) {
             return false;
@@ -1305,7 +2218,7 @@ class core_plugin_manager {
             return false;
         }
 
-        if ($pluginfo->get_status() === self::PLUGIN_STATUS_NEW) {
+        if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
             // The plugin is not installed. It should be either installed or removed from the disk.
             // Relying on this temporary state may be tricky.
             return false;
@@ -1320,4 +2233,32 @@ class core_plugin_manager {
 
         return true;
     }
+
+    /**
+     * Returns a code_manager instance to be used for the plugins code operations.
+     *
+     * @return \core\update\code_manager
+     */
+    protected function get_code_manager() {
+
+        if ($this->codemanager === null) {
+            $this->codemanager = new \core\update\code_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;
+    }
 }
index 6f7b2e6..cb80f29 100644 (file)
@@ -61,8 +61,11 @@ abstract class base {
     public $instances;
     /** @var int order of the plugin among other plugins of the same type - not supported yet */
     public $sortorder;
+    /** @var core_plugin_manager the plugin manager this plugin info is part of */
+    public $pluginman;
+
     /** @var array|null array of {@link \core\update\info} for this plugin */
-    public $availableupdates;
+    protected $availableupdates;
 
     /**
      * Finds all enabled plugins, the result may include missing plugins.
@@ -76,23 +79,26 @@ abstract class base {
      * Gathers and returns the information about all plugins of the given type,
      * either on disk or previously installed.
      *
+     * This is supposed to be used exclusively by the plugin manager when it is
+     * populating its tree of plugins.
+     *
      * @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
+     * @param core_plugin_manager $pluginman the plugin manager calling this method
      * @return array of plugintype classes, indexed by the plugin name
      */
-    public static function get_plugins($type, $typerootdir, $typeclass) {
+    public static function get_plugins($type, $typerootdir, $typeclass, $pluginman) {
         // Get the information about plugins at the disk.
         $plugins = core_component::get_plugin_list($type);
         $return = array();
         foreach ($plugins as $pluginname => $pluginrootdir) {
             $return[$pluginname] = self::make_plugin_instance($type, $typerootdir,
-                $pluginname, $pluginrootdir, $typeclass);
+                $pluginname, $pluginrootdir, $typeclass, $pluginman);
         }
 
         // Fetch missing incorrectly uninstalled plugins.
-        $manager = core_plugin_manager::instance();
-        $plugins = $manager->get_installed_plugins($type);
+        $plugins = $pluginman->get_installed_plugins($type);
 
         foreach ($plugins as $name => $version) {
             if (isset($return[$name])) {
@@ -105,6 +111,7 @@ abstract class base {
             $plugin->rootdir     = null;
             $plugin->displayname = $name;
             $plugin->versiondb   = $version;
+            $plugin->pluginman   = $pluginman;
             $plugin->init_is_standard();
 
             $return[$name] = $plugin;
@@ -121,14 +128,16 @@ abstract class base {
      * @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
+     * @param core_plugin_manager $pluginman the plugin manager of the new instance
      * @return base the instance of $typeclass
      */
-    protected static function make_plugin_instance($type, $typerootdir, $name, $namerootdir, $typeclass) {
+    protected static function make_plugin_instance($type, $typerootdir, $name, $namerootdir, $typeclass, $pluginman) {
         $plugin              = new $typeclass();
         $plugin->type        = $type;
         $plugin->typerootdir = $typerootdir;
         $plugin->name        = $name;
         $plugin->rootdir     = $namerootdir;
+        $plugin->pluginman   = $pluginman;
 
         $plugin->init_display_name();
         $plugin->load_disk_version();
@@ -205,7 +214,7 @@ abstract class base {
      * data) or is missing from disk.
      */
     public function load_disk_version() {
-        $versions = core_plugin_manager::instance()->get_present_plugins($this->type);
+        $versions = $this->pluginman->get_present_plugins($this->type);
 
         $this->versiondisk = null;
         $this->versionrequires = null;
@@ -259,7 +268,7 @@ abstract class base {
      * @return string|bool false if not a subplugin, name of the parent otherwise
      */
     public function get_parent_plugin() {
-        return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
+        return $this->pluginman->get_parent_of_subplugin($this->type);
     }
 
     /**
@@ -271,7 +280,7 @@ abstract class base {
      * data) or has not been installed yet.
      */
     public function load_db_version() {
-        $versions = core_plugin_manager::instance()->get_installed_plugins($this->type);
+        $versions = $this->pluginman->get_installed_plugins($this->type);
 
         if (isset($versions[$this->name])) {
             $this->versiondb = $versions[$this->name];
@@ -290,14 +299,15 @@ abstract class base {
      */
     public function init_is_standard() {
 
-        $standard = core_plugin_manager::standard_plugins_list($this->type);
+        $pluginman = $this->pluginman;
+        $standard = $pluginman::standard_plugins_list($this->type);
 
         if ($standard !== false) {
             $standard = array_flip($standard);
             if (isset($standard[$this->name])) {
                 $this->source = core_plugin_manager::PLUGIN_SOURCE_STANDARD;
             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
-                and core_plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
+                and $pluginman::is_deleted_standard_plugin($this->type, $this->name)) {
                 $this->source = core_plugin_manager::PLUGIN_SOURCE_STANDARD; // To be deleted.
             } else {
                 $this->source = core_plugin_manager::PLUGIN_SOURCE_EXTENSION;
@@ -338,6 +348,8 @@ abstract class base {
      */
     public function get_status() {
 
+        $pluginman = $this->pluginman;
+
         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
             return core_plugin_manager::PLUGIN_STATUS_NODB;
 
@@ -345,7 +357,7 @@ abstract class base {
             return core_plugin_manager::PLUGIN_STATUS_NEW;
 
         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
-            if (core_plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
+            if ($pluginman::is_deleted_standard_plugin($this->type, $this->name)) {
                 return core_plugin_manager::PLUGIN_STATUS_DELETE;
             } else {
                 return core_plugin_manager::PLUGIN_STATUS_MISSING;
@@ -384,7 +396,7 @@ abstract class base {
             return false;
         }
 
-        $enabled = core_plugin_manager::instance()->get_enabled_plugins($this->type);
+        $enabled = $this->pluginman->get_enabled_plugins($this->type);
 
         if (!is_array($enabled)) {
             return null;
@@ -393,26 +405,6 @@ abstract class base {
         return isset($enabled[$this->name]);
     }
 
-    /**
-     * Populates the property {@link $availableupdates} with the information provided by
-     * available update checker
-     *
-     * @param \core\update\checker $provider the class providing the available update info
-     */
-    public function check_available_updates(\core\update\checker $provider) {
-        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));
-    }
-
     /**
      * If there are updates for this plugin available, returns them.
      *
@@ -420,11 +412,20 @@ abstract class base {
      * is available. Returns null if there is no update available or if the update
      * availability is unknown.
      *
+     * Populates the property {@link $availableupdates} on first call (lazy
+     * loading).
+     *
      * @return array|null
      */
     public function available_updates() {
 
+        if ($this->availableupdates === null) {
+            // Lazy load the information about available updates.
+            $this->availableupdates = $this->pluginman->load_available_updates_for_plugin($this->component);
+        }
+
         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
+            $this->availableupdates = array();
             return null;
         }
 
@@ -584,13 +585,4 @@ abstract class base {
             'return' => $return,
         ));
     }
-
-    /**
-     * Provides access to the core_plugin_manager singleton.
-     *
-     * @return core_plugin_manager
-     */
-    protected function get_plugin_manager() {
-        return core_plugin_manager::instance();
-    }
 }
index d0d161d..5cbd7fb 100644 (file)
@@ -71,12 +71,14 @@ class format extends base {
      * @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
+     * @param core_plugin_manager $pluginman the plugin manager calling this method
      * @return array of plugintype classes, indexed by the plugin name
      */
-    public static function get_plugins($type, $typerootdir, $typeclass) {
+    public static function get_plugins($type, $typerootdir, $typeclass, $pluginman) {
         global $CFG;
-        $formats = parent::get_plugins($type, $typerootdir, $typeclass);
         require_once($CFG->dirroot.'/course/lib.php');
+
+        $formats = parent::get_plugins($type, $typerootdir, $typeclass, $pluginman);
         $order = get_sorted_course_formats();
         $sortedformats = array();
         foreach ($order as $formatname) {
@@ -137,7 +139,7 @@ class format extends base {
             return '';
         }
 
-        $defaultformat = $this->get_plugin_manager()->plugin_name('format_'.get_config('moodlecourse', 'format'));
+        $defaultformat = $this->pluginman->plugin_name('format_'.get_config('moodlecourse', 'format'));
         $message = get_string(
             'formatuninstallwithcourses', 'core_admin',
             (object)array('count' => $coursecount, 'format' => $this->displayname,
index ec78d39..09f12d6 100644 (file)
@@ -64,12 +64,12 @@ class orphaned extends base {
      * @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
+     * @param core_plugin_manager $pluginman the plugin manager calling this method
      * @return array of plugintype classes, indexed by the plugin name
      */
-    public static function get_plugins($type, $typerootdir, $typeclass) {
+    public static function get_plugins($type, $typerootdir, $typeclass, $pluginman) {
         $return = array();
-        $manager = \core_plugin_manager::instance();
-        $plugins = $manager->get_installed_plugins($type);
+        $plugins = $pluginman->get_installed_plugins($type);
 
         foreach ($plugins as $name => $version) {
             $plugin              = new $typeclass();
@@ -79,6 +79,7 @@ class orphaned extends base {
             $plugin->rootdir     = null;
             $plugin->displayname = $name;
             $plugin->versiondb   = $version;
+            $plugin->pluginman   = $pluginman;
             $plugin->init_is_standard();
 
             $return[$name] = $plugin;
diff --git a/lib/classes/update/api.php b/lib/classes/update/api.php
new file mode 100644 (file)
index 0000000..2901da2
--- /dev/null
@@ -0,0 +1,328 @@
+<?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/>.
+
+/**
+ * The class \core\update\api is defined here.
+ *
+ * @package     core
+ * @copyright   2015 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\update;
+
+use curl;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/filelib.php');
+
+/**
+ * General purpose client for https://download.moodle.org/api/
+ *
+ * The API provides proxy access to public information about plugins available
+ * in the Moodle Plugins directory. It is used when we are checking for
+ * updates, resolving missing dependecies or installing a plugin. This client
+ * can be used to:
+ *
+ * - obtain information about particular plugin version
+ * - locate the most suitable plugin version for the given Moodle branch
+ *
+ * TODO:
+ *
+ * - Convert \core\update\checker to use this client too, so that we have a
+ *   single access point for all the API services.
+ * - Implement client method for pluglist.php even if it is not actually
+ *   used by the Moodle core.
+ *
+ * @copyright 2015 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class api {
+
+    /** The root of the standard API provider */
+    const APIROOT = 'https://download.moodle.org/api';
+
+    /** The API version to be used by this client */
+    const APIVER = '1.3';
+
+    /**
+     * Factory method returning an instance of the class.
+     *
+     * @return \core\update\api client instance
+     */
+    public static function client() {
+        return new static();
+    }
+
+    /**
+     * Constructor is protected, use the factory method.
+     */
+    protected function __construct() {
+    }
+
+    /**
+     * Returns info about the particular plugin version in the plugins directory.
+     *
+     * Uses pluginfo.php end-point to find the given plugin version in the
+     * Moodle plugins directory. This is typically used to handle the
+     * installation request coming from the plugins directory (aka clicking the
+     * "Install" button there).
+     *
+     * 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 requested plugin version.  The
+     * ->version property is false if the requested version of the plugin was
+     * not found (yet the plugin itself is known).
+     *
+     * @param string $component frankenstyle name of the plugin
+     * @param int $version plugin version as declared via $plugin->version in its version.php
+     * @return \core\update\remote_info|bool
+     */
+    public function get_plugin_info($component, $version) {
+
+        $params = array(
+            'plugin' => $component.'@'.$version,
+            'format' => 'json',
+        );
+
+        return $this->call_pluginfo_service($params);
+    }
+
+    /**
+     * Locate the given plugin in the plugin directory.
+     *
+     * Uses pluginfo.php end-point to find a plugin with the given component
+     * name, that suits best for the given Moodle core branch. Minimal required
+     * plugin version can be specified. This is typically used for resolving
+     * dependencies.
+     *
+     * False is returned on error, or if there is no plugin with such component
+     * name found in the plugins directory via the API.
+     *
+     * 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).
+     *
+     * @param string $component frankenstyle name of the plugin
+     * @param string|int $reqversion minimal required version of the plugin, defaults to ANY_VERSION
+     * @param int $branch moodle core branch such as 29, 30, 31 etc, defaults to $CFG->branch
+     * @return \core\update\remote_info|bool
+     */
+    public function find_plugin($component, $reqversion=ANY_VERSION, $branch=null) {
+        global $CFG;
+
+        $params = array(
+            'plugin' => $component,
+            'format' => 'json',
+        );
+
+        if ($reqversion === ANY_VERSION) {
+            $params['minversion'] = 0;
+        } else {
+            $params['minversion'] = $reqversion;
+        }
+
+        if ($branch === null) {
+            $branch = $CFG->branch;
+        }
+
+        $params['branch'] = $this->convert_branch_numbering_format($branch);
+
+        return $this->call_pluginfo_service($params);
+    }
+
+    /**
+     * Makes sure the given data format match the expected output of the pluginfo service.
+     *
+     * Object validated by this method is guaranteed to contain all the data
+     * provided by the pluginfo.php version this client works with (self::APIVER).
+     *
+     * @param stdClass $data
+     * @return \core\update\remote_info|bool false if data are not valid, original data otherwise
+     */
+    public function validate_pluginfo_format($data) {
+
+        if (empty($data) or !is_object($data)) {
+            return false;
+        }
+
+        $output = new remote_info();
+
+        $rootproperties = array('id' => 1, 'name' => 1, 'component' => 1, 'source' => 0, 'doc' => 0,
+            'bugs' => 0, 'discussion' => 0, 'version' => 0);
+        foreach ($rootproperties as $property => $required) {
+            if (!property_exists($data, $property)) {
+                return false;
+            }
+            if ($required and empty($data->$property)) {
+                return false;
+            }
+            $output->$property = $data->$property;
+        }
+
+        if (!empty($data->version)) {
+            if (!is_object($data->version)) {
+                return false;
+            }
+            $versionproperties = array('id' => 1, 'version' => 1, 'release' => 0, 'maturity' => 0,
+                'downloadurl' => 1, 'downloadmd5' => 1, 'vcssystem' => 0, 'vcssystemother' => 0,
+                'vcsrepositoryurl' => 0, 'vcsbranch' => 0, 'vcstag' => 0, 'supportedmoodles' => 0);
+            foreach ($versionproperties as $property => $required) {
+                if (!property_exists($data->version, $property)) {
+                    return false;
+                }
+                if ($required and empty($data->version->$property)) {
+                    return false;
+                }
+            }
+            if (!preg_match('|^https?://|i', $data->version->downloadurl)) {
+                return false;
+            }
+
+            if (!empty($data->version->supportedmoodles)) {
+                if (!is_array($data->version->supportedmoodles)) {
+                    return false;
+                }
+                foreach ($data->version->supportedmoodles as $supportedmoodle) {
+                    if (!is_object($supportedmoodle)) {
+                        return false;
+                    }
+                    if (empty($supportedmoodle->version) or empty($supportedmoodle->release)) {
+                        return false;
+                    }
+                }
+            }
+        }
+
+        return $output;
+    }
+
+    /**
+     * Calls the pluginfo.php end-point with given parameters.
+     *
+     * @param array $params
+     * @return \core\update\remote_info|bool
+     */
+    protected function call_pluginfo_service(array $params) {
+
+        $serviceurl = $this->get_serviceurl_pluginfo();
+        $response = $this->call_service($serviceurl, $params);
+
+        if ($response) {
+            if ($response->info['http_code'] == 404) {
+                // There is no such plugin found in the plugins directory.
+                return false;
+
+            } else if ($response->info['http_code'] == 200 and isset($response->data->status)
+                    and $response->data->status === 'OK' and $response->data->apiver == self::APIVER
+                    and isset($response->data->pluginfo)) {
+                    return $this->validate_pluginfo_format($response->data->pluginfo);
+
+            } else {
+                debugging('cURL: Unexpected response', DEBUG_DEVELOPER);
+                return false;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Calls the given end-point service with the given parameters.
+     *
+     * Returns false on cURL error and/or SSL verification failure. Otherwise
+     * an object with the response, cURL info and HTTP status message is
+     * returned.
+     *
+     * @param string $serviceurl
+     * @param array $params
+     * @return stdClass|bool
+     */
+    protected function call_service($serviceurl, array $params=array()) {
+
+        $response = (object)array(
+            'data' => null,
+            'info' => null,
+            'status' => null,
+        );
+
+        $curl = new curl();
+
+        $response->data = json_decode($curl->get($serviceurl, $params, array(
+            'CURLOPT_SSL_VERIFYHOST' => 2,
+            'CURLOPT_SSL_VERIFYPEER' => true,
+        )));
+
+        $curlerrno = $curl->get_errno();
+
+        if (!empty($curlerrno)) {
+            debugging('cURL: Error '.$curlerrno.' when calling '.$serviceurl, DEBUG_DEVELOPER);
+            return false;
+        }
+
+        $response->info = $curl->get_info();
+
+        if (isset($response->info['ssl_verify_result']) and $response->info['ssl_verify_result'] != 0) {
+            debugging('cURL/SSL: Unable to verify remote service response when calling '.$serviceurl, DEBUG_DEVELOPER);
+            return false;
+        }
+
+        // The first response header with the HTTP status code and reason phrase.
+        $response->status = array_shift($curl->response);
+
+        return $response;
+    }
+
+    /**
+     * Converts the given branch from XY format to the X.Y format
+     *
+     * The syntax of $CFG->branch uses the XY format that suits the Moodle docs
+     * versioning and stable branches numbering scheme. The API at
+     * download.moodle.org uses the X.Y numbering scheme.
+     *
+     * @param int $branch moodle branch in the XY format (e.g. 29, 30, 31 etc)
+     * @return string moodle branch in the X.Y format (e.g. 2.9, 3.0, 3.1 etc)
+     */
+    protected function convert_branch_numbering_format($branch) {
+
+        $branch = (string)$branch;
+
+        if (strpos($branch, '.') === false) {
+            $branch = substr($branch, 0, -1).'.'.substr($branch, -1);
+        }
+
+        return $branch;
+    }
+
+    /**
+     * Returns URL of the pluginfo.php API end-point.
+     *
+     * @return string
+     */
+    protected function get_serviceurl_pluginfo() {
+        global $CFG;
+
+        if (!empty($CFG->config_php_settings['alternativepluginfoserviceurl'])) {
+            return $CFG->config_php_settings['alternativepluginfoserviceurl'];
+        } else {
+            return self::APIROOT.'/'.self::APIVER.'/pluginfo.php';
+        }
+    }
+}
index 9b211ef..b5c701a 100644 (file)
@@ -82,15 +82,19 @@ class checker {
     }
 
     /**
-     * Is automatic deployment enabled?
+     * Is checking for available updates enabled?
+     *
+     * The feature is enabled unless it is prohibited via config.php.
+     * If enabled, the button for manual checking for available updates is
+     * displayed at admin screens. To perform scheduled checks for updates
+     * automatically, the admin setting $CFG->updateautocheck has to be enabled.
      *
      * @return bool
      */
     public function enabled() {
         global $CFG;
 
-        // The feature can be prohibited via config.php.
-        return empty($CFG->disableupdateautodeploy);
+        return empty($CFG->disableupdatenotifications);
     }
 
     /**
@@ -192,7 +196,7 @@ class checker {
     public function cron() {
         global $CFG;
 
-        if (!$this->cron_autocheck_enabled()) {
+        if (!$this->enabled() or !$this->cron_autocheck_enabled()) {
             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
             return;
         }
@@ -262,7 +266,7 @@ class checker {
             throw new checker_exception('err_response_status', $response['status']);
         }
 
-        if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
+        if (empty($response['apiver']) or $response['apiver'] !== '1.3') {
             throw new checker_exception('err_response_format_version', $response['apiver']);
         }
 
@@ -409,7 +413,7 @@ class checker {
         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
             return $CFG->config_php_settings['alternativeupdateproviderurl'];
         } else {
-            return 'https://download.moodle.org/api/1.2/updates.php';
+            return 'https://download.moodle.org/api/1.3/updates.php';
         }
     }
 
diff --git a/lib/classes/update/code_manager.php b/lib/classes/update/code_manager.php
new file mode 100644 (file)
index 0000000..e18715f
--- /dev/null
@@ -0,0 +1,492 @@
+<?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 core\update\code_manager class.
+ *
+ * @package     core_plugin
+ * @copyright   2012, 2013, 2015 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\update;
+
+use core_component;
+use coding_exception;
+use SplFileInfo;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/filelib.php');
+
+/**
+ * General purpose class managing the plugins source code files deployment
+ *
+ * The class is able and supposed to
+ * - fetch and cache ZIP files distributed via the Moodle Plugins directory
+ * - unpack the ZIP files in a temporary storage
+ * - archive existing version of the plugin source code
+ * - move (deploy) the plugin source code into the $CFG->dirroot
+ *
+ * @copyright 2015 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class code_manager {
+
+    /** @var string full path to the Moodle app directory root */
+    protected $dirroot;
+    /** @var string full path to the temp directory root */
+    protected $temproot;
+
+    /**
+     * Instantiate the class instance
+     *
+     * @param string $dirroot full path to the moodle app directory root
+     * @param string $temproot full path to our temp directory
+     */
+    public function __construct($dirroot=null, $temproot=null) {
+        global $CFG;
+
+        if (empty($dirroot)) {
+            $dirroot = $CFG->dirroot;
+        }
+
+        if (empty($temproot)) {
+            // Note we are using core_plugin here as that is the valid core
+            // subsystem we are part of. The namespace of this class (core\update)
+            // does not match it for legacy reasons.  The data stored in the
+            // temp directory are expected to survive multiple requests and
+            // purging caches during the upgrade, so we make use of
+            // make_temp_directory(). The contents of it can be removed if needed,
+            // given the site is in the maintenance mode (so that cron is not
+            // executed) and the site is not being upgraded.
+            $temproot = make_temp_directory('core_plugin/code_manager');
+        }
+
+        $this->dirroot = $dirroot;
+        $this->temproot = $temproot;
+
+        $this->init_temp_directories();
+    }
+
+    /**
+     * Obtain the plugin ZIP file from the given URL
+     *
+     * The caller is supposed to know both downloads URL and the MD5 hash of
+     * the ZIP contents in advance, typically by using the API requests against
+     * the plugins directory.
+     *
+     * @param string $url
+     * @param string $md5
+     * @return string|bool full path to the file, false on error
+     */
+    public function get_remote_plugin_zip($url, $md5) {
+
+        // Sanitize and validate the URL.
+        $url = str_replace(array("\r", "\n"), '', $url);
+
+        if (!preg_match('|^https?://|i', $url)) {
+            $this->debug('Error fetching plugin ZIP: unsupported transport protocol: '.$url);
+            return false;
+        }
+
+        // The cache location for the file.
+        $distfile = $this->temproot.'/distfiles/'.$md5.'.zip';
+
+        if (is_readable($distfile) and md5_file($distfile) === $md5) {
+            return $distfile;
+        } else {
+            @unlink($distfile);
+        }
+
+        // Download the file into a temporary location.
+        $tempdir = make_request_directory();
+        $tempfile = $tempdir.'/plugin.zip';
+        $result = $this->download_plugin_zip_file($url, $tempfile);
+
+        if (!$result) {
+            return false;
+        }
+
+        $actualmd5 = md5_file($tempfile);
+
+        // Make sure the actual md5 hash matches the expected one.
+        if ($actualmd5 !== $md5) {
+            $this->debug('Error fetching plugin ZIP: md5 mismatch.');
+            return false;
+        }
+
+        // If the file is empty, something went wrong.
+        if ($actualmd5 === 'd41d8cd98f00b204e9800998ecf8427e') {
+            return false;
+        }
+
+        // Store the file in our cache.
+        if (!rename($tempfile, $distfile)) {
+            return false;
+        }
+
+        return $distfile;
+    }
+
+    /**
+     * Extracts the saved plugin ZIP file.
+     *
+     * Returns the list of files found in the ZIP. The format of that list is
+     * array of (string)filerelpath => (bool|string) where the array value is
+     * either true or a string describing the problematic file.
+     *
+     * @see zip_packer::extract_to_pathname()
+     * @param string $zipfilepath full path to the saved ZIP file
+     * @param string $targetdir full path to the directory to extract the ZIP file to
+     * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
+     * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
+     */
+    public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
+
+        $fp = get_file_packer('application/zip');
+        $files = $fp->extract_to_pathname($zipfilepath, $targetdir);
+
+        if (!$files) {
+            return array();
+        }
+
+        if (!empty($rootdir)) {
+            $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
+        }
+
+        // Sometimes zip may not contain all parent directories, add them to make it consistent.
+        foreach ($files as $path => $status) {
+            if ($status !== true) {
+                continue;
+            }
+            $parts = explode('/', trim($path, '/'));
+            while (array_pop($parts)) {
+                if (empty($parts)) {
+                    break;
+                }
+                $dir = implode('/', $parts).'/';
+                if (!isset($files[$dir])) {
+                    $files[$dir] = true;
+                }
+            }
+        }
+
+        return $files;
+    }
+
+    /**
+     * Make an archive backup of the existing plugin folder.
+     *
+     * @param string $folderpath full path to the plugin folder
+     * @param string $targetzip full path to the zip file to be created
+     * @return bool true if file created, false if not
+     */
+    public function zip_plugin_folder($folderpath, $targetzip) {
+
+        if (file_exists($targetzip)) {
+            throw new coding_exception('Attempting to create already existing ZIP file', $targetzip);
+        }
+
+        if (!is_writable(dirname($targetzip))) {
+            throw new coding_exception('Target ZIP location not writable', dirname($targetzip));
+        }
+
+        if (!is_dir($folderpath)) {
+            throw new coding_exception('Attempting to ZIP non-existing source directory', $folderpath);
+        }
+
+        $files = $this->list_plugin_folder_files($folderpath);
+        $fp = get_file_packer('application/zip');
+        return $fp->archive_to_pathname($files, $targetzip, false);
+    }
+
+    /**
+     * Archive the current plugin on-disk version.
+     *
+     * @param string $folderpath full path to the plugin folder
+     * @param string $component
+     * @param int $version
+     * @param bool $overwrite overwrite existing archive if found
+     * @return bool
+     */
+    public function archive_plugin_version($folderpath, $component, $version, $overwrite=false) {
+
+        if ($component !== clean_param($component, PARAM_SAFEDIR)) {
+            // This should never happen, but just in case.
+            throw new moodle_exception('unexpected_plugin_component_format', 'core_plugin', '', null, $component);
+        }
+
+        if ((string)$version !== clean_param((string)$version, PARAM_FILE)) {
+            // Prevent some nasty injections via $plugin->version tricks.
+            throw new moodle_exception('unexpected_plugin_version_format', 'core_plugin', '', null, $version);
+        }
+
+        if (empty($component) or empty($version)) {
+            return false;
+        }
+
+        if (!is_dir($folderpath)) {
+            return false;
+        }
+
+        $archzip = $this->temproot.'/archive/'.$component.'/'.$version.'.zip';
+
+        if (file_exists($archzip) and !$overwrite) {
+            return true;
+        }
+
+        $tmpzip = make_request_directory().'/'.$version.'.zip';
+        $zipped = $this->zip_plugin_folder($folderpath, $tmpzip);
+
+        if (!$zipped) {
+            return false;
+        }
+
+        // Assert that the file looks like a valid one.
+        list($expectedtype, $expectedname) = core_component::normalize_component($component);
+        $actualname = $this->get_plugin_zip_root_dir($tmpzip);
+        if ($actualname !== $expectedname) {
+            // This should not happen.
+            throw new moodle_exception('unexpected_archive_structure', 'core_plugin');
+        }
+
+        make_writable_directory(dirname($archzip));
+        return rename($tmpzip, $archzip);
+    }
+
+    /**
+     * Return the path to the ZIP file with the archive of the given plugin version.
+     *
+     * @param string $component
+     * @param int $version
+     * @return string|bool false if not found, full path otherwise
+     */
+    public function get_archived_plugin_version($component, $version) {
+
+        if (empty($component) or empty($version)) {
+            return false;
+        }
+
+        $archzip = $this->temproot.'/archive/'.$component.'/'.$version.'.zip';
+
+        if (file_exists($archzip)) {
+            return $archzip;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns list of all files in the given directory.
+     *
+     * Given a path like /full/path/to/mod/workshop, it returns array like
+     *
+     *  [workshop/] => /full/path/to/mod/workshop
+     *  [workshop/lang/] => /full/path/to/mod/workshop/lang
+     *  [workshop/lang/workshop.php] => /full/path/to/mod/workshop/lang/workshop.php
+     *  ...
+     *
+     * Which mathes the format used by Moodle file packers.
+     *
+     * @param string $folderpath full path to the plugin directory
+     * @return array (string)relpath => (string)fullpath
+     */
+    public function list_plugin_folder_files($folderpath) {
+
+        $folder = new RecursiveDirectoryIterator($folderpath);
+        $iterator = new RecursiveIteratorIterator($folder);
+        $folderpathinfo = new SplFileInfo($folderpath);
+        $strip = strlen($folderpathinfo->getPathInfo()->getRealPath()) + 1;
+        $files = array();
+        foreach ($iterator as $fileinfo) {
+            if ($fileinfo->getFilename() === '..') {
+                continue;
+            }
+            if (strpos($fileinfo->getRealPath(), $folderpathinfo->getRealPath() !== 0)) {
+                throw new moodle_exception('unexpected_filepath_mismatch', 'core_plugin');
+            }
+            $key = substr($fileinfo->getRealPath(), $strip);
+            if ($fileinfo->isDir() and substr($key, -1) !== '/') {
+                $key .= '/';
+            }
+            $files[$key] = $fileinfo->getRealPath();
+        }
+        return $files;
+    }
+
+    /**
+     * Detects the plugin's name from its ZIP file.
+     *
+     * Plugin ZIP packages are expected to contain a single directory and the
+     * directory name would become the plugin name once extracted to the Moodle
+     * dirroot.
+     *
+     * @param string $zipfilepath full path to the ZIP files
+     * @return string|bool false on error
+     */
+    public function get_plugin_zip_root_dir($zipfilepath) {
+
+        $fp = get_file_packer('application/zip');
+        $files = $fp->list_files($zipfilepath);
+
+        if (empty($files)) {
+            return false;
+        }
+
+        $rootdirname = null;
+        foreach ($files as $file) {
+            $pathnameitems = explode('/', $file->pathname);
+            if (empty($pathnameitems)) {
+                return false;
+            }
+            // Set the expected name of the root directory in the first
+            // iteration of the loop.
+            if ($rootdirname === null) {
+                $rootdirname = $pathnameitems[0];
+            }
+            // Require the same root directory for all files in the ZIP
+            // package.
+            if ($rootdirname !== $pathnameitems[0]) {
+                return false;
+            }
+        }
+
+        return $rootdirname;
+    }
+
+    // This is the end, my only friend, the end ... of external public API.
+
+    /**
+     * Makes sure all temp directories exist and are writable.
+     */
+    protected function init_temp_directories() {
+        make_writable_directory($this->temproot.'/distfiles');
+        make_writable_directory($this->temproot.'/archive');
+    }
+
+    /**
+     * Raise developer debugging level message.
+     *
+     * @param string $msg
+     */
+    protected function debug($msg) {
+        debugging($msg, DEBUG_DEVELOPER);
+    }
+
+    /**
+     * Download the ZIP file with the plugin package from the given location
+     *
+     * @param string $url URL to the file
+     * @param string $tofile full path to where to store the downloaded file
+     * @return bool false on error
+     */
+    protected function download_plugin_zip_file($url, $tofile) {
+
+        if (file_exists($tofile)) {
+            $this->debug('Error fetching plugin ZIP: target location exists.');
+            return false;
+        }
+
+        $status = $this->download_file_content($url, $tofile);
+
+        if (!$status) {
+            $this->debug('Error fetching plugin ZIP.');
+            @unlink($tofile);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Thin wrapper for the core's download_file_content() function.
+     *
+     * @param string $url URL to the file
+     * @param string $tofile full path to where to store the downloaded file
+     * @return bool
+     */
+    protected function download_file_content($url, $tofile) {
+
+        // Prepare the parameters for the download_file_content() function.
+        $headers = null;
+        $postdata = null;
+        $fullresponse = false;
+        $timeout = 300;
+        $connecttimeout = 20;
+        $skipcertverify = false;
+        $tofile = $tofile;
+        $calctimeout = false;
+
+        return download_file_content($url, $headers, $postdata, $fullresponse, $timeout,
+            $connecttimeout, $skipcertverify, $tofile, $calctimeout);
+    }
+
+    /**
+     * Renames the root directory of the extracted ZIP package.
+     *
+     * This method does not validate the presence of the single root directory
+     * (it is the validator's duty). It just searches for the first directory
+     * under the given location and renames it.
+     *
+     * The method will not rename the root if the requested location already
+     * exists.
+     *
+     * @param string $dirname fullpath location of the extracted ZIP package
+     * @param string $rootdir the requested name of the root directory
+     * @param array $files list of extracted files
+     * @return array eventually amended list of extracted files
+     */
+    protected function rename_extracted_rootdir($dirname, $rootdir, array $files) {
+
+        if (!is_dir($dirname)) {
+            $this->debug('Unable to rename rootdir of non-existing content');
+            return $files;
+        }
+
+        if (file_exists($dirname.'/'.$rootdir)) {
+            // This typically means the real root dir already has the $rootdir name.
+            return $files;
+        }
+
+        $found = null; // The name of the first subdirectory under the $dirname.
+        foreach (scandir($dirname) as $item) {
+            if (substr($item, 0, 1) === '.') {
+                continue;
+            }
+            if (is_dir($dirname.'/'.$item)) {
+                $found = $item;
+                break;
+            }
+        }
+
+        if (!is_null($found)) {
+            if (rename($dirname.'/'.$found, $dirname.'/'.$rootdir)) {
+                $newfiles = array();
+                foreach ($files as $filepath => $status) {
+                    $newpath = preg_replace('~^'.preg_quote($found.'/').'~', preg_quote($rootdir.'/'), $filepath);
+                    $newfiles[$newpath] = $status;
+                }
+                return $newfiles;
+            }
+        }
+
+        return $files;
+    }
+
+}
diff --git a/lib/classes/update/deployer.php b/lib/classes/update/deployer.php
deleted file mode 100644 (file)
index 25ba168..0000000
+++ /dev/null
@@ -1,574 +0,0 @@
-<?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/>.
-
-/**
- * Defines classes used for updates.
- *
- * @package    core
- * @copyright  2011 David Mudrak <david@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-namespace core\update;
-
-use coding_exception, core_component, moodle_url;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Implements a communication bridge to the mdeploy.php utility
- */
-class deployer {
-
-    /** @var \core\update\deployer holds the singleton instance */
-    protected static $singletoninstance;
-    /** @var moodle_url URL of a page that includes the deployer UI */
-    protected $callerurl;
-    /** @var moodle_url URL to return after the deployment */
-    protected $returnurl;
-
-    /**
-     * Direct instantiation not allowed, use the factory method {@link self::instance()}
-     */
-    protected function __construct() {
-    }
-
-    /**
-     * Sorry, this is singleton
-     */
-    protected function __clone() {
-    }
-
-    /**
-     * Factory method for this class
-     *
-     * @return \core\update\deployer the singleton instance
-     */
-    public static function instance() {
-        if (is_null(self::$singletoninstance)) {
-            self::$singletoninstance = new self();
-        }
-        return self::$singletoninstance;
-    }
-
-    /**
-     * Reset caches used by this script
-     *
-     * @param bool $phpunitreset is this called as a part of PHPUnit reset?
-     */
-    public static function reset_caches($phpunitreset = false) {
-        if ($phpunitreset) {
-            self::$singletoninstance = null;
-        }
-    }
-
-    /**
-     * Is automatic deployment enabled?
-     *
-     * @return bool
-     */
-    public function enabled() {
-        global $CFG;
-
-        if (!empty($CFG->disableupdateautodeploy)) {
-            // The feature is prohibited via config.php.
-            return false;
-        }
-
-        return get_config('updateautodeploy');
-    }
-
-    /**
-     * Sets some base properties of the class to make it usable.
-     *
-     * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
-     * @param moodle_url $returnurl the final URL to return to when the deployment is finished
-     */
-    public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
-
-        if (!$this->enabled()) {
-            throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
-        }
-
-        $this->callerurl = $callerurl;
-        $this->returnurl = $returnurl;
-    }
-
-    /**
-     * Has the deployer been initialized?
-     *
-     * Initialized deployer means that the following properties were set:
-     * callerurl, returnurl
-     *
-     * @return bool
-     */
-    public function initialized() {
-
-        if (!$this->enabled()) {
-            return false;
-        }
-
-        if (empty($this->callerurl)) {
-            return false;
-        }
-
-        if (empty($this->returnurl)) {
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns a list of reasons why the deployment can not happen
-     *
-     * If the returned array is empty, the deployment seems to be possible. The returned
-     * structure is an associative array with keys representing individual impediments.
-     * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
-     *
-     * @param \core\update\info $info
-     * @return array
-     */
-    public function deployment_impediments(info $info) {
-
-        $impediments = array();
-
-        if (empty($info->download)) {
-            $impediments['missingdownloadurl'] = true;
-        }
-
-        if (empty($info->downloadmd5)) {
-            $impediments['missingdownloadmd5'] = true;
-        }
-
-        if (!empty($info->download) and !$this->update_downloadable($info->download)) {
-            $impediments['notdownloadable'] = true;
-        }
-
-        if (!$this->component_writable($info->component)) {
-            $impediments['notwritable'] = true;
-        }
-
-        return $impediments;
-    }
-
-    /**
-     * Check to see if the current version of the plugin seems to be a checkout of an external repository.
-     *
-     * @see core_plugin_manager::plugin_external_source()
-     * @param \core\update\info $info
-     * @return false|string
-     */
-    public function plugin_external_source(info $info) {
-
-        $paths = core_component::get_plugin_types();
-        list($plugintype, $pluginname) = core_component::normalize_component($info->component);
-        $pluginroot = $paths[$plugintype].'/'.$pluginname;
-
-        if (is_dir($pluginroot.'/.git')) {
-            return 'git';
-        }
-
-        if (is_file($pluginroot.'/.git')) {
-            return 'git-submodule';
-        }
-
-        if (is_dir($pluginroot.'/CVS')) {
-            return 'cvs';
-        }
-
-        if (is_dir($pluginroot.'/.svn')) {
-            return 'svn';
-        }
-
-        if (is_dir($pluginroot.'/.hg')) {
-            return 'mercurial';
-        }
-
-        return false;
-    }
-
-    /**
-     * Prepares a renderable widget to confirm installation of an available update.
-     *
-     * @param \core\update\info $info component version to deploy
-     * @return \renderable
-     */
-    public function make_confirm_widget(info $info) {
-
-        if (!$this->initialized()) {
-            throw new coding_exception('Illegal method call - deployer not initialized.');
-        }
-
-        $params = array(
-            'updateaddon' => $info->component,
-            'version' =>$info->version,
-            'sesskey' => sesskey(),
-        );
-
-        // Append some our own data.
-        if (!empty($this->callerurl)) {
-            $params['callerurl'] = $this->callerurl->out(false);
-        }
-        if (!empty($this->returnurl)) {
-            $params['returnurl'] = $this->returnurl->out(false);
-        }
-
-        $widget = new \single_button(
-            new moodle_url($this->callerurl, $params),
-            get_string('updateavailableinstall', 'core_admin'),
-            'post'
-        );
-
-        return $widget;
-    }
-
-    /**
-     * Prepares a renderable widget to execute installation of an available update.
-     *
-     * @param \core\update\info $info component version to deploy
-     * @param moodle_url $returnurl URL to return after the installation execution
-     * @return \renderable
-     */
-    public function make_execution_widget(info $info, moodle_url $returnurl = null) {
-        global $CFG;
-
-        if (!$this->initialized()) {
-            throw new coding_exception('Illegal method call - deployer not initialized.');
-        }
-
-        $pluginrootpaths = core_component::get_plugin_types();
-
-        list($plugintype, $pluginname) = core_component::normalize_component($info->component);
-
-        if (empty($pluginrootpaths[$plugintype])) {
-            throw new coding_exception('Unknown plugin type root location', $plugintype);
-        }
-
-        list($passfile, $password) = $this->prepare_authorization();
-
-        if (is_null($returnurl)) {
-            $returnurl = new moodle_url('/admin');
-        } else {
-            $returnurl = $returnurl;
-        }
-
-        $params = array(
-            'upgrade' => true,
-            'type' => $plugintype,
-            'name' => $pluginname,
-            'typeroot' => $pluginrootpaths[$plugintype],
-            'package' => $info->download,
-            'md5' => $info->downloadmd5,
-            'dataroot' => $CFG->dataroot,
-            'dirroot' => $CFG->dirroot,
-            'passfile' => $passfile,
-            'password' => $password,
-            'returnurl' => $returnurl->out(false),
-        );
-
-        if (!empty($CFG->proxyhost)) {
-            // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
-            // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
-            // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
-            // fixed, the condition should be amended.
-            if (true or !is_proxybypass($info->download)) {
-                if (empty($CFG->proxyport)) {
-                    $params['proxy'] = $CFG->proxyhost;
-                } else {
-                    $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
-                }
-
-                if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
-                    $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
-                }
-
-                if (!empty($CFG->proxytype)) {
-                    $params['proxytype'] = $CFG->proxytype;
-                }
-            }
-        }
-
-        $widget = new \single_button(
-            new moodle_url('/mdeploy.php', $params),
-            get_string('updateavailableinstall', 'core_admin'),
-            'post'
-        );
-
-        return $widget;
-    }
-
-    /**
-     * Returns array of data objects passed to this tool.
-     *
-     * @return array
-     */
-    public function submitted_data() {
-        $component = optional_param('updateaddon', '', PARAM_COMPONENT);
-        $version = optional_param('version', '', PARAM_RAW);
-        if (!$component or !$version) {
-            return false;
-        }
-
-        $plugininfo = \core_plugin_manager::instance()->get_plugin_info($component);
-        if (!$plugininfo) {
-            return false;
-        }
-
-        if ($plugininfo->is_standard()) {
-            return false;
-        }
-
-        if (!$updates = $plugininfo->available_updates()) {
-            return false;
-        }
-
-        $info = null;
-        foreach ($updates as $update) {
-            if ($update->version == $version) {
-                $info = $update;
-                break;
-            }
-        }
-        if (!$info) {
-            return false;
-        }
-
-        $data = array(
-            'updateaddon' => $component,
-            'updateinfo'  => $info,
-            'callerurl'   => optional_param('callerurl', null, PARAM_URL),
-            'returnurl'   => optional_param('returnurl', null, PARAM_URL),
-        );
-        if ($data['callerurl']) {
-            $data['callerurl'] = new moodle_url($data['callerurl']);
-        }
-        if ($data['callerurl']) {
-            $data['returnurl'] = new moodle_url($data['returnurl']);
-        }
-
-        return $data;
-    }
-
-    /**
-     * Handles magic getters and setters for protected properties.
-     *
-     * @param string $name method name, e.g. set_returnurl()
-     * @param array $arguments arguments to be passed to the array
-     */
-    public function __call($name, array $arguments = array()) {
-
-        if (substr($name, 0, 4) === 'set_') {
-            $property = substr($name, 4);
-            if (empty($property)) {
-                throw new coding_exception('Invalid property name (empty)');
-            }
-            if (empty($arguments)) {
-                $arguments = array(true); // Default value for flag-like properties.
-            }
-            // Make sure it is a protected property.
-            $isprotected = false;
-            $reflection = new \ReflectionObject($this);
-            foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
-                if ($reflectionproperty->getName() === $property) {
-                    $isprotected = true;
-                    break;
-                }
-            }
-            if (!$isprotected) {
-                throw new coding_exception('Unable to set property - it does not exist or it is not protected');
-            }
-            $value = reset($arguments);
-            $this->$property = $value;
-            return;
-        }
-
-        if (substr($name, 0, 4) === 'get_') {
-            $property = substr($name, 4);
-            if (empty($property)) {
-                throw new coding_exception('Invalid property name (empty)');
-            }
-            if (!empty($arguments)) {
-                throw new coding_exception('No parameter expected');
-            }
-            // Make sure it is a protected property.
-            $isprotected = false;
-            $reflection = new \ReflectionObject($this);
-            foreach ($reflection->getProperties(\ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
-                if ($reflectionproperty->getName() === $property) {
-                    $isprotected = true;
-                    break;
-                }
-            }
-            if (!$isprotected) {
-                throw new coding_exception('Unable to get property - it does not exist or it is not protected');
-            }
-            return $this->$property;
-        }
-    }
-
-    /**
-     * Generates a random token and stores it in a file in moodledata directory.
-     *
-     * @return array of the (string)filename and (string)password in this order
-     */
-    public function prepare_authorization() {
-        global $CFG;
-
-        make_upload_directory('mdeploy/auth/');
-
-        $attempts = 0;
-        $success = false;
-
-        while (!$success and $attempts < 5) {
-            $attempts++;
-
-            $passfile = $this->generate_passfile();
-            $password = $this->generate_password();
-            $now = time();
-
-            $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
-
-            if (!file_exists($filepath)) {
-                $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
-                chmod($filepath, $CFG->filepermissions);
-            }
-        }
-
-        if ($success) {
-            return array($passfile, $password);
-
-        } else {
-            throw new \moodle_exception('unable_prepare_authorization', 'core_plugin');
-        }
-    }
-
-    /* === End of external API === */
-
-    /**
-     * Returns a random string to be used as a filename of the password storage.
-     *
-     * @return string
-     */
-    protected function generate_passfile() {
-        return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
-    }
-
-    /**
-     * Returns a random string to be used as the authorization token
-     *
-     * @return string
-     */
-    protected function generate_password() {
-        return complex_random_string();
-    }
-
-    /**
-     * Checks if the given component's directory is writable
-     *
-     * For the purpose of the deployment, the web server process has to have
-     * write access to all files in the component's directory (recursively) and for the
-     * directory itself.
-     *
-     * @see worker::move_directory_source_precheck()
-     * @param string $component normalized component name
-     * @return boolean
-     */
-    protected function component_writable($component) {
-
-        list($plugintype, $pluginname) = core_component::normalize_component($component);
-
-        $directory = core_component::get_plugin_directory($plugintype, $pluginname);
-
-        if (is_null($directory)) {
-            // Plugin unknown, most probably deleted or missing during upgrade,
-            // look at the parent directory instead because they might want to install it.
-            $plugintypes = core_component::get_plugin_types();
-            if (!isset($plugintypes[$plugintype])) {
-                throw new coding_exception('Unknown component location', $component);
-            }
-            $directory = $plugintypes[$plugintype];
-        }
-
-        return $this->directory_writable($directory);
-    }
-
-    /**
-     * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
-     *
-     * This is mainly supposed to check if the transmission over HTTPS would
-     * work. That is, if the CA certificates are present at the server.
-     *
-     * @param string $downloadurl the URL of the ZIP package to download
-     * @return bool
-     */
-    protected function update_downloadable($downloadurl) {
-        global $CFG;
-
-        $curloptions = array(
-            'CURLOPT_SSL_VERIFYHOST' => 2,      // This is the default in {@link curl} class but just in case.
-            'CURLOPT_SSL_VERIFYPEER' => true,
-        );
-
-        $curl = new \curl(array('proxy' => true));
-        $result = $curl->head($downloadurl, $curloptions);
-        $errno = $curl->get_errno();
-        if (empty($errno)) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Checks if the directory and all its contents (recursively) is writable
-     *
-     * @param string $path full path to a directory
-     * @return boolean
-     */
-    private function directory_writable($path) {
-
-        if (!is_writable($path)) {
-            return false;
-        }
-
-        if (is_dir($path)) {
-            $handle = opendir($path);
-        } else {
-            return false;
-        }
-
-        $result = true;
-
-        while ($filename = readdir($handle)) {
-            $filepath = $path.'/'.$filename;
-
-            if ($filename === '.' or $filename === '..') {
-                continue;
-            }
-
-            if (is_dir($filepath)) {
-                $result = $result && $this->directory_writable($filepath);
-
-            } else {
-                $result = $result && is_writable($filepath);
-            }
-        }
-
-        closedir($handle);
-
-        return $result;
-    }
-}
diff --git a/lib/classes/update/remote_info.php b/lib/classes/update/remote_info.php
new file mode 100644 (file)
index 0000000..ca85fea
--- /dev/null
@@ -0,0 +1,45 @@
+<?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 \core\update\remote_info class.
+ *
+ * @package     core_plugin
+ * @copyright   2015 David Mudrak <david@moodle.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\update;
+use stdClass;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Thin wrapper for data structures returned by {@link api::get_plugin_info()}
+ *
+ * Given that the API client returns instances of this class instead of pure
+ * objects allows us to have proper type hinting / declarations in method
+ * signatures. The validation of the data structure is happening in the API
+ * client so the rest of the code can simply rely on the class type.
+ *
+ * We extend the stdClass explicitly so that it can be eventually used in
+ * methods signatures, too (not recommended).
+ *
+ * @copyright 2015 David Mudrak <david@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class remote_info extends stdClass {
+}
similarity index 76%
rename from admin/tool/installaddon/classes/validator.php
rename to lib/classes/update/validator.php
index 0ae7097..7d0f997 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
  * Uses fragments of the local_plugins_archive_validator class copyrighted by
  * Marina Glancy that is part of the local_plugins plugin.
  *
- * @package     tool_installaddon
- * @subpackage  classes
- * @copyright   2013 David Mudrak <david@moodle.com>
+ * @package     core_plugin
+ * @subpackage  validation
+ * @copyright   2013, 2015 David Mudrak <david@moodle.com>
  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+namespace core\update;
+
+use core_component;
+use core_plugin_manager;
+use help_icon;
+use coding_exception;
+
 defined('MOODLE_INTERNAL') || die();
 
 if (!defined('T_ML_COMMENT')) {
-   define('T_ML_COMMENT', T_COMMENT);
+    define('T_ML_COMMENT', T_COMMENT);
 } else {
-   define('T_DOC_COMMENT', T_ML_COMMENT);
+    define('T_DOC_COMMENT', T_ML_COMMENT);
 }
 
 /**
  * Validates the contents of extracted plugin ZIP file
  *
- * @copyright 2013 David Mudrak <david@moodle.com>
+ * @copyright 2013, 2015 David Mudrak <david@moodle.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class tool_installaddon_validator {
+class validator {
 
     /** Critical error message level, causes the validation fail. */
     const ERROR     = 'error';
@@ -79,15 +85,12 @@ class tool_installaddon_validator {
     /** @var string|null the name of found English language file without the .php extension */
     protected $langfilename = null;
 
-    /** @var moodle_url|null URL to continue with the installation of validated add-on */
-    protected $continueurl = null;
-
     /**
      * Factory method returning instance of the validator
      *
      * @param string $zipcontentpath full path to the extracted ZIP contents
      * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error
-     * @return tool_installaddon_validator
+     * @return \core\update\validator
      */
     public static function instance($zipcontentpath, array $zipcontentfiles) {
         return new static($zipcontentpath, $zipcontentfiles);
@@ -158,13 +161,76 @@ class tool_installaddon_validator {
         return $this->messages;
     }
 
+    /**
+     * Returns human readable localised name of the given log level.
+     *
+     * @param string $level e.g. self::INFO
+     * @return string
+     */
+    public function message_level_name($level) {
+        return get_string('validationmsglevel_'.$level, 'core_plugin');
+    }
+
+    /**
+     * If defined, returns human readable validation code.
+     *
+     * Otherwise, it simply returns the code itself as a fallback.
+     *
+     * @param string $msgcode
+     * @return string
+     */
+    public function message_code_name($msgcode) {
+
+        $stringman = get_string_manager();
+
+        if ($stringman->string_exists('validationmsg_'.$msgcode, 'core_plugin')) {
+            return get_string('validationmsg_'.$msgcode, 'core_plugin');
+        }
+
+        return $msgcode;
+    }
+
+    /**
+     * Returns help icon for the message code if defined.
+     *
+     * @param string $msgcode
+     * @return \help_icon|false
+     */
+    public function message_help_icon($msgcode) {
+
+        $stringman = get_string_manager();
+
+        if ($stringman->string_exists('validationmsg_'.$msgcode.'_help', 'core_plugin')) {
+            return new help_icon('validationmsg_'.$msgcode, 'core_plugin');
+        }
+
+        return false;
+    }
+
+    /**
+     * Localizes the message additional info if it exists.
+     *
+     * @param string $msgcode
+     * @param array|string|null $addinfo value for the $a placeholder in the string
+     * @return string
+     */
+    public function message_code_info($msgcode, $addinfo) {
+
+        $stringman = get_string_manager();
+
+        if ($addinfo !== null and $stringman->string_exists('validationmsg_'.$msgcode.'_info', 'core_plugin')) {
+            return get_string('validationmsg_'.$msgcode.'_info', 'core_plugin', $addinfo);
+        }
+
+        return '';
+    }
+
     /**
      * Return the information provided by the the plugin's version.php
      *
-     * If version.php was not found in the plugin (which is tolerated for
-     * themes only at the moment), null is returned. Otherwise the array
-     * is returned. It may be empty if no information was parsed (which
-     * should not happen).
+     * If version.php was not found in the plugin, null is returned. Otherwise
+     * the array is returned. It may be empty if no information was parsed
+     * (which should not happen).
      *
      * @return null|array
      */
@@ -194,29 +260,11 @@ class tool_installaddon_validator {
         return $this->rootdir;
     }
 
-    /**
-     * Sets the URL to continue to after successful validation
-     *
-     * @param moodle_url $url
-     */
-    public function set_continue_url(moodle_url $url) {
-        $this->continueurl = $url;
-    }
+    // End of external API.
 
     /**
-     * Get the URL to continue to after successful validation
-     *
-     * Null is returned if the URL has not been explicitly set by the caller.
+     * No public constructor, use {@link self::instance()} instead.
      *
-     * @return moodle_url|null
-     */
-    public function get_continue_url() {
-        return $this->continueurl;
-    }
-
-    // End of external API /////////////////////////////////////////////////////
-
-    /**
      * @param string $zipcontentpath full path to the extracted ZIP contents
      * @param array $zipcontentfiles (string)filerelpath => (bool|string)true or error
      */
@@ -225,15 +273,17 @@ class tool_installaddon_validator {
         $this->extractfiles = $zipcontentfiles;
     }
 
-    // Validation methods //////////////////////////////////////////////////////
+    // Validation methods.
 
     /**
-     * @return bool false if files in the ZIP do not have required layout
+     * Returns false if files in the ZIP do not have required layout.
+     *
+     * @return bool
      */
     protected function validate_files_layout() {
 
         if (!is_array($this->extractfiles) or count($this->extractfiles) < 4) {
-            // We need the English language pack with the name of the plugin at least
+            // We need the English language pack with the name of the plugin at least.
             $this->add_message(self::ERROR, 'filesnumber');
             return false;
         }
@@ -254,7 +304,8 @@ class tool_installaddon_validator {
 
         foreach (array_keys($this->extractfiles) as $filerelname) {
             $matches = array();
-            if (!preg_match("#^([^/]+)/#", $filerelname, $matches) or (!is_null($this->rootdir) and $this->rootdir !== $matches[1])) {
+            if (!preg_match("#^([^/]+)/#", $filerelname, $matches)
+                    or (!is_null($this->rootdir) and $this->rootdir !== $matches[1])) {
                 $this->add_message(self::ERROR, 'onedir');
                 return false;
             }
@@ -272,7 +323,9 @@ class tool_installaddon_validator {
     }
 
     /**
-     * @return bool false if the version.php file does not declare required information
+     * Returns false if the version.php file does not declare required information.
+     *
+     * @return bool
      */
     protected function validate_version_php() {
 
@@ -359,7 +412,9 @@ class tool_installaddon_validator {
     }
 
     /**
-     * @return bool false if the English language pack is not provided correctly
+     * Returns false if the English language pack is not provided correctly.
+     *
+     * @return bool
      */
     protected function validate_language_pack() {
 
@@ -408,9 +463,10 @@ class tool_installaddon_validator {
         return true;
     }
 
-
     /**
-     * @return bool false of the given add-on can't be installed into its location
+     * Returns false of the given add-on can't be installed into its location.
+     *
+     * @return bool
      */
     public function validate_target_location() {
 
@@ -429,29 +485,42 @@ class tool_installaddon_validator {
             throw new coding_exception('Plugin type location does not exist!');
         }
 
-        $target = $plugintypepath.'/'.$this->rootdir;
-
-        if (file_exists($target)) {
-            $this->add_message(self::ERROR, 'targetexists', $target);
+        // Always check that the plugintype root is writable.
+        if (!is_writable($plugintypepath)) {
+            $this->add_message(self::ERROR, 'pathwritable', $plugintypepath);
             return false;
+        } else {
+            $this->add_message(self::INFO, 'pathwritable', $plugintypepath);
         }
 
-        if (is_writable($plugintypepath)) {
-            $this->add_message(self::INFO, 'pathwritable', $plugintypepath);
-        } else {
-            $this->add_message(self::ERROR, 'pathwritable', $plugintypepath);
-            return false;
+        // The target location itself may or may not exist. Even if installing an
+        // available update, the code could have been removed by accident (and
+        // be reported as missing) etc. So we just make sure that the code
+        // can be replaced if it already exists.
+        $target = $plugintypepath.'/'.$this->rootdir;
+        if (file_exists($target)) {
+            if (!is_dir($target)) {
+                $this->add_message(self::ERROR, 'targetnotdir', $target);
+                return false;
+            }
+            $this->add_message(self::WARNING, 'targetexists', $target);
+            if ($this->get_plugin_manager()->is_directory_removable($target)) {
+                $this->add_message(self::INFO, 'pathwritable', $target);
+            } else {
+                $this->add_message(self::ERROR, 'pathwritable', $target);
+                return false;
+            }
         }
 
         return true;
     }
 
-    // Helper methods //////////////////////////////////////////////////////////
+    // Helper methods.
 
     /**
      * Get as much information from existing version.php as possible
      *
-     * @param string full path to the version.php file
+     * @param string $fullpath full path to the version.php file
      * @return array of found meta-info declarations
      */
     protected function parse_version_php($fullpath) {
@@ -517,54 +586,52 @@ class tool_installaddon_validator {
                 list($id, $text) = $token;
             }
             switch ($id) {
-            case T_WHITESPACE:
-            case T_COMMENT:
-            case T_ML_COMMENT:
-            case T_DOC_COMMENT:
-                // Ignore whitespaces, inline comments, multiline comments and docblocks.
-                break;
-            case T_OPEN_TAG:
-                // Start processing.
-                $doprocess = true;
-                break;
-            case T_CLOSE_TAG:
-                // Stop processing.
-                $doprocess = false;
-                break;
-            default:
-                // Anything else is within PHP tags, return it as is.
-                if ($doprocess) {
-                    $output .= $text;
-                    if ($text === 'function') {
-                        // Explicitly keep the whitespace that would be ignored.
-                        $output .= ' ';
+                case T_WHITESPACE:
+                case T_COMMENT:
+                case T_ML_COMMENT:
+                case T_DOC_COMMENT:
+                    // Ignore whitespaces, inline comments, multiline comments and docblocks.
+                    break;
+                case T_OPEN_TAG:
+                    // Start processing.
+                    $doprocess = true;
+                    break;
+                case T_CLOSE_TAG:
+                    // Stop processing.
+                    $doprocess = false;
+        &nb