MDL-38509 Add ability to install add-ons from the remote repository
authorDavid Mudrák <david@moodle.com>
Thu, 28 Mar 2013 23:06:06 +0000 (00:06 +0100)
committerDavid Mudrák <david@moodle.com>
Thu, 28 Mar 2013 23:16:06 +0000 (00:16 +0100)
There is a new hook in the index.php file. If valid HTTP parameter
installaddonrequest is detected, the installer asks the administrator to
confirm the request.

If confirmed, the installer calls download.moodle.org/api/1.2/pluginfo.php
service to get information about the given plugin version. The essential
data are the URL of the ZIP to download and the MD5 hash of the ZIP.
These data must be fetched via HTTPS to protect against MiM attack.

If the ZIP is downloaded and the MD5 content hash is correct, the user
is redirected to the previously implemented ZIP validation page, as if
the ZIP was uploaded manually.

The valid format of the installaddonrequest is documented via the
test_decode_remote_request() unit test method.

admin/tool/installaddon/classes/installer.php
admin/tool/installaddon/classes/pluginfo_client.php [new file with mode: 0644]
admin/tool/installaddon/index.php
admin/tool/installaddon/lang/en/tool_installaddon.php
admin/tool/installaddon/renderer.php
admin/tool/installaddon/tests/installer_test.php

index adac7c1..c83fa09 100644 (file)
@@ -16,7 +16,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Provides tool_installaddon_installer class
+ * Provides tool_installaddon_installer related classes
  *
  * @package     tool_installaddon
  * @subpackage  classes
@@ -46,6 +46,16 @@ class tool_installaddon_installer {
         return new static();
     }
 
+    /**
+     * Returns the URL to the main page of this admin tool
+     *
+     * @param array optional parameters
+     * @return moodle_url
+     */
+    public function index_url(array $params = null) {
+        return new moodle_url('/admin/tool/installaddon/index.php', $params);
+    }
+
     /**
      * Returns URL to the repository that addons can be searched in and installed from
      *
@@ -87,7 +97,7 @@ class tool_installaddon_installer {
             return $this->installfromzipform;
         }
 
-        $action = new moodle_url('/admin/tool/installaddon/index.php');
+        $action = $this->index_url();
         $customdata = array('installer' => $this);
 
         $this->installfromzipform = new tool_installaddon_installfromzip($action, $customdata);
@@ -214,6 +224,208 @@ class tool_installaddon_installer {
         return is_writable($plugintypepath);
     }
 
+    /**
+     * Hook method to handle the remote request to install an add-on
+     *
+     * This is used as a callback when the admin picks a plugin version in the
+     * Moodle Plugins directory and is redirected back to their site to install
+     * it.
+     *
+     * This hook is called early from admin/tool/installaddon/index.php page so that
+     * it has opportunity to take over the UI.
+     *
+     * @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');
+
+        if (is_null($request)) {
+            return;
+        }
+
+        $data = $this->decode_remote_request($request);
+
+        if ($data === false) {
+            echo $output->remote_request_invalid_page($this->index_url());
+            exit();
+        }
+
+        list($plugintype, $pluginname) = normalize_component($data->component);
+
+        $plugintypepath = $this->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)) {
+            $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());
+            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;
+
+        $newlines = array("\r", "\n");
+        $source = str_replace($newlines, '', $source);
+        if (!preg_match('|^https?://|i', $source)) {
+            throw new tool_installaddon_installer_exception('err_download_transport_protocol', $source);
+        }
+        if (!$ch = curl_init($source)) {
+            throw new tool_installaddon_installer_exception('err_curl_init', $source);
+        }
+
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // verify the peer's certificate
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // check the existence of a common name and also verify that it matches the hostname provided
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the transfer as a string
+        curl_setopt($ch, CURLOPT_HEADER, false); // don't include the header in the output
+        curl_setopt($ch, CURLOPT_TIMEOUT, 3600);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); // nah, moodle.org is never unavailable! :-p
+        curl_setopt($ch, CURLOPT_URL, $source);
+
+        $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
+        if (is_readable($cacertfile)) {
+            // Do not use CA certs provided by the operating system. Instead,
+            // use this CA cert to verify the ZIP provider.
+            curl_setopt($ch, CURLOPT_CAINFO, $cacertfile);
+        }
+
+        if (!empty($CFG->proxyhost) and !is_proxybypass($source)) {
+            if (!empty($CFG->proxytype)) {
+                if (strtoupper($CFG->proxytype) === 'SOCKS5') {
+                    curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
+                } else {
+                    curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
+                    curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, false);
+                }
+            }
+
+            if (empty($CFG->proxyport)) {
+                curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost);
+            } else {
+                curl_setopt($ch, CURLOPT_PROXY, $CFG->proxyhost.':'.$CFG->proxyport);
+            }
+
+            if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
+                curl_setopt($ch, CURLOPT_PROXYUSERPWD, $CFG->proxyuser.':'.$CFG->proxypassword);
+                curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC | CURLAUTH_NTLM);
+            }
+        }
+
+        $targetfile = fopen($target, 'w');
+
+        if (!$targetfile) {
+            throw new tool_installaddon_installer_exception('err_download_write_file', $target);
+        }
+
+        curl_setopt($ch, CURLOPT_FILE, $targetfile);
+
+        $result = curl_exec($ch);
+
+        // try to detect encoding problems
+        if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) {
+            curl_setopt($ch, CURLOPT_ENCODING, 'none');
+            $result = curl_exec($ch);
+        }
+
+        fclose($targetfile);
+
+        $curlerrno = curl_errno($ch);
+        $curlerror = curl_error($ch);
+        $curlinfo = curl_getinfo($ch);
+
+        if ($result === false or $curlerrno) {
+            throw new tool_installaddon_installer_exception('err_curl_exec', array('url' => $source, 'errorno' => $curlerrno, 'error' => $curlerror));
+
+        } else if (is_array($curlinfo) and (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200)) {
+            throw new tool_installaddon_installer_exception('err_curl_code', array('url' => $source, 'httpcode' => $curlinfo['http_code']));
+        }
+    }
+
     //// End of external API ///////////////////////////////////////////////////
 
     /**
@@ -324,4 +536,70 @@ class tool_installaddon_installer {
 
         return $files;
     }
+
+    /**
+     * Decode the request from the Moodle Plugins directory
+     *
+     * @param string $request submitted via 'installaddonrequest' HTTP parameter
+     * @return stdClass|bool false on error, object otherwise
+     */
+    protected function decode_remote_request($request) {
+
+        $data = base64_decode($request, true);
+
+        if ($data === false) {
+            return false;
+        }
+
+        $data = json_decode($data);
+
+        if (is_null($data)) {
+            return false;
+        }
+
+        if (!isset($data->name) or !isset($data->component) or !isset($data->version)) {
+            return false;
+        }
+
+        $data->name = s(strip_tags($data->name));
+
+        if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) {
+            return false;
+        }
+
+        list($plugintype, $pluginname) = normalize_component($data->component);
+
+        if ($plugintype === 'core') {
+            return false;
+        }
+
+        if ($data->component !== $plugintype.'_'.$pluginname) {
+            return false;
+        }
+
+        // Keep this regex in sync with the one used by the download.moodle.org/api/x.y/pluginfo.php
+        if (!preg_match('/^[0-9]+$/', $data->version)) {
+            return false;
+        }
+
+        return $data;
+    }
+}
+
+
+/**
+ * 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));
+    }
 }
diff --git a/admin/tool/installaddon/classes/pluginfo_client.php b/admin/tool/installaddon/classes/pluginfo_client.php
new file mode 100644 (file)
index 0000000..5213899
--- /dev/null
@@ -0,0 +1,209 @@
+<?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();
+        if (!empty($curlerrno)) {
+            throw new tool_installaddon_pluginfo_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
+        }
+        $curlinfo = $curl->get_info();
+/*        if ($curlinfo['http_code'] == 404) {
+            throw new tool_installaddon_pluginfo_exception('err_response_http_code_404');
+}*/
+        if ($curlinfo['http_code'] != 200) {
+            throw new tool_installaddon_pluginfo_exception('err_response_http_code', $curlinfo['http_code']);
+        }
+        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,
+        );
+
+        $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
+        if (is_readable($cacertfile)) {
+            // Do not use CA certs provided by the operating system. Instead,
+            // use this CA cert to verify the updates provider.
+            $options['CURLOPT_CAINFO'] = $cacertfile;
+        }
+
+        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));
+    }
+}
index 8ca05d1..cb0d2a6 100644 (file)
@@ -35,6 +35,14 @@ if (!empty($CFG->disableonclickaddoninstall)) {
 
 $installer = tool_installaddon_installer::instance();
 
+$output = $PAGE->get_renderer('tool_installaddon');
+$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);
+
 $form = $installer->get_installfromzip_form();
 
 if ($form->is_cancelled()) {
@@ -55,7 +63,5 @@ if ($form->is_cancelled()) {
     redirect($nexturl);
 }
 
-$output = $PAGE->get_renderer('tool_installaddon');
-$output->set_installer_instance($installer);
-
+// Output starts here.
 echo $output->index_page();
index 6179238..bca494f 100644 (file)
@@ -32,6 +32,7 @@ $string['acknowledgementtext'] = 'I understand that it is my responsibility to h
 $string['featuredisabled'] = 'Add-on installer is disabled at this site.';
 $string['installaddon'] = 'Install add-on!';
 $string['installaddons'] = 'Install add-ons';
+$string['installexception'] = 'Oops... An error occured while trying to install the add-on. Turn debugging mode on to see more details about the error.';
 $string['installfromrepo'] = 'Install add-ons from Moodle plugins directory';
 $string['installfromrepo_help'] = 'You will be redirected to the Moodle plugins directory to search for and install an add-on. Note that your site fullname, URL and major version will be sent as well, to make the installation process easier for you.';
 $string['installfromzip'] = 'Install add-on from the ZIP file';
@@ -49,6 +50,11 @@ $string['permcheckprogress'] = 'Checking for write permission ...';
 $string['permcheckresultno'] = 'Plugin type location <em>{$a->path}</em> not writable';
 $string['permcheckresultyes'] = 'Plugin type location <em>{$a->path}</em> is writable';
 $string['pluginname'] = 'Add-on installer';
+$string['remoterequestalreadyinstalled'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. This plugin is <strong>already installed</strong> at this site.';
+$string['remoterequestconfirm'] = 'There is a request to install add-on <strong>{$a->name}</strong> ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. If you continue, the add-on ZIP package will be downloaded for validation. Nothing will be installed yet.';
+$string['remoterequestinvalid'] = 'There is a request to install add-on from the Moodle plugins directory to this site. Unfortunately, the request is not valid. The add-on cannot be installed.';
+$string['remoterequestpermcheck'] = 'There is a request to install add-on {$a->name} ({$a->component}) version {$a->version} from the Moodle plugins directory to this site. The plugin type location <strong>{$a->typepath}</strong> is <strong>not writable</strong> though. You need to give the write access for the web server user to the plugin type location now. Once the write access is granted, press the continue button to repeat the check.';
+$string['remoterequestpluginfoexception'] = 'Oops... An error occured while trying to obtain information about the add-on {$a->name} ({$a->component}) version {$a->version}. The add-on cannot be installed. Turn debugging mode on to see more details about the error.';
 $string['validation'] = 'Add-on package validation';
 $string['validationmsg_componentmatch'] = 'Full component name';
 $string['validationmsg_componentmismatchname'] = 'Add-on name mismatch';
index 0cc273d..c4c9706 100644 (file)
@@ -116,7 +116,125 @@ class tool_installaddon_renderer extends plugin_renderer_base {
         $out .= $this->output->footer();
 
         return $out;
+    }
+
+    /**
+     * Inform the user about invalid remote installation request.
+     *
+     * @param moodle_url $continueurl
+     * @return string
+     */
+    public function remote_request_invalid_page(moodle_url $continueurl) {
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->box(get_string('remoterequestinvalid', 'tool_installaddon'), 'generalbox', 'notice');
+        $out .= $this->output->continue_button($continueurl, 'get');
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Inform the user that such plugin is already installed
+     *
+     * @param stdClass $data decoded request data
+     * @param moodle_url $continueurl
+     * @return string
+     */
+    public function remote_request_alreadyinstalled_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('remoterequestalreadyinstalled', 'tool_installaddon', $data), 'generalbox', 'notice');
+        $out .= $this->output->continue_button($continueurl, 'get');
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * Let the user confirm the remote installation request.
+     *
+     * @param stdClass $data decoded request data
+     * @param moodle_url $continueurl
+     * @param moodle_url $cancelurl
+     * @return string
+     */
+    public function remote_request_confirm_page(stdClass $data, moodle_url $continueurl, moodle_url $cancelurl) {
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->confirm(get_string('remoterequestconfirm', 'tool_installaddon', $data), $continueurl, $cancelurl);
+        $out .= $this->output->footer();
+
+        return $out;
+    }
 
+    /**
+     * Inform the user that the target plugin type location is not writable.
+     *
+     * @param stdClass $data decoded request data
+     * @param string $plugintypepath full path to the plugin type location
+     * @param moodle_url $continueurl to repeat the write permission check
+     * @param moodle_url $cancelurl to cancel the installation
+     * @return string
+     */
+    public function remote_request_permcheck_page(stdClass $data, $plugintypepath, moodle_url $continueurl, moodle_url $cancelurl) {
+
+        $data->typepath = $plugintypepath;
+
+        $out = $this->output->header();
+        $out .= $this->output->heading(get_string('installfromrepo', 'tool_installaddon'));
+        $out .= $this->output->confirm(get_string('remoterequestpermcheck', 'tool_installaddon', $data), $continueurl, $cancelurl);
+        $out .= $this->output->footer();
+
+        return $out;
+    }
+
+    /**
+     * 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
+     *
+     * 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 moodle_url $continueurl
+     * @return string
+     */
+    public function installer_exception(tool_installaddon_installer_exception $e, 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->continue_button($continueurl, 'get');
+        $out .= $this->output->footer();
+
+        return $out;
     }
 
     // End of the external API /////////////////////////////////////////////////
index 7f53cb1..fd8d1d1 100644 (file)
@@ -74,6 +74,57 @@ class tool_installaddon_installer_test extends advanced_testcase {
             }
         }
     }
+
+    public function test_decode_remote_request() {
+        $installer = testable_tool_installaddon_installer::instance();
+
+        $request = base64_encode(json_encode(array(
+            'name' => '<h1>Stamp collection</h1>"; DELETE FROM mdl_users; --',
+            'component' => 'mod_stampcoll',
+            'version' => 2013032800,
+        )));
+        $request = $installer->testable_decode_remote_request($request);
+        $this->assertTrue(is_object($request));
+        // One, my little hobbit, never trusts the input parameters!
+        $this->assertEquals('Stamp collection&quot;; DELETE FROM mdl_users; --', $request->name);
+        $this->assertEquals('mod_stampcoll', $request->component);
+        $this->assertEquals(2013032800, $request->version);
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Theme with invalid version number',
+            'component' => 'theme_invalid',
+            'version' => '1.0',
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Invalid activity name',
+            'component' => 'mod_invalid_activity',
+            'version' => 2013032800,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Moodle 3.0',
+            'component' => 'core',
+            'version' => 2022010100,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Invalid core subsystem',
+            'component' => 'core_cache',
+            'version' => 2014123400,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+
+        $request = base64_encode(json_encode(array(
+            'name' => 'Non-existing plugintype',
+            'component' => 'david_mudrak',
+            'version' => 2012123199,
+        )));
+        $this->assertSame(false, $installer->testable_decode_remote_request($request));
+    }
 }
 
 
@@ -97,6 +148,10 @@ class testable_tool_installaddon_installer extends tool_installaddon_installer {
         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;
     }